The scheduler emits MessageEvents on a durable, persistent schedule. All schedule state lives in the DB — nothing is held in memory only.
| Type | spec field | Behaviour |
|---|---|---|
cron | Cron expression (e.g. "0 9 * * 1-5") | Fires at each matching time |
interval | Integer seconds (e.g. "30") | Fires every N seconds from last run |
one_shot | ISO-8601 datetime | Fires once at the specified time; disables itself after |
The scheduler runs every 5 seconds:
SELECT * FROM schedules
WHERE enabled = 1 AND next_run_at <= now()
FOR UPDATE SKIP LOCKEDFor each due schedule:
AuditEvent("schedule.skipped"), advance next_run_at, commit.catch_up_policy if last_run_at is stale (indicates missed firings).MessageEvent with source.channel = "scheduler" and source.connector_id = schedule_id.last_run_at; compute new next_run_at.FOR UPDATE SKIP LOCKED ensures safe operation if multiple workers are ever introduced.
| Policy | Behaviour on missed firings |
|---|---|
skip | Advance to the next future slot. Emit one AuditEvent("schedule.missed") per skipped slot. |
run_once | Fire once for the entire missed window. Note the slot count in audit. |
run_all_capped | Fire up to N times for the missed window; skip the remainder. N is configurable. |
The catch-up policy is evaluated on startup (crash recovery) and whenever last_run_at diverges from expected. See architecture/task-engine for the startup reconciliation sequence.
schedules
schedule_id: str PK
name: str
enabled: bool
type: str — cron | interval | one_shot
spec: str — cron expression or interval_seconds
next_run_at: datetime
last_run_at: datetime | None
timezone: str — IANA timezone; default UTC
quiet_hours_policy_id: str | None
catch_up_policy: str — skip | run_once | run_all_capped; default skip
payload: str — JSON: MessageEvent template to emit
created_at: datetime
updated_at: datetimeKey index: idx_schedules_next_run ON schedules(next_run_at) WHERE enabled = 1 — partial index; only active schedules are scanned.