# Workflows

URL: https://docs.domainruntime.dev/docs/data/workflows
Status: Guide
Reviewed: 2026-09-22



<div className="docs-note">
  Workflow actions run durably in production releases today (Vercel Workflow, started and followed through the runtime). In 

  **development**

   environments they currently run once, in memory, without durability: test restarts and 

  `sleep`

   against a production release.
</div>

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.

```ts title="src/domain.ts"
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,
    }),
  });
```

```ts title="src/workflows/fulfil-order.ts"
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 [#starting-and-following-a-run]

A workflow action answers as soon as the run is **admitted**, not when it finishes:

```bash
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:

```ts
db.useQuery({ $executions: { $: { where: { id: executionId } } } });
```

A typed browser helper that returns a run you can await is **planned**.

## Steps [#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.
* `runtime` is 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.
* `sleep` waits without holding a process, for seconds or for days.

## Identity and releases [#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.
