Workflows
Actions that run for minutes or days: steps and waits that survive restarts.
sleep against a production release.A workflow is an action whose work continues after it is called. You write it as an ordinary async function marked "use workflow", split into steps marked "use step". Each step's result is saved; if the process restarts, the workflow resumes after the last finished step instead of starting over.
import { fulfilOrder } from "./workflows/fulfil-order";
export default domain("orders")
.withSchema({ /* … */ })
.withActions({
fulfil: defineAction({
input: z.object({ orderId: z.string().uuid() }),
output: z.object({ shipmentId: z.string() }),
execute: fulfilOrder,
}),
});import { sleep } from "workflow";
export async function fulfilOrder({ input, runtime }) {
"use workflow";
const order = await loadOrder(runtime, input.orderId);
const label = await createLabel(order.id);
await sleep("2h");
await markShipped(runtime, order.id, label.trackingNumber);
return { shipmentId: label.shipmentId };
}
async function loadOrder(runtime, orderId: string) {
"use step";
const db = await runtime.db();
const { orders_order } = await db.query({ orders_order: { $: { where: { id: orderId } } } });
if (!orders_order[0]) throw new Error("order_not_found");
return orders_order[0];
}
async function createLabel(orderId: string) {
"use step";
return shippingProvider.createLabel({ reference: orderId }); // your carrier's SDK
}
async function markShipped(runtime, orderId: string, trackingNumber: string) {
"use step";
const db = await runtime.db();
await db.transact(db.tx.orders_order[orderId].update({ status: "shipped", trackingNumber }));
}Callers see the action's input and output, not how the work is done.
Starting and following a run
A workflow action answers as soon as the run is admitted, not when it finishes:
curl -X POST "$ENV/actions/orders.fulfil/invoke" \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"requestId": "7c1e…", "input": {"orderId": "…"}}'
# 202 { "kind": "workflow", "runId": "…", "statusUrl": "…/executions/<id>", … }
curl "$ENV/executions/<id>/result" # waits up to 110 s, then returns { "shipmentId": "…" }Every run is an execution you can query live; each user sees the runs they started:
db.useQuery({ $executions: { $: { where: { id: executionId } } } });A typed browser helper that returns a run you can await is planned.
Steps
- A step runs at least once. Keep its effect idempotent, or look up its result before acting again.
- A step's return value must be JSON-serializable; it is what gets saved.
- Pass ids to steps, not large objects: every argument and result is stored.
runtimeis passed between steps, so it is saved with the run and restored on resume. Keep only data in it — ids and configuration — never open connections or secrets.- Code outside steps runs again on every resume. Keep it to control flow.
sleepwaits without holding a process, for seconds or for days.
Identity and releases
A run keeps the identity of whoever started it. It does not store their token, so a run that takes three days is not cut short when the token expires. Revoking a user blocks their new calls; runs already admitted continue as that user.
A run finishes on the release it started on. Publishing new code affects new runs only.