Coming from Restate
This page maps the words you know onto Hyperspool’s, shows a workflow and a keyed object written here, and lists the behaviour you need to know before porting one.
The model
Section titled “The model”A workflow is a program the engine can run more than once: a bash
script calling spool commands, a Python script using the SDK, a YAML
pipeline or a graph. Each durable call (spool step, spool sleep,
spool wait signal and the rest) is a checkpoint, recorded the first
time it completes. When a run resumes, the script runs again from the
top; calls with a checkpoint return their recorded result at once, and
execution carries on from the first call without one.
A run starts when you submit a file or a stored workflow, when a handler matches an event, or on a schedule. Hyperspool runs it on an agent: a managed environment we run, or an agent you run yourself. While a run sleeps or waits, its process has exited and it holds no agent. The run keeps its own copy of the file.
A workflow with an outside callback
Section titled “A workflow with an outside callback”A sign-up that sends a verification link and waits up to a day for the user to click it:
email="$(python3 -c 'import json, os; print(json.load(open(os.environ["SPOOL_INPUT_FILE"]))["email"])')"
spool step create-account -- echo "account created for $email"spool step send-link -- sh -c 'echo "verification link sent to $1 for run $2"' _ "$email" "$SPOOL_WORKFLOW_ID"
if spool wait signal verified --timeout 1d > /dev/null; then spool step activate -- echo "activated $email"else spool step remove -- echo "removed unverified $email"fiSubmit it once per user. The idempotency key makes the email address the workflow’s identity: a second submit with the same key answers with the first run’s id and starts nothing, during the run or after it ends.
spool submit signup.sh --input '{"email":"ada@example.com"}' \ --idempotency-key signup-ada@example.com# dev:default:run-ff311c997ccf63edThe link carries the run id. When the user clicks it, your web app
sends the signal, from a terminal or over HTTP with a token from
spool token create:
spool run signal dev:default:run-ff311c997ccf63ed verifiedcurl -X POST -H "Authorization: Bearer $TOKEN" \ -H 'content-type: application/json' -d '{"clicked_at":"2026-11-01T21:40:00Z"}' \ "https://api.hyperspool.dev/api/v1/orgs/$ORG/projects/$PROJECT/runs/$RUN/signals/verified"The request body becomes the signal’s payload, which spool wait signal
prints. A signal sent before the run reaches its wait is kept until the
run asks for it.
account created for ada@example.comverification link sent to ada@example.com for run dev:default:run-ff311c997ccf63edactivated ada@example.comState per key
Section titled “State per key”State that belongs to a key, changed by one run at a time per key:
user="$(python3 -c 'import json, os; print(json.load(open(os.environ["SPOOL_INPUT_FILE"]))["user"])')"item="$(python3 -c 'import json, os; print(json.load(open(os.environ["SPOOL_INPUT_FILE"]))["item"])')"
# One run at a time per user, in arrival order, until this run ends.spool lock "cart-$user"cart="$(spool state get "cart:$user" --entity carts)"spool state set "cart:$user" "${cart:+$cart,}$item" --entity cartsThree submits at once for the same user leave the cart at
tent,stove,lamp, in submit order. Runs for other users don’t wait.
A project member can read the cell from a terminal, with no run and no
agent involved:
spool state get cart:ada --entity carts# tent,stove,lampA cell set with --entity belongs to the project and outlives every
run. Only runs write cells; to tell a running workflow something, send
it a signal. See durable state.
Vocabulary
Section titled “Vocabulary”| Restate | Here | Notes |
|---|---|---|
| service | a workflow you call | spool call -w <name> --input <json> from a run waits for its output; spool submit -w <name> starts it from outside |
| handler | a stored workflow, or a handler on an event type | an event handler starts one run per matching event; see events and handlers |
| virtual object | entity-scoped state plus a lock per key | spool state … --entity <name>, spool lock "<key>" |
| exclusive handler | a run holding spool lock <key> |
one at a time per key, in arrival order; a waiting run holds no agent |
| shared handler | reading a cell from outside | spool state get <key> --entity <name>, no run needed; read-only |
| workflow | a run | |
| workflow ID | spool submit --idempotency-key <id> |
repeat submits answer with the same run |
| invocation | run | <org>:<project>:run-<random> |
| journal | run history | the checkpoints a run recorded |
ctx.run |
spool step |
step_fn in the Python SDK |
ctx.sleep, durable timer |
spool sleep <duration> |
holds no agent |
| awakeable | a signal | hand out the run id and the signal name; spool run signal or POST …/runs/{id}/signals/{name} resolves it |
| durable promise | a signal | spool wait signal <name>; a signal sent early is kept |
| timeout on an awakeable or promise | --timeout on the wait |
exits 124 when it gives up |
ctx.get / ctx.set |
spool state get / set |
run scope by default, project scope with --entity |
| request-response call | spool call |
-w <name> for a stored workflow, --node <kind> for one node, or a file |
| one-way call | spool spawn |
starts a separate run and prints its id |
| delayed call | spool spawn of a script that sleeps first |
see below |
| idempotency key | spool submit --idempotency-key |
|
ctx.rand, ctx.date |
a spool step |
read the value inside a step and use what it recorded |
| terminal error | an exit code you don’t retry | exit_codes: on retry:, --retry-on-exit on an activity |
| retry policy | retry:, spool activity run --attempts |
see retry a flaky call |
| cancellation | spool run cancel, cancel_on: |
cancel_on: cancels a pipeline or graph run on a matching event |
| saga | a script that records undo steps | see the saga recipe |
A delayed call is a child run that sleeps before doing its work:
spool spawn - --as script <<'CHILD'spool sleep 3dspool step remind -- echo "reminder sent"CHILDThe parent moves on at once; the child holds no agent while it sleeps.
What behaves differently
Section titled “What behaves differently”A step is a command. spool step <name> -- <cmd> records the command’s
stdout and exit code. In Python, step_fn records a function’s return
value, which has to round-trip through JSON.
State reads and writes aren’t recorded. spool state get and set run
on every pass of the script, and get reads the store, so it sees other
runs’ writes. That is why the cart example holds a lock for its read and
write and keeps both in one pass. spool state add, incr and cas
record their answers, so a replay sees the same answer. See state and
replay.
The sequence of durable calls has to be the same on every pass. Code between them runs again each time, so read the time, random numbers and API responses inside a step and branch on what it recorded. A replay that reaches a different durable call than the first pass recorded at that position fails the run with a determinism error. When replay goes wrong shows how to trace one.
Step names repeat. A name is a place in the script, so a loop reaches
one name many times and each pass of the loop gets its own checkpoint.
Three things stay unique per run, because something refers back to
them: an activity submit name, a bare spool lock <group>, and a
state cas / state incr --op name.
A failed step stays failed. Its failure is recorded like a success, and
a replay returns it. Nothing retries a single spool step for you: retry
inside the command, run the call as an activity with --attempts, or
put retry: on the job, which runs the whole job again with its
recorded steps cleared.
A run keeps the definition it started with. Changing the workflow file leaves runs already in flight on the old code; new runs pick up the change. Commands the script calls on the agent are not pinned.
What we don’t have yet
Section titled “What we don’t have yet”- Request-response handlers on a running workflow. A run can’t answer a question it didn’t prepare for. It can publish values in state cells for outside readers, and it can take input through signals.
- A delay on
spool spawn. Put the sleep in the child. - Recorded state reads. Hold a lock, or use
add,incrorcas. - A published TypeScript, Go or Rust SDK; see workflows in your language.
Durable patterns has more recipes: deadlines, approvals with escalation, debouncing, polling.