Schedules
A schedule starts a fresh run on a cadence, such as “every hour” or “every weekday at nine”.
Each tick is an independent run with its own history, so nothing accumulates across ticks.
spool sleep is for pausing inside one run; a script that loops forever around spool sleep
replays its whole history on every wake (see Repeating work).
Creating a schedule
Section titled “Creating a schedule”# Humantime interval - fires every N units after the last fire.spool schedule create --every 1m health-probe.sh# prints: dev:default:sch-RVXkHnlMdBf83IyG
# Cron expression - fires on the wall-clock cadence in `--tz`.spool schedule create --cron "0 0 9 * * MON" --tz "America/New_York" weekly-report.sh
# A YAML pipeline instead of a script.spool schedule create --every 1d --as pipeline nightly.yaml
# A stored graph, by name.spool schedule create -w deploy --every 1d --input '{"ref":"main"}'In the console, More > Schedules has New schedule, with the same choices: a script, a pipeline, a stored graph or a single step, on an interval or a cron, with overlap, jitter and tags.
Cadence is --every with a duration (1m, 5h, 1d) or --cron with a 6-field cron
expression (sec min hour dom mon dow); a 5-field one is refused with a hint to add the
seconds. Cron takes an optional --tz (an IANA name) and defaults to UTC; an interval has no
timezone.
Across a clock change a cron keeps local time: a time the clocks pass twice fires once, the
first time, and a time they skip fires just after the jump (02:30 becomes 03:30). A cron that fires
more often than hourly, like 0 */15 * * * *, keeps its step and fires in both passes of a
repeated hour. An expression that can never match, like the 30th of February, is refused. One
whose year field runs out fires its last tick and then pauses itself, with the reason on the
schedule.
A graph schedule names a stored graph. A bare name runs the graph’s published version at each
tick, so republishing changes what later ticks run; deploy@3 pins version 3. Publish on the
canvas, or with spool workflow set deploy deploy.yaml --publish. A graph file passed where the
script or pipeline goes is refused with a pointer to -w, because a file on disk has no
stored version for a tick to run. spool handler set does the same.
If the graph declares an input_schema:,
--input is checked against it when you create the schedule. Ticks are not re-checked, so if
you republish the graph with a stricter schema, check its schedules yourself.
Optional flags:
--name <s>- a label.--tag <t>- tags on every run it starts, beside the automaticschedule:<id>.--as <pipeline|script>- how to read the file. Inferred from the extension when omitted.--requires <cap>- a capability the agent running a script schedule must have. Defaults topolyglot.-w, --workflow <name>[@<version>]- run a stored graph instead of a file.--node <kind> [--config <json>]- run one node kind per tick with no graph around it: a builtin (http,shell,code) or a<connector>.<action>.--input <json>- the run input for a graph or node schedule, read asinput.--overlap <policy>and--jitter <duration>- see below.
Inspecting and controlling
Section titled “Inspecting and controlling”spool schedule list # status table, next/last as durationsspool schedule list -o json # the stored rows, absolute timestampsspool schedule get <id> # cadence, policy, and what it runsspool schedule get <id> -o yaml # the stored row: the body, and how the last tick wentspool schedule run <id> # fire it once, nowspool schedule pause <id> # stop ticking (admin)spool schedule resume <id> # start again (admin)spool schedule delete <id> # delete (admin)To change the body, read it from get -o yaml (under kind), edit a copy, and pass the file
to update:
spool schedule get sch-abc -o yamlspool schedule update sch-abc nightly.yamlA schedule’s page in the console has Run now, Pause/Resume, Edit and
Delete, shows how its last tick went (and why, when it was skipped or turned away), and
lists its recent runs. On the Runs page, filter by the schedule:<id> tag to see every run it
started.
Running one now
Section titled “Running one now”spool schedule run <id> starts the body once, now, and prints the run id; Run now in the
console does the same and opens the run. Use it after editing the body, or when what it
reconciles has drifted and the next tick is hours away.
spool schedule run sch-abc | xargs spool logs -fA manual run is not a tick:
- The cadence doesn’t move. A daily schedule run by hand at 2:55 still fires at 3:00.
--overlapdoesn’t apply to it, and it doesn’t count as the previous run for the next tick. Underbuffer-one, though, a held tick waits for a manual run that is executing, so the schedule never has two going.- It works on a paused schedule.
- It carries the
schedule:<id>tag plustrigger:manual; filter on that to leave it out when reasoning about tick timing.
If runs of the schedule are already going, including one waiting for its concurrency: group,
nothing starts: the CLI lists what is running and exits non-zero, and the console asks.
--alongside (Run alongside in the console) starts another anyway. To have a new run
replace the old one, put policy: cancel-running in the body’s
concurrency: block, which applies to ticks and manual
runs alike.
If the body’s concurrency group is busy with other work, a manual run waits in that queue,
where a tick would be skipped. Graph and node schedules run with the --input they were
created with; there is no per-run override.
Three cases print no run id and exit non-zero: a handler subscribed to schedule.tick owns the
firing (its runs carry the schedule’s tag), the body’s admission turned it away (a full
concurrency queue, policy: skip, a spent throttle:), or runs are already going and you
didn’t pass --alongside.
Editing
Section titled “Editing”spool schedule update <id> (admin) takes the same flags as create and changes only what you
pass. The schedule keeps its id, so the schedule:<id> tag on its earlier runs still finds it.
spool schedule update sch-abc --cron "0 0 3 * * *" --tz Europe/Stockholmspool schedule update sch-abc nightly.yaml # new body, same schedulespool schedule update sch-abc --overlap buffer-one --jitter 5mspool schedule update sch-abc --input '{"ref":"release"}' # graph schedulesspool schedule update sch-abc --clear name # back to its defaultA cadence or jitter change measures the next deadline from now. Any other edit leaves the clock alone, so a new body takes effect at the next tick.
When a tick arrives and the last one is still going
Section titled “When a tick arrives and the last one is still going”--overlap says what the arriving tick does. The deadline advances either way.
--overlap |
The arriving tick |
|---|---|
skip (default) |
is dropped. A slow run costs you the ticks it overran. |
allow |
fires anyway. Ticks overlap. |
buffer-one |
is remembered, and fires when the running one finishes. |
cancel-other |
cancels what’s running, then fires. |
spool schedule create --every 5m --overlap buffer-one reconcile.shbuffer-one is for ticks that must not be lost, like a reconcile that sometimes overruns. It
holds only one missed deadline, so a schedule that is always slower than its cadence can’t
build a backlog. cancel-other is for work a newer tick makes pointless (deploy latest,
warm a cache); cancelling a run
takes its children with it. allow is for
independent samples, like a prober.
The body’s own admission (the concurrency: and
throttle: blocks of a pipeline or a graph) is a separate, later gate. A tick never queues
there: if the body’s concurrency group is busy with other work, or its throttle is spent, the
tick is skipped, since another is coming. A buffer-one tick turned away like that is tried
again every 30 seconds instead, and held like a missed tick once the next tick is closer than
that.
Spreading the load
Section titled “Spreading the load”Fifty schedules written 0 0 * * * * all fire at the top of the hour. --jitter moves each
schedule’s deadlines by a random offset within the window you give:
spool schedule create --cron '0 0 * * * *' --jitter 5m warm-cache.shThe offset is drawn once per schedule and kept, so an hourly schedule stays hourly and lands
at the same point in the window every time, across restarts. It shifts every deadline by the
same amount, so 0 */5 * * * * with --jitter 5m still fires twelve times an hour. Don’t set
it on a schedule that has to land on the minute. --jitter is capped at 24h.
An interval has no slots of its own, so a jittered one is lined up on its period counted from
midnight UTC, 1 January 1970, and then offset: --every 1h --jitter 5m fires once an hour,
somewhere in the first five minutes past the hour, at the same point each time.
No backfill
Section titled “No backfill”Missed ticks don’t fire later. A spool dev closed over the weekend fires the next future tick
when it comes back, and nothing for the ticks in between; resuming a paused schedule works the
same way. If every interval must be accounted for, have each run record how far it got (a
watermark) and pick up from there.
When a tick can’t start
Section titled “When a tick can’t start”The deadline advances before the run is submitted, so a failure right at that moment can lose one tick. A tick that fails on the platform’s side is tried again 30 seconds later, as the same tick with the same run ids, until the next tick is closer than that.
A tick that can’t start because of the schedule itself, such as a concurrency: group that
doesn’t render, is skipped, and the schedule’s page says why; the next tick finds out whether
it was fixed. Deleting a graph pauses the schedules that run it, with the reason on the
schedule.