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.
Submit
stado submit "python train.py --epochs 3"
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.
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 --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.
stado profiles # list available profiles
stado profiles <name> # show one profile's JSON
stado submit --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, 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.
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.
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>
The default --overlap-policy skip does not fire while the prior instance is
still queued or running. 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