On this page
Jobs
You have a command to run on fleet compute: how does it get from your shell to a worker, what happens while it runs, and what do you do at each state? This page follows one job through its whole lifecycle. First submission is covered in quick-start; flag-by-flag detail lives in cli.
A detached agent session is one of these jobs. stado workload start jeden-session submits it for you, sized from the kind's declared
reservation and pinned to the host its placement chose, so everything on
this page — the claim, the heartbeats, the reaper, stado job watch,
stado results and stado cancel — is how you follow and stop it. List
the ones the fleet holds with stado workload sessions, and read
workloads for the placement itself.
Submit
stado submit --run-id train-topic "python train.py --epochs 3"
Every submission carries --run-id, the caller's own stable name for the run:
repeating the same run id and request answers the jobs already submitted
instead of queueing them twice, so a retried submission is safe. The job enters
queue/<id>.json in the configured canonical backend. Routing
and sizing are declared at submit time: an optional provider constraint
(--provider, with --pin-provider or the default --any-provider),
spot policy, a hard --max-cost-per-hour cap, priority within the FIFO
bucket, a hard RFC 3339 --deadline-at, and the resource envelope
(--gpu-type, --vram-gb, --machine-type, --exclusive). Inputs are
declared, not ambient: a git repo pinned to an exact commit (--repo
plus --repo-ref), pinned artifacts (--input-artifact), and scoped
workload secrets as --secret-env ENV_NAME=SKARBIEC_ITEM#FIELD.
Priority
--priority accepts the nonnegative range of the job model's signed integer,
not a limit chosen by the index's decimal formatting. Higher priorities are
claimed first, then older jobs. Reorder an existing queued job without
resubmitting it:
stado job set-priority <job-id> <priority> --json
A negative value is refused as job priority <value> must be nonnegative;
submission reports priority <value> must be nonnegative. A job no longer
queued is refused with its current state and is not resurrected. Desktop
Queue → Change a queued job's priority invokes the same native operation,
shows its mutation confirmation and retains the same receipt or refusal.
The priority index encodes the full integer in descending order. Existing queue documents remain authoritative: the normal backfill rewrites their keys and removes superseded entries before marking the new coverage complete. An index entry that disagrees with its job's current key cannot admit it. Deliver the new queue readers and writers together; an older writer does not understand the new index order.
The real CLI journey is node tests/queue/priority.mjs. It needs STADO_BIN,
STADO_TEST_SOURCE_REVISION, STADO_TEST_PRIORITY_NEGATIVE (a negative
integer), and the host's PATH. It creates an isolated local deployment under
.build/queue-priority, submits a job naming no priority and reads the default
stado submit gave it, submits jobs beyond the old decimal range, promotes
and demotes them, checks persisted order and negative-value refusals, then
cancels them and checks terminal-state refusal. It retains the source and
binary identities, command streams, statuses, the default priority and final
state in report.json.
Nothing about the machine is assumed. A job that may run on a rented machine
states its disk with --boot-disk-gb <GB>; a job that states none is never
the reason a machine is rented (the dispatcher logs REFUSING to rent agent machines … none of its queued job(s) states a boot disk) and waits for a
registered host. A rented machine gets the largest disk any job in its bucket
stated. --yieldable needs --max-yields <N>, how often the worker may yield
the job before it is protected; a job that states no budget is protected from
the start. stado schedule create --boot-disk-gb and the machine interface's
boot_disk_gb field state the same disk for scheduled and machine jobs. A
non-positive value is refused by name.
Nor is the run time. Placement prices a job, holds it to its deadline and
orders the local pack by how long it runs, and the only two sources of that are
the submitter's --runtime-seconds-estimate <SECONDS> (also on stado schedule create, and the machine interface's runtime_seconds_estimate) and
the median of completed runs of
the same model on the same hardware. A job with neither is refused a priced
placement (no run time for this job on <target>: no runtime_seconds_estimate was stated and no completed run was measured), left unassigned by the
makespan matcher, and packed locally only after every job whose run time is
known. A non-positive estimate is refused by name; a rerun keeps the
original's.
Priority changes ordering, not the runtime evidence: a high-priority job with
neither source also remains unassigned. Any previous derived assignment is
cleared; eligible workers may still claim it through the normal priority queue.
The makespan diagnostic reports every skipped model/task group with its count,
ordered by count. It does not hide groups outside a fixed display window.
The real regression is cargo test --manifest-path stado-rs/Cargo.toml --test scheduler -- sizing::runtime::, from the Stado checkout. Supply
STADO_SOURCE_REVISION, STADO_TEST_WORKER_POLL_SECONDS,
STADO_TEST_COORDINATOR_INTERVAL_SECONDS, STADO_TEST_POSITIVE_PRIORITY, and
STADO_TEST_RUNTIME_SECONDS. It starts an isolated worker, retains its actual
capacity publication, stops that worker before submitting jobs, then runs a
real coordinator. A declared-runtime job must be assigned to that published
consumer while both ordinary and priority jobs without runtime stay unassigned.
If admission fails or the measured publication expires before assignment, the
journey fails, not passes. Commands, process exits, logs and persisted before/
after state are retained in its .build/z/ report. No provider is simulated.
A batch is one submit over a file of commands — one command per line, with
blank lines and # comments skipped. With --batch the positional command is
ignored; every submitted job carries the same batch id, and stado status
filters by job id or batch id substring, so a batch is inspected as one unit.
stado submit --run-id nightly-batch --batch commands.txt ""
stado status <batch-id-substring>
Submit profiles
A profile is a named, reusable submit spec. --profile NAME applies one from
the bundled profiles directory (or $WC_PROFILES_DIR); explicit CLI flags
override profile fields. A WC_PROFILES_DIR that names no directory is
refused with WC_PROFILES_DIR names <path>, which is not a directory, and one
that is not text with WC_PROFILES_DIR cannot be read: <error>, rather than
applying the bundled profile of the same name.
stado profiles # list available profiles
stado profiles <name> # show one profile's JSON
stado submit --run-id <run id> --profile <name> "python train.py"
Queued → running: the claim
Job state is a set of storage prefixes, not a mutable status field. The
queued-to-running transition is a create-if-absent write of
running/<id>.json, so it has exactly one winner — two agents scanning the
same queue cannot both start your job. Every other prefix transition writes
the new record before deleting the old one; readers tolerate the retry window
and resolve terminal state first.
Who wins the claim is a scheduling decision. The scheduler orders the queue by
priority and creation time and admits only targets whose capabilities, policy,
deadline, resource envelope, and provider fence match. Every agent broadcasts
its live CPU, RAM, disk, running-job, and per-device accelerator capacity to
capacity/<consumer-id>.json; the cloud scheduler reads these to decide
whether to yield a queued job to a free local consumer
instead of dispatching a paid VM, marking jobs with the highest
$-saved-per-GB-of-local-VRAM for local pickup first. So a job you expected on
a cloud GPU may legitimately run on an idle local machine — see
providers for the provider model. --pinned-host opts out:
only the named consumer may claim the job.
Workers have no configured job-slot count. On each poll a worker subtracts the
resources used by running jobs and by the live reservations
placed workloads hold on it, then admits queued jobs until the live CPU, RAM,
VRAM, disk, or policy budget is exhausted. A job without explicit CPU or RAM
sizing reserves one CPU core and 1 GiB of RAM; --exclusive starts only when
the worker is idle and prevents further admission until that job finishes.
Running: heartbeats and proof of life
A running job's evidence lives under status/<id>/. The job's per-job
heartbeat at status/<job_id>/heartbeat is written by the running job itself,
independent of the agent's capacity-broadcast loop. That independence is the
point: a long training subprocess can starve the broadcast loop until it looks
stale, and the reaper's heartbeat guard uses the per-job heartbeat as the
second signal — if any job on a VM heartbeats fresh, the reap is deferred.
Even the heartbeat can starve: a multi-GB checkpoint upload saturates outbound network and delays the small heartbeat write while the job is demonstrably alive. The guard therefore also accepts a fresh checkpoint write as proof of life — the newest blob under the job's checkpoint prefix stays fresh throughout an upload. A genuinely dead job writes neither, and is requeued once both signals age out. A coordinator-side read or listing failure is never treated as proof of death; the guard fails safe and defers.
What this means for you: a busy job is protected without any action on your part, and a job whose worker really died is requeued automatically rather than stranded.
A maintenance job that ends by stopping the agent running it (upgrade the
agent, then restart it) is submitted with --terminates-agent. The agent
vanishing is then that job's success: the reapers record it completed
instead of requeuing it, which would run the stop again on the next agent
generation. Only the declaration counts; Stado does not read the command text,
so a command that stops the agent without the flag is requeued like any other
orphan, and one that merely mentions kill is not taken for a maintenance job.
stado job rerun carries the declaration to the new job.
Watch a job while it runs:
stado job watch <job-id> --follow
--follow polls the log until the job reaches a terminal prefix.
Terminal states and results
completed/ records success; failed/ records failure with bounded error
classification. If the job declared --verify, that command must exit 0
after success — a non-zero exit reverses COMPLETED to FAILED, catching
silent-success failure modes. Output always lands under canonical
status/<id>/output/; --output-uri adds a second stado:// destination.
stado results <job-id> ./out
The download includes the result manifest recording artifact size and SHA-256. A failed job remains inspectable and may still publish logs and partial artifacts.
A job runs in its own process group. When the agent first sees the job's
process exit, it ends whatever is still running in that group — a download or
a server a step started in the background — once, and logs <job>: ended the processes its job left running in process group <pgid> when there was
something to end. Nothing a job started outlives it. Processes that earlier
agents left behind are found with stado service list --unowned and ended with
stado service reap --host <host> --command <program> --apply, which never
signals a process a declared unit holds.
Cancel
stado cancel <job-id>
stado cancel <job-id> --terminate
Cancel works on a queued or running job and writes a durable record to
cancelled/. Without --terminate, a cancelled job's cloud VM keeps running
— and billing; --terminate also deletes the instance the job holds.
Rerun
stado job rerun <job-id>
Rerun resubmits a job's exact spec under a new job id, from any lifecycle prefix — the original record is untouched.
Lifecycle table
| State (prefix) | Who sets it | What an operator does |
|---|---|---|
queue/ |
stado submit (or the coordinator, for a due schedule) |
Wait, or check why it is not claimed: eligible worker running, queue not paused, capacity fits, deadline not expired. |
running/ |
The winning agent, via the create-if-absent claim | stado job watch <id> --follow; nothing else — heartbeats and checkpoint writes protect the execution. |
running/ (stale heartbeat and checkpoint) |
The reaper requeues to queue/ |
Nothing; the job restarts from its last checkpoint. |
completed/ |
The agent, after the job (and any --verify) succeeds |
stado results <id> <dir>. |
failed/ |
The agent, on job failure or a failed --verify |
Read the log and error classification; fix; stado job rerun <id>. |
cancelled/ |
The operator, via stado cancel |
Confirm --terminate was used if the job held a VM. |
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.
What it cost
Cost reporting is computed from observed wall-times, per job and per batch:
stado cost report # $ spent per target_kind and per model, from completed jobs
stado cost estimate commands.txt # project total $ for a batch file using observed per-job cost
stado cost also carries allocation, forecast, anomalies, and
savings; budgets and the wider cost model are covered in costs.
Source: this website