Script commands
What each spool command does when a script calls it from inside a
run. Polyglot scripts is the guide; this page is
the lookup. Flags and examples for every command are in the command
reference, and spool help job prints a summary
offline.
These commands talk to the agent running the script, over the socket in
$SPOOL_AGENT_SOCKET. They need no server address or token.
At a glance
Section titled “At a glance”Recorded commands store their result in the run the first time they complete; a replay returns that result without doing the work again. Live commands run again on every pass. Commands marked “waits” end the script process and free its agent until the wait is over; the script then runs again from the top.
| Command | Python SDK | What it does | Recorded | Waits |
|---|---|---|---|---|
spool step |
step, step_fn |
Runs a command once, records stdout and exit code | yes | |
spool sleep |
sleep |
Sleeps for a wall-clock duration | yes | yes |
spool wait signal |
wait_signal |
Waits for a named signal, prints its payload | yes | yes |
spool wait event |
wait_event |
Waits for a project bus event, prints the envelope | yes | yes |
spool lock |
lock |
Holds a project-wide lease | yes | while held elsewhere |
spool activity run |
activity |
Runs a command on any matching agent, prints its output | yes | yes |
spool activity submit |
activity_submit |
Queues a command on any matching agent | yes | |
spool activity wait |
activity_collect, activity_wait_all |
Collects submitted work | yes | yes |
spool spawn |
spawn |
Starts another run, prints its id | yes | |
spool call |
call_node, call_graph |
Starts another run, waits, prints its output | yes | yes |
spool event emit |
emit |
Publishes an event on the project bus | yes | |
spool state add, cas, incr |
state_add (no cas, incr) |
Claims, swaps or counts a state cell | yes | |
spool state get, set, delete, list, version |
state_get, state_set, state_delete, state_list |
Reads and writes a state cell | live | |
spool artifact put, get |
artifact_put, artifact_get |
Stores and restores files | yes | |
spool artifact list |
artifact_list |
Lists artifacts | live | |
spool output set |
output_set |
Stages this job’s output | live | |
spool annotate |
annotate |
Posts a markdown note to the run page | live | |
spool checkout |
checkout |
Clones the run’s repository at its commit | live |
When replay goes wrong lists which of the recorded commands count toward the order check a replay runs.
spool step
Section titled “spool step”spool step <name> [--label <text>] -- <cmd...>Runs <cmd> once, records its stdout and exit code, echoes the stdout
and exits with that code. A replay returns the recorded output without
running the command. A failed step is recorded too, so its replay
returns the failure.
- The name is a place in the script. A loop reaches one name many times
and each reach gets its own recorded result.
--labelis display text forspool run stepsand the run page; it never changes which result a call replays. - A step records at most 2 MiB of output; more fails the step. Record a path or an id and put large files in an artifact.
- Exit codes are the command’s own; 125, 126 or 127 when it couldn’t start, isn’t executable or doesn’t exist.
The body runs in a sealed environment: the agent’s working directory
(not the script’s current directory), PATH, the agent variables on
the allowlist and the run’s credentials. The
job’s env:, variables the script exported and CI_TRIGGER_* don’t
reach it, so pass what <cmd> needs as arguments.
A body can’t call spool itself (a nested call exits 2 with this runs only inside a job). The one SPOOL_* variable it gets is
$SPOOL_IDEMPOTENCY_KEY, a token for this step of this job of this
run, for an API that dedupes on one:
account=acct-42spool step charge -- sh -c ' curl -fsS https://api.example.com/charges \ -H "Idempotency-Key: $SPOOL_IDEMPOTENCY_KEY" \ -d "account=$1"' _ "$account"Keep the single quotes: the variable exists only inside the body, and
under set -u expanding it in the script’s own shell aborts the script.
A step that dies after acting but before its result is recorded runs
again with the same token. Two steps get two tokens, and a rerun gets
new ones.
spool sleep
Section titled “spool sleep”spool sleep <duration>Sleeps for 500ms, 30s, 5m, 2h, 7d and so on, holding no agent.
The deadline is fixed on the first pass, so an agent crash mid-sleep
resumes toward the original wake time.
spool wait signal
Section titled “spool wait signal”spool wait signal <name> [--timeout <duration>]Waits until a signal called name is sent to the run (spool run signal <run> <name>), then prints its payload as JSON.
- With
--timeout, a wait that gives up exits 124. Underbash -euput a bounded wait in anif. The outcome is fixed the first time it resolves, so a signal landing just after a recorded timeout doesn’t change a branch already taken. - A name is a queue. Each wait takes the next signal, signals sent while nothing is waiting are kept, and two jobs in one run waiting on one name take one signal each. A loop can wait on the same name every time round.
- The sender addresses the run and the name and can’t see where the script is. If a particular turn needs a particular answer, put the discriminator in the payload.
- Names starting with
event:are refused.
A signal has no approve/reject form. For a person answering a form, use
an approval job or step.
spool wait event
Section titled “spool wait event”spool wait event <type> [--timeout <duration>] [--accept-untrusted]Waits until an event of type lands on the project bus (from spool event emit, a handler, or a webhook) and prints the event envelope as
JSON. --timeout works as for wait signal.
- Every waiting job gets its own copy, so any number of jobs, runs and graph steps can wait for one type.
- A wait that starts after an event fired waits for the next one. Waiting on one type twice takes two successive events.
- A run started for a subject (a pull request, a customer) only takes events for that subject, or events emitted without one.
spool event emit <type>from your terminal unblocks a wait by hand.- An untrusted event, such as
a fork’s pull request, ends the wait only with
--accept-untrusted; the envelope then carries"untrusted": true.
spool lock
Section titled “spool lock”spool lock <group> [-- <cmd...>]Takes a durable lease on group, shared across the project. While
another run holds it, this one waits holding no agent. Leases are
granted in arrival order.
| Form | Holds the lease | Belongs to |
|---|---|---|
spool lock g -- <cmd> |
while <cmd> runs; <cmd> is recorded as a step |
the job |
spool lock g |
until the run ends | the run, so once per run |
The bracketed command runs as a step, so it can’t call spool. When the
critical section is itself spool work, such as a state read and write,
take the bare lease. A job’s
concurrency:
takes the same lease, so a script and a job naming one group contend on
one lock. A run waiting for a lease shows the awaiting_lease
substate.
spool activity run
Section titled “spool activity run”spool activity run <name> [flags] -- <cmd...>Runs a command as a separate unit of work that any agent matching the flags can claim, waits for it, and prints its stdout. It exits with the command’s code.
| Flag | Meaning |
|---|---|
--requires <cap> |
Capability the claiming agent must advertise (repeatable) |
--agent <id> |
Only this agent may claim it |
--attempts <n> |
Total attempts, counting the first |
--backoff <duration> |
Wait between attempts (needs --attempts) |
--retry-on-exit <code> |
Retry only on these exit codes (repeatable); default any non-zero |
The command runs on the claiming agent’s filesystem in its own working
directory, so use absolute paths or cd first. Capabilities beyond
shell, polyglot, connector and build come from agents you
run. Names are unique in the run.
spool activity submit and wait
Section titled “spool activity submit and wait”spool activity submit <name> [flags] -- <cmd...>spool activity wait <name>... | --allsubmit takes the flags of activity run and returns once the work is
queued. wait <name> waits for that activity and prints its output,
exiting with its code; with several names it waits in order and exits
with the first failure’s code. wait --all covers every activity this
run submitted, prints a table of names, statuses and exit codes, and
exits 1 if any failed.
Submit everything before the first wait, or the work runs one item at a
time. An activity nobody waits for is never read, so its failure goes
unnoticed and the script can end before it reports; a run that succeeds
with activities unreported gets a note on its page. Activities don’t
appear in the run’s event log: the run page lists them under
Activities, and spool run activities <run> [--failed] lists them
in a terminal.
spool spawn
Section titled “spool spawn”spool spawn <file|-> [--as pipeline|graph|script] [--input <json>] [--tag <t>]...spool spawn -w <name[@version]> [--input <json>]spool spawn --node <kind> --config <json>Starts another run and prints its id without waiting. A file whose
version: is hyperspool-graph/v1 runs as a graph, a .yaml or .yml
file as a pipeline, anything else as a script; --as overrides that.
- reads the source from stdin.
- The spawned run goes through admission like a submit: its own
concurrency:andthrottle:apply (run admission). When admission turns it away, the spawn fails withthe spawned pipeline was refused: <reason>. - It runs on its own: cancelling or finishing the run that spawned it doesn’t end it. Its tags record who spawned it.
- It inherits the parent’s untrusted mark, reserved tags passed with
--tagare dropped, and spawn chains are capped in depth. - A spawned script runs under
bash -euwhatever its extension, with noenv:and no credentials. Spawn a pipeline with ascript:job when the child needs either, or another interpreter.
spool spawn reads a local file, so the script needs a checkout of the
repo to spawn from it.
spool call
Section titled “spool call”spool call <file|-> [--as pipeline|graph|script] [--input <json>] [--timeout <duration>]spool call -w <name[@version]> [--input <json>]spool call --node <kind> --config <json>Starts another run, waits for it holding no agent, and prints its output. The file can be a graph document in JSON or YAML, a pipeline or a script, so a script can run work it decided at run time.
- The called run goes through the same admission as a spawn.
- It belongs to the caller: it’s listed with the calling run as its parent, and ends when the calling run ends.
- A child that fails makes
spool callexit 1. With--timeout, a wait that gives up exits 124 and the child keeps running until the caller ends.
spool event emit
Section titled “spool event emit”spool event emit <type> [--key <subject>] [--payload <json>]Publishes an event on the project bus and prints its id. Handlers
subscribed to the type start runs, and waits on the type wake. A replay
returns the same id and doesn’t publish again. --key sets the event’s
subject.
Refused types: push, manual, schedule.tick, spawn, the run.*,
spool.* and feedback.* namespaces, and types that belong to a signed
webhook in the project.
From a terminal, outside any run, spool event emit publishes every
time it runs.
spool state
Section titled “spool state”spool state get <key> [--required] | version <key> | set <key> <value>|- | add <key> [<value>] | incr <key> [<by>] [--op <name>] | delete <key> | listspool state cas <key> <value> --expect <version> [--op <name>]all take [--entity <name>]; get, version and list also take [--run <run>]Durable state cells are named values that outlive a
pass. No --entity means run scope, private to this run;
--entity <name> is the project’s cross-run scope.
| Subcommand | Recorded | Prints | Exit codes |
|---|---|---|---|
get |
live | the value; nothing when unset | 0, or 4 with --required on an unset cell |
version |
live | the version, for cas --expect; nothing when unset |
0 |
set |
live | nothing | 0 |
delete |
live | nothing | 0 |
list |
live | key<TAB>value<TAB>version lines in a pipe |
0 |
add |
yes, by cell | nothing | 0 you hold it, 3 someone else does |
incr |
yes, by --op |
the new value | 0 |
cas |
yes, by --op |
nothing | 0 swapped, 3 the version moved |
set takes JSON or a bare string, and - reads stdin. incr and cas
default --op to the key; a loop that swaps one cell several times
needs a distinct --op per attempt, and reusing one in a run is
refused. Durable state
has the patterns.
spool artifact
Section titled “spool artifact”spool artifact put <path> [--name <name>] [--cache <key>]spool artifact get <name> [<path>] [--cache <key>] [--fallback-prefix <prefix>]spool artifact list [--cache <key>]put stores a file or directory and prints its digest. Without
--cache the artifact belongs to this run, which is how one job hands a
build to the next; with --cache it goes in the project cache. get
exits 4 on a miss, so a cache lookup is plain shell, and
--fallback-prefix takes the newest key with that prefix on a miss.
put and get are recorded, so a later pass gets the same digest and
restores the same artifact, even on another machine where the file the
first pass stored isn’t there. spool run has no artifact store; use
spool dev to try them locally.
spool output set
Section titled “spool output set”spool output set <key>=<value>|<key>:=<json>... | --values <json>Stages values as this job’s output, committed if the script exits 0.
key=value is a string and key:=json any JSON value. A pipeline
publishes them with the job’s outputs:, and later jobs read
steps.<job>.output.<name>:
name: canaryjobs: canary: outputs: verdict: verdict score: score script: | score="$(spool step metrics -- echo 0.97)" spool output set verdict=ok score:="$score" report: depends_on: [canary] steps: - name: report run: echo "verdict=${{ steps.canary.output.verdict }} score=${{ steps.canary.output.score }}"On a graph, a later step reads nodes.<id>.output.<key>.
spool annotate
Section titled “spool annotate”Posts a markdown note to the run page. --name lets a later call
replace or --append to it. See annotations.
spool checkout
Section titled “spool checkout”Clones the run’s repository at the commit that started it, into the
current directory (which must be empty) or a directory you name.
--depth <n> makes it shallow. See CI from a repo.
The run’s event
Section titled “The run’s event”Every run carries the event that started it, as one JSON object in the
file named by $SPOOL_INPUT_FILE, never truncated. It has a type
(manual for a submit, schedule.tick, spawn with the parent run
id, or the bus type of an event a handler received) and fields for that
type. An event from a webhook carries its shaped fields (sha, ref,
repo) at the top level and the forge’s raw JSON under payload.body.
Scalar fields also appear as CI_TRIGGER_* variables.
The managed environments have no jq, so parse it with python3 or
node:
branch="$(python3 -c 'import json, osevent = json.load(open(os.environ["SPOOL_INPUT_FILE"]))print(event.get("ref", "").removeprefix("refs/heads/"))')"echo "branch: ${branch:-none}"Environment variables
Section titled “Environment variables”| Variable | What it is |
|---|---|
SPOOL_AGENT_SOCKET |
Socket the spool CLI talks to the agent over |
SPOOL_JOB_PROTOCOL |
Job protocol version the agent speaks |
SPOOL_AGENT_PID |
Agent process id |
SPOOL_AGENT_ID |
The agent running this pass (spool agent list); may differ per pass |
SPOOL_WORKFLOW_ID |
The run’s id |
SPOOL_ACTIVITY_ID |
This script’s activity id, one value for the whole job |
SPOOL_INPUT_FILE |
The run’s event, as JSON (above) |
SPOOL_IDEMPOTENCY_KEY |
Inside a spool step body only (above) |
CI_TRIGGER_* |
One per scalar field of the event: EVENT (the type), EVENT_ID, SHA, SHORT_SHA, REF, REPO, SENDER, FORGE, SCHEDULE, PARENT, as present |
PATH, PYTHONPATH, NODE_PATH |
From the agent, so spool and vendored SDKs resolve |
A script also gets its job’s env: and credentials. The SPOOL_*
variables are applied last, so a workflow’s env: can’t shadow them.
Nothing else from the agent’s environment passes through except a fixed
list: USER, LOGNAME, SHELL, TERM, LANG, LANGUAGE, LC_ALL,
LC_CTYPE, TZ, TMPDIR, SSL_CERT_FILE, SSL_CERT_DIR,
NIX_SSL_CERT_FILE. On an agent you run, SPOOL_ENV_PASSTHROUGH
forwards more, and HOME is an empty directory of the job’s own unless
the agent says otherwise (the job
environment).
Interpreters
Section titled “Interpreters”spool submit and spool run pick the interpreter by extension:
| Extension | Interpreter |
|---|---|
.py |
python3 |
.ts |
bun run |
.js, .mjs |
node |
.go |
go run |
| anything else | bash -eu |
On spool submit, --interpreter overrides it (space-split, so
--interpreter "bun run" works); spool run has no override. A graph
code step sets interpreter in its config. A pipeline script: job
always runs bash.
The interpreter has to exist where the script runs. Managed
environments lists what
Hyperspool’s environments carry; an agent you run uses whatever is on
its host’s PATH; spool run and spool dev use your machine and run
the file in place from the directory you submit it in.
Exit codes
Section titled “Exit codes”The exit code table applies inside a job too. The ones a script branches on most:
| Code | From |
|---|---|
| 3 | state add or state cas lost |
| 4 | artifact get miss, state get --required of an unset cell |
| 124 | a wait or call with --timeout gave up |
| the command’s own | step, lock -- <cmd>, activity run, activity wait <name> |