Skip to content

Tour: the durable shapes

Thirty minutes with a local instance, one shape at a time: durable steps, timers, human gates, signals, events, and child workflows - plus the part that makes them worth having, which is killing the engine mid-run and watching nothing break.

You need the cryo binary (curl -fsSL https://cryosleep.io/install.sh | sh) and nothing else. Everything below runs locally.

Terminal window
cryo dev

One process: server, an embedded agent, and the web UI at http://localhost:18753. Leave it running in this terminal; the rest of the tour happens in a second one. (cryo run <file> is the even shorter path - a one-shot instance for a single run - but the tour wants one instance across several runs.)

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
cryo submit steps.yaml --follow

While transform’s five seconds tick, kill cryo dev with Ctrl-C, 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 durable boundary, so its steps re-run together. That’s deliberate honesty at this point in the tour: per-step checkpoints that survive a restart are the polyglot primitive (cryo 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 look at the runs page: during the two minutes, the run is parked. No agent is held, no process is sleeping on your behalf - the run is rows in a database until the timer fires. Change 2m to 30d and nothing about the mechanics changes; restart cryo dev during the wait and the wake-up still happens on time.

A run parked on a timer: the header says sleeping and names when it resumes, and it holds no agent while it waits.
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 parks at review - same mechanics as the timer, so it can sit for a week without cost. Open the run in the web UI and the approval form is inline; submit it and ship templates against what the human chose.

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

Terminal window
cryo approve <run-id>

The gate name is optional because the run is parked on exactly one thing. cryo status <run-id> shows what that is - an awaiting_signal substate named after the approval job.

cryo follow <run-id> gets you the same form without leaving the terminal you’re watching the run in: select the parked wait, press a.

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

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

Signals target one run by id. Events are the broadcast version - any run (or cryo 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
cryo emit deploy.approved
# or end the run early instead:
cryo 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.

When a run is started for something - a push to a repo, a specific PR - it carries that as its subject, and these reactions correlate to it: a preview started for acme/web’s PR cancels on that PR’s pr.closed, not another’s. The unkeyed cryo emit pr.closed above is a broadcast, so it still ends any run watching for one - handy from a terminal.

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="$(cryo step baseline -- date +%s)"
cryo sleep 30s
after="$(cryo 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
cryo submit canary.sh --follow

Kill cryo dev during the cryo sleep. On restart the script re-runs from the top and baseline returns its recorded timestamp rather than calling date again, so the subtraction still measures the gap the canary actually soaked for - not the gap since the restart. Steps capture values; everything outside them is recomputed on every pass.

That re-run-from-the-top model has rules to understand before you write something real - read the authoring model, and see workflows in your language for the same thing in Python, TypeScript, Go, and Rust.

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 UI - while its structured output flows back into the parent’s DAG:

canary-gate.yaml
jobs:
canary:
outputs:
verdict: verdict
workflow:
type: cryosleep/script/v1
script: |
cryo step probe -- echo "canary healthy"
cryo sleep 10s
cryo 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 calls cryo spawn to start any workflow as a child run. See composition for where each is the right tool.

  • Concepts - the vocabulary, in nine terms.
  • 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 substrate.
  • Deploying agents - put runners where your workloads are.