Skip to content

Triggers & Scheduling

A trigger is what kicks a flow off without a human running it. A flow can have multiple triggers — fired by a cron at 09:00 and by a webhook on demand and by a file landing in an SFTP directory. The flow body does not change based on which trigger fired it; the difference shows up in the payload, which any step can read.

TypeFires when…Required fields
cronA cron schedule is due.expression (+ optional timezone)
webhookAn HTTP request hits the flow’s webhook endpoint.— (optional secret)
sqsA message arrives on an SQS queue.queue-url
snsAn AWS SNS notification arrives.topic-arn
kinesisA record lands on a Kinesis stream.stream-name
mailA matching message arrives in a watched IMAP mailbox.environment (the IMAP connection)
filesystemA file appears/changes in a watched directory (local or SFTP).path
log-monitorA configured log pattern matches.path, pattern
system-eventA system-level log event matches (journald, Windows Event Log, or the macOS unified log).— (optional unit, priority, pattern)
flow-eventAnother flow emits a lifecycle event.events (+ optional flow)
task-eventA human-task lifecycle event fires.— (optional events)
gitA Git push webhook arrives.events (+ optional secret)

Each trigger is a named, typed entry in the flow’s triggers: block.

triggers: is a sequence — a list of entries, each with a name: and a type:, plus that type’s own fields:

Terminal window
triggers:
- name: nightly
type: cron
expression: "0 2 * * *" # 5-field cron — see syntax below
- name: on-upload
type: filesystem
path: /incoming # directory to watch
recursive: true
debounce: 5000 # ms to coalesce rapid changes
glob: "*.csv" # only files matching this glob
- name: on-demand
type: webhook
secret: ${secret:webhooks/on-demand} # enables HMAC-SHA-256 verification

Every entry needs a unique name: and a type:. The type-specific fields (expression, path, queue-url, …) come from the trigger types table above.

Add a triggers: entry of type cron with a standard 5-field expression:

Terminal window
status: active
triggers:
- name: daily-report
type: cron
expression: "0 9 * * *" # every day at 09:00
timezone: Europe/Berlin
using:
- zenvara/http
output:
value: !str
steps:
- $report:
invoke: http.get
with:
Url: "https://api.example.com/daily-report"
- return:
value: "${report.body}"

There is no top-level schedule: field. It was superseded by triggers: and is now rejected outright (V1201), as is a top-level name: — a flow’s identity is its filename (V1004).

Terminal window
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
* * * * *

Special characters: * (any), , (list), - (range), / (step).

PatternExpression
Every 15 minutes*/15 * * * *
Daily at 9 AM0 9 * * *
Weekdays at 8 AM0 8 * * 1-5
Monday at 6 AM0 6 * * 1
First of month, midnight0 0 1 * *
Every 6 hours0 */6 * * *
  • Cron runs in the trigger’s timezone: (an IANA name such as Europe/Berlin), defaulting to UTC when absent or unrecognised.
  • Multiple cron triggers per flow are fully supported — give each its own name: and expression:. A flow can fire on several schedules at once (e.g. an incremental sync every 15 minutes and a full reload nightly).
  • Cron fires inject the schedule run source, so ${source} reads schedule on a cron-driven run (see Which trigger fired).
  • The scheduler picks up changes automatically; remove the trigger entry to unschedule.

A trigger can supply static flow inputs with a with: block. This lets you schedule a parameterized run natively — no external scheduler, no companion invoke step. Each value overrides the matching input: default when that trigger fires:

Terminal window
status: active
input:
full-reload: !bool? false # incremental by default
triggers:
- name: incremental
type: cron
expression: "*/15 * * * *" # no with: → uses the default (full-reload = false)
- name: weekly-full
type: cron
expression: "15 3 * * 0"
timezone: "UTC"
with:
full-reload: true # this run does a full re-push
using:
- zenvara/http
output:
fired: !str
steps:
- $sync:
invoke: http.post
with:
Url: "https://api.example.com/sync"
- return:
fired: "${trigger.name}"
  • Trigger wins over defaults. A with: value replaces the declared input: default for that key, exactly as if a caller had run the flow with { "full-reload": true }. Inputs not named keep their defaults.
  • Static literals only. Values must be literals whose type matches the declared input — no expressions, no ${secret:…}.
  • Validated at deploy time. validate-flow rejects a with: key that is not a declared flow input (V1067) or whose value has the wrong type / is not a literal (V1069).
  • Works on every trigger type. The per-event payload (webhook body, SQS message) is unaffected and still arrives alongside.

A trigger can pin the run to a specific environment with on:. This is what lets one flow fire against different targets on different schedules — the dev→prod pattern:

Terminal window
using:
- zenvara/http
- environment/pim-prod # first env entry = the flow default
- environment/pim-test # declared → usable as a trigger on: target
triggers:
- name: prod-incremental
type: cron
expression: "*/15 * * * *"
on: pim-prod # fire against the prod environment
- name: test-nightly-full
type: cron
expression: "0 2 * * *"
on: pim-test # fire against the test environment
with:
full-reload: true
  • on: names an environment, not a connection or a connection alias. It must be one of the flow’s declared using: - environment/<name> entries — otherwise validate-flow raises V1317. Omitting on: uses the flow’s default (first-declared) environment.
  • At fire time the trigger’s on: is threaded as the on=<env> parameter, exactly as if a caller had selected that environment.
  • A trigger may carry both on: and with:.

Two payload values tell a step how the run started:

  • ${source} — the canonical name of the run source: schedule (cron), trigger (event triggers), or cli / api / flow / serve / warmup for the non-trigger paths. It is a string, not an object — it tells you the class of source, not which specific trigger.
  • ${trigger.name} — the name of the specific trigger that fired, plus ${trigger.type}. This is how you branch on which trigger started the run. Cron triggers additionally expose fire-time metadata under their own name — e.g. ${trigger.nightly.fired-at} (kebab-case, cron-only).
Terminal window
output:
ok: !bool
steps:
- if: "trigger.name = 'on-demand'"
do:
- log: "On demand"
- if: "source = 'schedule'"
do:
- log: "Cron fired at ${trigger.nightly.fired-at}"
- return:
ok: true

The per-event payload (a webhook body, an SQS message) arrives under the firing trigger’s name, so a step can read the incoming request that fired it.

A scheduled flow that alerts on failure combines cron, retry, and messaging:

Terminal window
status: active
triggers:
- name: health-check
type: cron
expression: "0 * * * *"
using:
- zenvara/http
- zenvara/notify
output:
status: !str
steps:
- $check:
invoke: http.get
with:
Url: "https://api.example.com/health"
retry: { max-attempts: 3 }
- if: "check.body.status != 'healthy'"
do:
- $alert:
invoke: notify.send
with: { Message: "Health check failed: ${check.body.status}" }
- return:
status: "${check.body.status}"

Dotted access into body works here because the health endpoint responds with a JSON Content-Typehttp.* auto-parses in that case. Against a non-JSON endpoint, reach into it with path() instead: path(check.body, '$.status') != 'healthy'.