Skip to content

A tour of Hyperspool

Thirty minutes with a local instance, one shape at a time: durable steps, timers, human gates, signals, events, and child workflows. Along the way you kill the engine mid-run and watch the runs carry on.

You need the spool binary (curl -fsSL https://hyperspool.com/install.sh | sh) and nothing else. Everything below runs locally.

Terminal window
spool dev

One process: server, an embedded agent, and the console at http://localhost:18753. Leave it running in this terminal; the rest of the tour happens in a second one. If the CLI points at another server, local and the server shows how to switch.

Save and submit this:

steps.yaml
jobs:
work:
steps:
- name: fetch
run: echo "fetched 1200 records"
- name: transform
run: |
echo "transforming..."
sleep 5
echo "done"
Terminal window
spool submit steps.yaml --follow

While transform’s five seconds tick, stop spool dev (Ctrl-C, or kill -9 for a real crash), then start it again. The server resumes the in-flight run (resumed 1 in-flight run(s) in the startup log) and the work job runs again from its first step: a shell job is one checkpoint, so its steps run again together. Per-step checkpoints that survive a restart come from spool step, and section 5 shows the same kill leaving completed steps untouched.

soak.yaml
jobs:
deploy:
steps:
- name: ship
run: echo "deployed v2"
soak:
depends_on: [deploy]
wait:
duration: 2m
verify:
depends_on: [soak]
steps:
- name: check
run: echo "metrics clean, keeping v2"

Submit it and open the run in the console: for the two minutes, the run is asleep. No agent is held and no process waits on your behalf; the run is rows in a database until the timer fires. Change 2m to 30d and the mechanics stay the same; restart spool dev during the wait and the wake-up still happens on time.

A run asleep on a timer: the headline says when it wakes and that it holds no agent while it sleeps, and the journal shows the deploy done and the soak waiting.
gate.yaml
jobs:
build:
steps:
- name: build
run: echo "built v2"
review:
depends_on: [build]
outputs:
env: env
approval:
prompt: "Ship v2 where?"
fields:
- { name: env, type: select, options: [staging, production] }
ship:
depends_on: [review]
steps:
- name: ship
run: echo "shipping to ${{ steps.review.output.env }}"

The run waits for approval at review. Like the timer, it can wait a week without holding an agent. Open the run in the console and the approval form is inline; submit it and ship templates against what the person chose. Reject instead and the run is cancelled, with the reject and its reason recorded on the approval.

From a terminal it’s the same form, asked one field at a time:

Terminal window
spool approve <run>

The job name is optional because the run is waiting for only one thing. spool run get <run> shows what that is, waiting for approval `review`, and spool run get <run> -o json has it as an awaiting_signal substate named after the approval job.

spool run follow <run> gets you the same form without leaving the terminal you’re watching the run in: select the waiting step, press a.

A wait: {signal: ...} job waits until something delivers a named signal:

Terminal window
spool run signal <run> go --payload '{"reason":"maintenance window open"}'

Signals target one run by id. Events are the broadcast version - any run (or spool event emit from your terminal) can publish on the project bus, and runs subscribe by type:

preview.yaml
cancel_on:
events: [pr.closed]
jobs:
deploy:
steps:
- name: up
run: echo "preview environment up"
watch:
depends_on: [deploy]
wait_for_event:
event: deploy.approved
promote:
depends_on: [watch]
steps:
- name: promote
run: echo "promoted"

Submit it, then from your terminal:

Terminal window
spool event emit deploy.approved
# or end the run early instead:
spool event emit pr.closed

wait_for_event resumes the run when the event lands; cancel_on tears the whole run down. Both are registered at submit, so an event arriving before the waiting job starts still counts.

A run started for something, such as a pull request, only reacts to that thing’s events; subjects explains how.

Everything above was YAML. The same primitives are available inside a normal program, where checkpoints can be as fine as you like:

canary.sh
before="$(spool step baseline -- date +%s)"
spool sleep 30s
after="$(spool step recheck -- date +%s)"
echo "healthy at $before, still healthy $(( after - before ))s later"

(In a real canary those are curl -fsS calls against your service returning a metric; a non-zero exit fails the step, and the run.)

Terminal window
spool submit canary.sh --follow

Kill spool dev during the spool sleep. On restart the script runs from the top again and baseline returns its recorded timestamp, so the subtraction measures how long the canary soaked. That’s replay at work.

The re-run-from-the-top model has rules. Read the authoring model before you write something real, and see workflows in your language for the same thing with the Python SDK.

A pipeline can embed a polyglot child as a job, and the child gets its own run, its own checkpoint timeline, and its own page in the console, while its structured output flows back into the parent’s DAG:

canary-gate.yaml
jobs:
canary:
outputs:
verdict: verdict
workflow:
type: hyperspool/script/v1
script: |
spool step probe -- echo "canary healthy"
spool sleep 10s
spool output set verdict=ok
ship:
depends_on: [canary]
steps:
- name: ship
run: echo "canary said ${{ steps.canary.output.verdict }} - shipping"

The other direction works too: a script starts another run with spool spawn, or with spool call to wait for it. See composition for where each is the right tool.

  • Concepts - the vocabulary.
  • YAML reference - every field of every job kind, plus concurrency: and cancel_on:.
  • CI from a repo - webhook in, statuses back: the CI layer on the same engine.
  • Agents - where steps run on hyperspool.dev, and running your own agent.