Skip to content

Triggers

Implemented

The triggers block on a pipeline declares when it should run automatically — on file changes, on a cron schedule, or by explicit invocation only.

yaml
pipeline:
  name: ingest-inbox
  triggers:
    - type: file
      watch: inbox/*.txt
    - type: cron
      schedule: "0 9 * * 1-5"
      timezone: America/New_York
  steps:
    - name: process
      action: ai
      prompt: "Process the latest item"
  output: "{{ process.text }}"

triggers is optional. A pipeline without it behaves as if it had triggers: [manual] — it only runs when invoked explicitly.

When triggers actually fire

Triggers are armed only when the pipeline runs under jigspec app. They are ignored by jigspec run.

When the app starts, it does a one-shot scan of the working directory for *.pipe.yaml files (excluding node_modules/ and .jigspec/), parses each, and arms every file and cron trigger it finds. Edits to a pipeline's triggers: block require restarting the app to re-arm — there is no live YAML watcher in v1.

One-shot scan

Adding, removing, or editing triggers in a YAML file does not take effect until you restart jigspec app. The pipeline body itself (steps, prompts, etc.) is re-read on every fire, so step changes do not require a restart.

Manual, conversational, and ambient triggers do nothing at arm time — they exist as schema-level declarations only.

Trigger types

TypeStatusFires when
manualImplementedRun button or CLI invocation only — never automatic
fileImplementedA path matching watch is added, changed, or deleted
cronImplementedThe cron expression matches the current time
conversationalSchema-only(no runtime behavior in v1)
ambientSchema-only(no runtime behavior in v1)

file

Implemented

Watches a glob pattern relative to the project root. When a matching path is added, modified, or deleted, the pipeline fires with a debounce window collapsing rapid bursts into a single run.

yaml
triggers:
  - type: file
    watch: inbox/*.txt
    debounce_ms: 500
    concurrency: skip
FieldTypeDefaultDescription
watchstring (glob)requiredGlob pattern, relative to project root
debounce_msint > 01000Trailing-edge debounce in milliseconds
concurrencyskip | queue | allowskipBehavior when a fire arrives mid-run — see Concurrency

Path safety

watch patterns are confined to the project workspace. The validator rejects:

  • Absolute paths (anything starting with /)
  • Any segment equal to ..
yaml
# rejected at validate time
- type: file
  watch: /etc/hosts            # absolute — error
- type: file
  watch: ../../outside/*.txt   # parent traversal — error

Symlinks that point outside the workspace are silently ignored at watch time as a second layer of defense.

Glob examples

yaml
- type: file
  watch: inbox/*.txt           # files directly under inbox/
- type: file
  watch: src/**/*.ts           # any TS file under src/, recursively
- type: file
  watch: data/2026-*/report.md # interpolated date directory

Dotfiles are not matched by default (consistent with shell glob semantics). The watcher excludes node_modules/ and .jigspec/ from descent.

File path is not yet a step input

File events fire the pipeline with no input. The pipeline runs against its declared input block — if you need the changed path, plumb it in via a code step that reads the directory.

cron

Implemented

Schedules the pipeline against a cron expression. Backed by croner, so all of croner's syntax is available.

yaml
triggers:
  - type: cron
    schedule: "0 9 * * 1-5"
    timezone: America/New_York
    concurrency: skip
FieldTypeDefaultDescription
schedulestringrequired5-field cron, 6-field cron, or named preset
timezonestring (IANA)UTCIANA timezone, e.g. America/New_York
concurrencyskip | queue | allowskipBehavior when a tick arrives mid-run

Schedule syntax

yaml
- schedule: "0 9 * * 1-5"      # 5-field: 09:00 Mon–Fri
- schedule: "* * * * * *"      # 6-field: every second
- schedule: "@hourly"          # preset: top of every hour
- schedule: "@daily"           # preset: 00:00 every day

Named presets supported: @hourly, @daily, @weekly, @monthly, @yearly. The 6-field form gives second-level precision.

Timezone handling

Both invalid cron expressions and unknown IANA timezone strings are caught at arm time. The trigger logs a warning, stays disarmed, and the rest of the pipeline (and other triggers) keep running:

[TriggerManager] WARN: cron trigger skipped — invalid schedule/timezone
for "my-pipeline" (schedule: every-fifteen-minutes, timezone: UTC): ...

The Triggers panel surfaces a nextRunAt ISO timestamp computed at arm time and refreshed after every fire.

manual

Implemented

The default. A manual trigger arms no watcher and registers no schedule — the pipeline only runs when explicitly invoked (Run button, jigspec run, or programmatic call).

Two equivalent forms:

yaml
# explicit object
triggers:
  - type: manual

# bare-string shorthand
triggers: [manual]

The bare string is expanded to { type: "manual" } by the schema preprocessor before validation.

A pipeline that omits triggers: entirely behaves identically to one with [manual].

conversational / ambient

Schema-only

Both types parse and validate, but have no runtime behavior in v1. They are placeholders for future trigger sources (an MCP-driven chat trigger and a condition-watching ambient trigger).

yaml
triggers:
  - type: conversational
    description: "When the user asks for a daily summary"

  - type: ambient
    condition: "GitHub PR opened on jigspec/jigspec"

When the scanner encounters either, it emits a warning at startup and moves on. No watcher, no schedule, no fire.

Concurrency

When a trigger fires while a previous run from the same trigger is still in flight, concurrency decides what happens:

ModeBehavior
skip (default)The new fire is dropped. Use this for idempotent jobs where running once is enough.
queueAt most one pending fire is buffered. When the in-flight run completes, the buffered fire kicks off. Additional fires while one is already pending are coalesced into that single pending fire — no stacking.
allowEvery fire starts a new run immediately, in parallel. No state guard.

Each trigger gets its own concurrency state — a file and a cron trigger on the same pipeline do not block each other.

yaml
triggers:
  - type: cron
    schedule: "*/5 * * * *"
    concurrency: queue       # never lose a tick
  - type: file
    watch: inbox/*.json
    concurrency: allow       # process every drop in parallel

Triggers Panel

When running under jigspec app, the right-side Triggers panel lists every armed trigger across every pipeline in the workspace. For each entry it shows:

  • Pipeline name + trigger type
  • Pattern (glob for file, schedule for cron)
  • Concurrency mode
  • lastFiredAt and last runId (clickable — opens the run viewer)
  • nextRunAt for cron triggers

For full UI behavior see Using the App — Triggers Panel.

Limitations (v1)

  • One-shot scan. Triggers are armed once at jigspec app startup. Adding or editing a triggers: block requires restarting the app. The pipeline body (steps, prompts, code) is re-read on every fire and does not need a restart.
  • App-only. jigspec run ignores triggers entirely — it executes the requested pipeline once and exits. Triggers only fire under jigspec app.
  • Path safety enforced at validate time. Absolute paths and .. segments in watch patterns are rejected by the schema, not by the watcher — jigspec validate catches them before the app ever runs.
  • No file path injected as input. File triggers fire the pipeline against its declared input block; the changed path is not yet exposed as a built-in reference.
  • Conversational and ambient are schema stubs. They parse but do not arm.