Cron trigger
Fire a workflow on a schedule. No chain config needed — a cron → HTTP → notify project runs with zero networks and zero signers.
workflows:
daily-report:
trigger:
cron:
expression: "0 9 * * *" # every day at 09:00 UTC
catch_up: true # optional — replay ticks missed while down
steps:
- id: fetch
http_call:
url: https://api.example.com/stats
- id: report
notify:
channel: ops
message: "daily stats: ${{ steps.fetch.output.body }}"| Field | Required | Description |
|---|---|---|
expression | ✅ | Standard 5-field cron expression (minute hour day month weekday) |
tz | UTC | IANA timezone name, e.g. Europe/London. Ticks compute in the named zone (DST-correct); an unknown name is a startup error. UTC / Etc/UTC mean the plain UTC path. See Timezones |
catch_up | false | Replay schedule slots missed while rflow was down — see below |
The trigger payload exposes trigger.scheduled_for (the ISO tick) and
trigger.workflow to expressions.
Exactly-once for schedules
Each scheduled firing claims a trigger key derived from its scheduled time
(never now()), exactly like events claim chain_id:tx_hash:log_index — so a
restart around a tick never double-fires.
catch_up — missed ticks
By default, a tick that comes due while the process is down is skipped: a restart schedules from the next future occurrence.
With catch_up: true, the last fired tick is persisted per workflow
(rflow.cron_state) and on boot every slot missed while rflow was down is
claimed — each with its own scheduled-time key, so a repeated boot can never
double-fire the same tick.
Timezones
By default a cron runs in UTC. Set tz to any IANA timezone
name and the
schedule is computed on that zone's local wall clock:
cron:
expression: "0 9 * * 1" # every Monday at 09:00 LOCAL
tz: "Europe/London" # 09:00 GMT in winter, 09:00 BST in summerThe zone is validated against the IANA database at config-validation time — an
unknown name (rflow validate or boot) is a hard error, never silently UTC.
DST is handled honestly:
- Spring forward (a skipped hour): a tick that lands in the gap does not hang and does not double-fire — it snaps to the first valid instant after the gap.
- Fall back (a repeated hour): a fixed-time tick fires once, not twice.
The dedupe key stays a UTC instant regardless of zone, so exactly-once holds across restarts and daylight transitions exactly as it does for UTC crons.
Examples
cron: { expression: "*/5 * * * *" } # every 5 minutes
cron: { expression: "0 * * * *" } # hourly, on the hour
cron: { expression: "0 9 * * 1", catch_up: true } # Mondays 09:00 UTC, replayed if missed
cron: { expression: "0 9 * * 1", tz: "America/New_York" } # Mondays 09:00 New York timeFor a fixed cadence (every: 30s) rather than a calendar schedule, use the
interval trigger — it allows sub-minute
cadences, which cron cannot express.
Combine with send_transaction for DCA and
treasury sweeps, or keep it fully off-chain. To rehearse a cron workflow's steps
without waiting for the schedule, use rflow test.