On this page
Recurring jobs
A schedule submits a fresh job on a cron expression, with the same routing, sizing, and secret-reference contract as a direct submit. Schedules live in configured Stado storage and are evaluated every coordinator tick.
stado schedule create --cron "0 2 * * *" "python nightly.py"
stado schedule list
stado schedule pause <schedule-id>
stado schedule resume <schedule-id>
stado schedule run <schedule-id>
A schedule records who declared it, as a submission records its submitter:
USER, else LOGNAME, else the account this process runs as in the system's
user database. A variable that is not text is refused with USER cannot be read: <error>, and an account the database does not hold with neither USER nor LOGNAME is set and the user database holds no account for uid <uid>,
instead of storing a schedule created by nobody. Every other record that
names who acted (autonomy pause and resume, resources adopt,
release quarantine clear, a service ensure audit and a space reclaim
audit) reads the same account and refuses with the same reasons.
The default --overlap-policy skip does not fire while the prior instance is
still queued or running, and the coordinator's log says what that instance is
doing at every tick it skips: schedule <id>: skip fire: prior job <job> is still running, started <time> (<n> s ago), worker lease until <time>, on local@<host>, with put back after: worker lease expired (restarts <n>) when
a reaper requeued it. rm deletes a schedule without affecting jobs it
already submitted; run fires once immediately regardless of the next run
time; resume recomputes the next run from now.
Declarations held on this host
A host declares its schedules while the fleet store does not answer. When
schedule create meets a store that answers a server error, a request the
resolver holds unanswered, or a transport failure, it keeps the declaration
on this host at ~/.stado/schedules/held/<schedule-id>.json and exits 0 with
held schedule <id> on this host at <file>: the fleet store did not answer (<refusal>); the next schedule command that reaches it writes it there
(--json answers {held, refusal, schedule}). Any other refusal of the
store still ends the command. Every schedule command that reaches the store
writes the held declarations into it first, as schedule create would, and
says stado: held schedule <id> written into the fleet store; <file> removed on stderr (was already declared in the fleet store when the store
holds the same declaration under a retained id, brought the stored schedule to its declaration: changed <fields> when it was declared with converge);
one the store refuses (another schedule under its id) stays held with
stado: held schedule <id> stays at <file>: the fleet store refused it: <refusal>. schedule list shows held declarations beside the
store's, marked H with their file, time and refusal (--json adds a
held object to each); when the store does not answer it lists what is held
here and exits with the fleet store did not answer (<refusal>), so only the schedules held on this host are listed.
A held fleet declaration is not a host-local routine: it waits for the fleet store. Use the host store explicitly for work that must run without it.
Host-local routines
A host also keeps its own store, ~/.stado/host-store, with the same
schedule and job records as the fleet store, for routines that keep that one
host working. schedule create --host-store declares a routine there, never
in the fleet store, and needs no answer from the fleet store:
stado schedule create --host-store --cron "0 2 * * *" "your-maintenance-command"
stado schedule list
schedule list lists the host store's routines beside the fleet store's
schedules, marked R in the EN column (r when paused) and counted as
<n> routine(s) of this host's own store (R), fired and run by its stado serve --worker; --json marks each with host_store: true, and an overdue one is
judged against the host pass's last sweep. When the fleet store does not
answer, the host's routines are still listed. A host store that cannot be
opened (no Stado directory under HOME) is said after the listing and the
command exits with that refusal. schedule show, edit, pause, resume,
rm and run take a host routine's id too: they ask the host store first
and act on the routine there, so they work while the fleet store does not
answer, and an id the host store does not hold goes to the fleet store as
before.
The Stado CLI installation declares one such routine on every machine it
installs on: the submit queue's resident pass, "$STADO_BIN" change run
every ten minutes (who runs the queue).
Its worker (stado serve --worker) retains its normal fleet configuration.
It runs host routines itself on its poll, before and independently of the
fleet store it binds: each poll fires every due schedule held there (overlap
policy included) and records the sweep, records the end of every routine job
it started, moves to failed every running host job no child of it runs whose
lease has run out (the worker that ran it stopped before recording its end; its lease ran out at <time>), so a worker that stopped mid-job never leaves
an overlap-skip routine waiting forever, deletes the cleaned sentinel every
finished job leaves under queue/ and running/ once the job has settled in
a terminal prefix (said as retired N settled sentinel(s) under <prefix>/;
a failed sweep as retiring the settled sentinels under <prefix>/ failed),
so the host store does not keep two records per fired routine for good and
each pass's listings stay the size of the live work, renews the leases of the jobs it
runs, and starts every queued host job as a child shell running the job's
command, its output appended to ~/.stado/host-store/logs/<job-id>.log.
A host job runs as a fleet worker runs a job: in its own directory,
~/.stado/host-store/work/<job-id>, with WC_JOB_ID, WC_JOB_STARTED_AT,
STADO_BIN, and the firing schedule's STADO_SCHEDULE_ID,
STADO_SCHEDULE_CRON and STADO_SCHEDULE_NEXT_DUE_AT (its next firing, the
moment the run must end before) set, and whatever it leaves in that directory's output/ is kept
in the host store under status/<job-id>/output/ when it ends, where
stado artifacts <job-id> against the host store reads it; a routine that
hands back its receipt as an artifact works the same from either store. Once
the output is kept the work tree is removed, so the host store holds no tree
per fired routine; a job the pass fails because its worker stopped mid-run
has its output kept and its tree removed the same way. A child that exits
moves its job to completed, or to failed with the command ended with <status> in error; output that could not be kept fails the job with the command ended with <status>, but its output in <dir> was not kept (<error>); its work tree stays there for inspection, and a tree that could not be
removed after its output was kept is said in the log as <job-id>: its output is kept, but its work tree <dir> was not removed: <error>.
The schedule's immutable submission plan, replay lookup, claims and queue
writes all use that same host store. They do not reopen the configured fleet
backend while submitting a due routine. The library operation
JobStorage::submit_batch requires the request's bucket to equal the selected
store's bucket; otherwise it refuses with submission bucket <requested> does not match selected store bucket <selected> before writing a plan or
queue entry. Ordinary stado submit still selects the configured backend.
Diagnose an outage
Nothing in the host pass reads the fleet store, so a fleet store that does
not answer stops no host routine. Its log lines start [host-routines]; a
store that cannot be opened is said once as the host store cannot be opened, so no host routine runs: <error>, and every failed step names itself (firing due schedules failed, the sweep record was not written, renewing the lease of <job> failed, starting <job> failed, <job> ended but was not moved to <state>).
A routine whose next run passed and did not fire is named in the listing
itself: schedule list reads the coordinator's last recorded schedule sweep
once and, for each enabled schedule still waiting past its next run, adds the
reason schedule show <id> gives as overdue (--json) or as an
overdue: <reason> line under its row: its occurrence is reserved and not
yet enqueued, no sweep was recorded in the store, the last sweep failed with
its error, the coordinator has not swept since a time on a host, a sweep ran
after the due time and did not fire it, or the recorded sweep time is
unreadable. Each sweep records when its sweeper promises the next one
(next_by: its start plus two of its periods, the coordinator's interval or
the worker's poll; none for a one-off stado scheduler tick), and a schedule
that fell due after the last sweep is not overdue until that promise has
passed: the reason then names it (… which promised its next sweep by <time>: that coordinator has not ticked since …). A sweep recorded by an
older Stado promises nothing and is judged as before. A schedule that is
due, paused or held has none. Oko's oko routines list passes the same field through for its own routines, and Oko
Desktop's Automation screen shows it under the next scheduled time.
A fleet-store refusal alone does not prove the routine ran. Inspect the
routine's actual job state in ~/.stado/host-store/completed/ or failed/
and its log under logs/; match the job's schedule_id to the declaration.
A healthy worker process or a listed schedule is not execution evidence.
Real qualification
Run the existing isolated product journey from the Stado checkout:
kompilator test --manifest-path stado-rs/Cargo.toml --test scheduler -- \
host_routine::a_host_routine_fires_and_runs_while_the_fleet_store_does_not_answer \
--nocapture
It declares a genuine host-local schedule through the CLI, starts the actual
standalone worker against a loopback endpoint with no listener, observes the
real fleet connection refusal, and requires the schedule's completed job and
its command output in the host store. Each run retains command exits, source
revision, worker output and report.json under .build/z/<run>/. It does not
change the operator's configuration, schedules or service process.
Source: this website