Skip to content

Coming from Inngest

This page maps the words you know onto Hyperspool’s, shows an event-driven workflow 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 event 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.

Events start runs through handlers. A handler is a workflow registered for an event type, and each matching event starts one run of it. You submit the workflow file and Hyperspool runs it on an agent: a managed environment we run, or an agent you run yourself. The run keeps its own copy of the file.

An onboarding flow: welcome a new user, give them three days to create a project, then send tips or a nudge depending on whether they did.

onboarding.sh
email="$(python3 -c 'import json, os; print(json.load(open(os.environ["SPOOL_INPUT_FILE"]))["payload"]["email"])')"
spool step welcome -- echo "welcome mail to $email"
spool sleep 1d
if spool wait event project.created --timeout 3d > /dev/null; then
spool step tips -- echo "tips for new projects to $email"
else
spool step nudge -- echo "how to create a first project, to $email"
fi

Register it for user.signup and send events:

Terminal window
spool handler set onboarding ./onboarding.sh --on user.signup
spool event emit user.signup --key ada@example.com --payload '{"email":"ada@example.com"}'
spool event emit user.signup --key bob@example.com --payload '{"email":"bob@example.com"}'
# a day later, Ada creates a project
spool event emit project.created --key ada@example.com

The --key is the event’s subject. A run takes the subject of the event that started it, and its spool wait event then accepts only events with that subject (or with none), so Ada’s project.created releases Ada’s run and Bob’s run keeps waiting. With the durations shortened to seconds, the two runs log:

Terminal window
welcome mail to ada@example.com
tips for new projects to ada@example.com
Terminal window
welcome mail to bob@example.com
error: no "project.created" event arrived before the timeout
how to create a first project, to bob@example.com

While a run sleeps or waits, its process has exited and it holds no agent. Events and handlers covers handlers, filters with --when, and subjects.

Inngest Here Notes
function workflow a script, a pipeline or a graph
event trigger handler spool handler set <name> <file> --on <type>; one run per matching event
trigger expression (if) --when '<CEL>' filters before any run starts
event event a type, an optional subject (key) and a JSON payload
event data payload a script reads the event from $SPOOL_INPUT_FILE
inngest.send() spool event emit from a terminal, a script or a graph step
step.run() spool step step_fn in the Python SDK
step.sleep() spool sleep <duration> holds no agent
step.sleepUntil() spool sleep with a computed duration work out the number of seconds in a step, then sleep that long
step.waitForEvent() spool wait event <type> --timeout <d> exits 124 on timeout; matched by subject. A pipeline’s wait_for_event: takes a when: filter
step.invoke() spool call -w <name> runs a stored workflow as a child and prints its output
step.sendEvent() spool event emit inside a run recorded, so a replay doesn’t publish twice
parallel steps spool activity submit / activity wait or a graph’s fan_out; see fan out
fan-out to several functions several handlers on one type each starts its own run
concurrency concurrency:, spool lock <group> one run, or one locked section, at a time per group; the group can be computed per key
throttle throttle: limit, period, burst, and a key per tenant
rateLimit throttle: with policy: drop runs over the rate are refused
debounce a recipe debounce a burst of events
batchEvents a recipe the batch example
cancelOn cancel_on: on a pipeline or graph; matched by subject
cron trigger schedule spool schedule create --cron "0 0 9 * * MON": six fields, seconds first, --tz
idempotency spool submit --idempotency-key, spool state add see handle each event once
retries retry:, spool activity run --attempts see retry a flaky call
NonRetriableError an exit code you don’t retry exit_codes: on retry:, --retry-on-exit on an activity
onFailure an if: failure() job, or a run.failed handler
run run <org>:<project>:run-<random>

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.

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. Put the loop variable in --label for the run page. 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.

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.

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.

State reads aren’t recorded. spool state get reads the store on every pass and sees other runs’ writes. spool state add, incr and cas record their answers, which is what makes the debounce and claim recipes work. See durable state.

A run keeps the definition it started with. Changing the workflow file or a handler 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.

  • More than one running run per concurrency group. A group admits one run at a time.
  • Debounce and batching as settings. Both are recipes built on state cells.
  • Priority between queued runs. A concurrency queue is first in, first out.
  • A published TypeScript SDK. Scripts in JavaScript call the spool commands as subprocesses; see workflows in your language.

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