Skip to content

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).

Terminal window
# 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 automatic schedule:<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 to polyglot.
  • -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 as input.
  • --overlap <policy> and --jitter <duration> - see below.
Terminal window
spool schedule list # status table, next/last as durations
spool schedule list -o json # the stored rows, absolute timestamps
spool schedule get <id> # cadence, policy, and what it runs
spool schedule get <id> -o yaml # the stored row: the body, and how the last tick went
spool schedule run <id> # fire it once, now
spool 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:

Terminal window
spool schedule get sch-abc -o yaml
spool schedule update sch-abc nightly.yaml

A 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.

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.

Terminal window
spool schedule run sch-abc | xargs spool logs -f

A 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.
  • --overlap doesn’t apply to it, and it doesn’t count as the previous run for the next tick. Under buffer-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 plus trigger: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.

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.

Terminal window
spool schedule update sch-abc --cron "0 0 3 * * *" --tz Europe/Stockholm
spool schedule update sch-abc nightly.yaml # new body, same schedule
spool schedule update sch-abc --overlap buffer-one --jitter 5m
spool schedule update sch-abc --input '{"ref":"release"}' # graph schedules
spool schedule update sch-abc --clear name # back to its default

A 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.
Terminal window
spool schedule create --every 5m --overlap buffer-one reconcile.sh

buffer-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.

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:

Terminal window
spool schedule create --cron '0 0 * * * *' --jitter 5m warm-cache.sh

The 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.

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.

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.