Durable workflows
Multi-step functions whose steps run exactly once and survive crashes, deploys, retries and waits of hours or days.
A workflow is an ordinary function that names its steps through context.step. Each finished step is saved with its result, so when something fails — a card network times out, a deploy restarts the server — the run picks up at the step that failed instead of starting over. Steps can also sleep (for minutes or days, at no cost) and wait for a signal such as a human approval or a webhook.
export async function handler(order, { step }) {
const charge = await step.run('charge', () => chargeCard(order)) // runs once; result saved
await step.run('receipt', () => sendReceipt(order.email, charge.id))
await step.sleep('cool-off', '1h') // seconds, '15m', '2h', '3d' or a Date
const ok = await step.waitForSignal('approval', { timeout: '2d' }) // null after the timeout
if (!ok?.approved) return { shipped: false }
await step.run('ship', () => createShipment(order, charge.id))
return { shipped: true }
}Start, signal and follow a run
sov functions workflows start checkout -i '{"order_id": 7, "email": "ada@example.com"}' --key order-7
sov functions workflows list --status waiting
sov functions workflows get <run-id> # status, output and every finished step
sov functions workflows signal <run-id> approval -d '{"approved": true}'
sov functions workflows retry <run-id> --latest # after fixing a failed step
sov functions workflows cancel <run-id>Starting twice with the same key returns the first run, so a client can retry the start safely. A signal sent before the run waits for it is kept until it does. Runs belong to the project and environment you start them in. Starting and signalling need the Call functions permission; cancel and retry need Deploy functions.
Rules for workflow code
- Step names are unique within a run (
charge,receipt-1,ship); a loop can build names likeemail-${i}. - The function is called once per step, and finished steps return their saved results, so code between steps must take the same path every time. Put anything random, time-based or external (
Date.now(), ids, API calls) insidestep.run. - A step's result is JSON of at most 256 KB, and a run's saved results at most 4 MB. Keep large data in a document or a file and return its id.
- A step that throws is retried with backoff (2 s doubling, at most 10 minutes apart) up to the plan's tries; then the run is FAILED. Fix the code and retry it: only the failed step runs again.
- A run stays on the function version it started with, so a deploy never changes the code under a run halfway.
retry --latestmoves a failed run to the live version. - Each call is one invocation (
context.idempotency_keyis unique per call). Like a trigger, it runs with the function's own identity (context.caller.role == "workflow"), never the starter's session. - Deleting the function cancels its unfinished runs.
Run a workflow locally
sov functions serve gives functions a context.step that runs every step inline in one call: sleeps are skipped and waitForSignal returns null, or the data you send in the X-Sovaria-Signals header.
curl -X POST http://127.0.0.1:8787/functions/v1/checkout \
-H 'X-Sovaria-Signals: {"approval": {"approved": true}}' -d '{"order_id": 7}'Plans
| Free | Pro | Ultra | Team | Enterprise | |
|---|---|---|---|---|---|
| Unfinished runs per project | 10 | 1,000 | 5,000 | 20,000 | 100,000 |
| Steps per run | 25 | 200 | 500 | 1,000 | 2,000 |
| Longest sleep or signal wait | 1 day | 7 days | 30 days | 90 days | 365 days |
| Finished runs kept | 1 day | 7 days | 14 days | 30 days | 30 days |
| Tries per step | 3 | 8 | 10 | 12 | 15 |
Each step counts as one invocation and its compute; sleeping and waiting cost nothing. See Plan limits.