Overview
TanStack Workflow is a durable execution engine for TypeScript. Workflows are async functions that pause, persist, and resume across process restarts.
The goal is durable workflows without making a workflow platform the center of your application. Your workflow code stays in your app, your durability boundary stays in a store you choose, and the same workflow model can run across Cloudflare, Railway, Netlify, Node, AWS, Vercel, or your own infrastructure.
Why TanStack Workflow#
Most workflow systems solve durability by asking you to adopt their control plane, hosted runtime, or state-machine service. TanStack Workflow is the headless option: a TypeScript engine, explicit durable primitives, a storage contract, and adapters for the environments you already deploy to.
Use TanStack Workflow when you want:
- long-running app logic without long-running processes
- durable steps, sleeps, signals, and approvals in plain TypeScript
- persistence you own or configure
- workflow code that can move between deployment providers
- TanStack-style primitives instead of a separate workflow platform
Mental model#
- A workflow is a closure — async (ctx) => .... Plain JS control flow.
- Every durable call goes through ctx.* and writes to an append-only event log.
- State is derived — reconstructed by replaying the log + re-running the handler. Never persisted directly.
- Pause = handler throws an internal sentinel. Resume = run the handler again; replay short-circuits past completed work.
Three things go in / two things come out#
Input ──┐ ┌── Output (handler's return value)
│ │
▼ │
createWorkflow({...}) │
⇒ handler(ctx) ─────────────┘
│
├─ writes ──▶ Event log (durability + UI transport)
│
└─ reads ◀── RunState (status, version, pause info)The event log is the source of truth. The browser subscribes to the same log via the runStore.
Authoring rules#
- Side effects go inside ctx.step(id, fn). Bare fetch() / db.x() outside a step is a determinism violation.
- Use ctx.now() / ctx.uuid() — not Date.now() / crypto.randomUUID().
- Step IDs must be unique per call site. Loops use interpolation: ctx.step(\charge-${i}`, fn)`.
- Helpers take ctx: WorkflowCtx<TExt> and call primitives through it. No ambient state.
What persists vs what doesn't#
| In the log (durable) | Emit-only (observability) |
|---|---|
| STEP_FINISHED / STEP_FAILED | RUN_STARTED |
| SIGNAL_AWAITED / SIGNAL_RESOLVED | STEP_STARTED |
| APPROVAL_REQUESTED / APPROVAL_RESOLVED | STATE_DELTA |
| NOW_RECORDED / UUID_RECORDED | CUSTOM (ctx.emit) |
| RUN_FINISHED / RUN_ERRORED |
Replay reads the durable events. Live subscribers see both.
Where it sits#
- Below: any HTTP server (TanStack Start, Hono, Express), any persistence (in-memory, Postgres, Durable Objects).
- Above: agent frameworks (@tanstack/ai-orchestration), domain workflows in app code.
- Beside: TanStack DB (reactive state from the log), TanStack Query (client cache).
Runtime shape#
The production path has three layers:
- @tanstack/workflow-core: the replay engine and workflow authoring API.
- @tanstack/workflow-runtime: registered workflows, execution store contract, schedules, timers, signals, approvals, leases, and bounded sweeps.
- Store and host adapters: Postgres, Cloudflare D1, Cloudflare Workers, Railway, Netlify, Node, Vercel, queues, and other environment-specific capabilities.
This keeps the engine headless without leaving deployment mechanics as an exercise for every user.
Status#
@tanstack/workflow-core ships the engine and the in-memory RunStore. @tanstack/workflow-runtime is the experimental durable runtime layer. @tanstack/workflow-store-drizzle-postgres, @tanstack/workflow-store-cloudflare-d1, @tanstack/workflow-vercel, and @tanstack/workflow-netlify are experimental capability adapters.
Start with the Guide for the full production model.