Triggers
ImplementedThe triggers block on a pipeline declares when it should run automatically — on file changes, on a cron schedule, or by explicit invocation only.
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
| Type | Status | Fires when |
|---|---|---|
manual | Implemented | Run button or CLI invocation only — never automatic |
file | Implemented | A path matching watch is added, changed, or deleted |
cron | Implemented | The cron expression matches the current time |
conversational | Schema-only | (no runtime behavior in v1) |
ambient | Schema-only | (no runtime behavior in v1) |
file
ImplementedWatches 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.
triggers:
- type: file
watch: inbox/*.txt
debounce_ms: 500
concurrency: skip| Field | Type | Default | Description |
|---|---|---|---|
watch | string (glob) | required | Glob pattern, relative to project root |
debounce_ms | int > 0 | 1000 | Trailing-edge debounce in milliseconds |
concurrency | skip | queue | allow | skip | Behavior 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
..
# rejected at validate time
- type: file
watch: /etc/hosts # absolute — error
- type: file
watch: ../../outside/*.txt # parent traversal — errorSymlinks that point outside the workspace are silently ignored at watch time as a second layer of defense.
Glob examples
- 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 directoryDotfiles 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
ImplementedSchedules the pipeline against a cron expression. Backed by croner, so all of croner's syntax is available.
triggers:
- type: cron
schedule: "0 9 * * 1-5"
timezone: America/New_York
concurrency: skip| Field | Type | Default | Description |
|---|---|---|---|
schedule | string | required | 5-field cron, 6-field cron, or named preset |
timezone | string (IANA) | UTC | IANA timezone, e.g. America/New_York |
concurrency | skip | queue | allow | skip | Behavior when a tick arrives mid-run |
Schedule syntax
- 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 dayNamed 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
ImplementedThe 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:
# 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-onlyBoth 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).
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:
| Mode | Behavior |
|---|---|
skip (default) | The new fire is dropped. Use this for idempotent jobs where running once is enough. |
queue | At 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. |
allow | Every 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.
triggers:
- type: cron
schedule: "*/5 * * * *"
concurrency: queue # never lose a tick
- type: file
watch: inbox/*.json
concurrency: allow # process every drop in parallelTriggers 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 forcron) - Concurrency mode
lastFiredAtand lastrunId(clickable — opens the run viewer)nextRunAtfor cron triggers
For full UI behavior see Using the App — Triggers Panel.
Limitations (v1)
- One-shot scan. Triggers are armed once at
jigspec appstartup. Adding or editing atriggers: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 runignores triggers entirely — it executes the requested pipeline once and exits. Triggers only fire underjigspec app. - Path safety enforced at validate time. Absolute paths and
..segments inwatchpatterns are rejected by the schema, not by the watcher —jigspec validatecatches them before the app ever runs. - No file path injected as input. File triggers fire the pipeline against its declared
inputblock; the changed path is not yet exposed as a built-in reference. - Conversational and ambient are schema stubs. They parse but do not arm.