Skip to content

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.

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 sign-up that sends a verification link and waits up to a day for the user to click it:

signup.sh
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"
fi

Submit 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.

Terminal window
spool submit signup.sh --input '{"email":"ada@example.com"}' \
--idempotency-key signup-ada@example.com
# dev:default:run-ff311c997ccf63ed

The 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:

Terminal window
spool run signal dev:default:run-ff311c997ccf63ed verified
Terminal window
curl -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.

Terminal window
account created for ada@example.com
verification link sent to ada@example.com for run dev:default:run-ff311c997ccf63ed
activated ada@example.com

State that belongs to a key, changed by one run at a time per key:

add-to-cart.sh
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 carts

Three 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:

Terminal window
spool state get cart:ada --entity carts
# tent,stove,lamp

A 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.

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:

Terminal window
spool spawn - --as script <<'CHILD'
spool sleep 3d
spool step remind -- echo "reminder sent"
CHILD

The parent moves on at once; the child holds no agent while it sleeps.

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.

  • 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, incr or cas.
  • A published TypeScript, Go or Rust SDK; see workflows in your language.

Durable patterns has more recipes: deadlines, approvals with escalation, debouncing, polling.