Scheduler
The scheduler turns a cron expression into a guaranteed-once-per-occurrence event. Everything time-based in Subspace, date reminders, recurring agent runs, trigger schedules, and the app’s own backups, rides this one engine.
How a schedule fires
Section titled “How a schedule fires”Each row in ops.schedules (cron, target, a single_flight_key, last_run) drives
a croner job. When the cron boundary hits, fire
runs one Postgres transaction that:
-
Takes a single-flight lock
pg_try_advisory_xact_lockon the schedule’s key. If another fire for the same schedule is already mid-transaction, this one backs off rather than racing it. -
Checks the watermark
If
last_runis already at or past the occurrence being fired, the fire is a no-op, this is what makes a redelivered or duplicate fire harmless. -
Commits watermark + event + run row
last_runadvances, aschedule-firedevent lands on the events spine, and a row lands in the task queue’s Scheduled section, all in the same transaction.
After that transaction commits, the fire dispatches its target: an agentPageId starts
an agent run, a systemJob name runs a built-in maintenance job, or a triggerPageId
fires a schedule-kind trigger page. That dispatch happens outside
the transaction, but is itself claim-keyed on the occurrence timestamp, so a redelivered
kick can’t double-run it.
Missed runs
Section titled “Missed runs”If the server was down across one or more scheduled occurrences, boot catches up exactly
one fire per schedule: it walks forward from the schedule’s last_run watermark
computing each missed occurrence, and fires using the latest one, not the first.
Firing the latest occurrence (rather than the earliest) means the watermark lands
current in a single catch-up fire even when two or more periods were slept through, so
the next boot doesn’t see a stale watermark and fire again.
The schedules settings UI
Section titled “The schedules settings UI”/settings/schedules lists every schedule: name, cron expression, last run (with its
status: ran / failed / running), and next occurrence, computed live from the cron.
New schedule creates one from a name and a cron string, validated before it saves.
The same upcoming and past runs also render in the task queue’s
Scheduled section, so you don’t need to leave the queue to see what’s coming.
System jobs ride the same scheduler
Section titled “System jobs ride the same scheduler”Subspace’s own maintenance work is seeded as ordinary schedules targeting a
systemJob name rather than an agent page:
| Job | What it does |
|---|---|
blob-gc |
Sweeps orphaned file blobs no page references anymore. |
| Nightly backup | Runs the WAL-G/restic backup pass, see Backup & restore. |
| Retention | Applies retention policy and rebuilds any consumer (search index, OKF export, blob replica) that policy touched. |
| Restore drill | Periodically restores the latest backup into a scratch environment to prove it actually works. |
Because these are ordinary ops.schedules rows, they show up in /settings/schedules
and the task queue exactly like an agent schedule does.
Triggers can target a schedule too
Section titled “Triggers can target a schedule too”A trigger page can fire on a schedule instead of (or alongside)
an event filter, “9am daily, run the research heartbeat” is a schedule-kind trigger under
the hood, using the same fire path and the same missed-run catch-up described above.