Skip to content

Cron-driven workflow

In this tutorial you’ll register a cron schedule for the workflow you built in Build your first workflow, then drive a dispatcher tick to fire it.

Production deployments drive the tick from pg_cron or a worker process; we’ll do it manually with POST /api/admin/tick so you can see exactly what happens.

Terminal window
URL=http://localhost:3000
H="Authorization: Bearer $(thodare token)"
WFID=<your workflow id from the previous tutorial>
curl -sX POST "$URL/api/schedules" \
-H "$H" -H 'content-type: application/json' \
-d '{
"workflowId": "'$WFID'",
"cron": "* * * * *",
"payload": { "tag": "minutely-test" }
}' | jq
# → { "id": "sch_…", "workflowId": "...", "cron": "* * * * *", "payload": { "tag": "minutely-test" } }
  • The schedule lives in your org’s schedules table.
  • * * * * * means “every minute” — useful for testing. Real schedules use 5-field cron: 0 9 * * 1-5 is “9am Monday-Friday”.
  • The schedule references a workflowId you own. Cross-org binding is refused at create time.
Terminal window
curl -sX POST "$URL/api/admin/tick" -H "$H" | jq
# → {
# "fired": [{ "scheduleId": "sch_…", "runId": "<uuid>" }],
# "failed": [],
# "skippedAlreadyFired": 0,
# "skippedNotMatching": 0,
# "skippedExpired": 0
# }

If you tick twice in the same minute, the second tick has fired: [] and skippedAlreadyFired: 1 — the row’s last_fired_at column tracks “this cutoff already went out.”

The claim is persistent and atomic at the row level (SELECT … FOR UPDATE inside a transaction, then UPDATE last_fired_at). Multi-process tickers (e.g., your CI test harness running concurrently with a real cron worker) compete for the same row and exactly one wins.

Terminal window
RUNID=<runId from step 2>
curl -s "$URL/api/runs/$RUNID" -H "$H" | jq '{state, output}'

Each tick that fires the schedule creates a new run. Inspect each via /api/runs/:runId.

Terminal window
curl -s "$URL/api/schedules" -H "$H" | jq
curl -sX DELETE "$URL/api/schedules/sch_..." -H "$H"
# → 204
  • Schedules are rows in your org’s schedules table; cross-org binding is refused.
  • The tick endpoint reads schedules across all orgs but uses per-(scheduleId, cutoff) row locks to dispatch each exactly once.
  • For production, drive the tick from pg_cron or a 60s-interval worker pod. See Schedule a workflow for patterns.