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.
Trigger types
Section titled “Trigger types”| Type | Fires when… | Required fields |
|---|---|---|
cron | A cron schedule is due. | expression (+ optional timezone) |
webhook | An HTTP request hits the flow’s webhook endpoint. | — (optional secret) |
sqs | A message arrives on an SQS queue. | queue-url |
sns | An AWS SNS notification arrives. | topic-arn |
kinesis | A record lands on a Kinesis stream. | stream-name |
mail | A matching message arrives in a watched IMAP mailbox. | environment (the IMAP connection) |
filesystem | A file appears/changes in a watched directory (local or SFTP). | path |
log-monitor | A configured log pattern matches. | path, pattern |
system-event | A system-level log event matches (journald, Windows Event Log, or the macOS unified log). | — (optional unit, priority, pattern) |
flow-event | Another flow emits a lifecycle event. | events (+ optional flow) |
task-event | A human-task lifecycle event fires. | — (optional events) |
git | A Git push webhook arrives. | events (+ optional secret) |
Each trigger is a named, typed entry in the flow’s triggers: block.
The triggers: block
Section titled “The triggers: block”triggers: is a sequence — a list of entries, each with a name: and a type:, plus that type’s own fields:
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 verificationEvery entry needs a unique name: and a type:. The type-specific fields (expression, path, queue-url, …) come from the trigger types table above.
Cron scheduling
Section titled “Cron scheduling”Add a triggers: entry of type cron with a standard 5-field expression:
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).
Cron syntax
Section titled “Cron syntax”┌───────────── 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).
| Pattern | Expression |
|---|---|
| Every 15 minutes | */15 * * * * |
| Daily at 9 AM | 0 9 * * * |
| Weekdays at 8 AM | 0 8 * * 1-5 |
| Monday at 6 AM | 0 6 * * 1 |
| First of month, midnight | 0 0 1 * * |
| Every 6 hours | 0 */6 * * * |
Scheduler behaviour
Section titled “Scheduler behaviour”- Cron runs in the trigger’s
timezone:(an IANA name such asEurope/Berlin), defaulting to UTC when absent or unrecognised. - Multiple cron triggers per flow are fully supported — give each its own
name:andexpression:. 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
schedulerun source, so${source}readsscheduleon a cron-driven run (see Which trigger fired). - The scheduler picks up changes automatically; remove the trigger entry to unschedule.
Static inputs — the with: block
Section titled “Static inputs — the with: block”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:
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 declaredinput: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-flowrejects awith: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.
Pinning a trigger to an environment (on:)
Section titled “Pinning a trigger to an environment (on:)”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:
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: trueon:names an environment, not a connection or a connection alias. It must be one of the flow’s declaredusing: - environment/<name>entries — otherwisevalidate-flowraises V1317. Omittingon:uses the flow’s default (first-declared) environment.- At fire time the trigger’s
on:is threaded as theon=<env>parameter, exactly as if a caller had selected that environment. - A trigger may carry both
on:andwith:.
Which trigger fired
Section titled “Which trigger fired”Two payload values tell a step how the run started:
${source}— the canonical name of the run source:schedule(cron),trigger(event triggers), orcli/api/flow/serve/warmupfor 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).
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: trueThe 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.
Health-check pattern
Section titled “Health-check pattern”A scheduled flow that alerts on failure combines cron, retry, and messaging:
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-Type — http.* auto-parses in that case. Against a non-JSON endpoint, reach into it with path() instead: path(check.body, '$.status') != 'healthy'.