Summary
Add an optional, first-class machine-workflow layer for TanStack Workflow, initially centered on XState. The goal is to support workflows as data structures without replacing the existing async/await authoring model.
The durable runtime primitives stay the same: run state, events/signals, timers, leases, durable steps/checkpoints, persistence, retries, and observability. The machine layer gives users a graph-shaped workflow representation that can be annotated, analyzed, visualized, diffed, simulated, generated, and used to drive UI.
Proposed package shape:
import { defineXStateWorkflow } from '@tanstack/workflow-xstate'
export const orderWorkflow = defineXStateWorkflow({
id: 'order',
machine: orderMachine,
actors: {
chargeCard: async ({ input }) => {
// executed durably via Workflow step/checkpoint primitives
},
sendReceipt: async ({ input }) => {},
},
})
Motivation
Async/await workflows are excellent for human-authored, procedural workflows:
- sequential orchestration
- simple branching
- retries
- checkpointed side effects
- readable TypeScript
But they are weaker as a workflow representation when users need:
- many possible external events while a run is paused
- UI-driven or human-driven workflow states
- visualization
- static analysis
- simulation
- diffing workflow definitions
- AI-generated workflows
- graph editors
- "what events are valid right now?" tooling
- "what states can this workflow be in?" tooling
State machines make the workflow itself inspectable. The core question is whether TanStack Workflow can run those machine definitions durably without making XState or graph workflows the foundation of the entire runtime.
Non-goals
- Do not replace
defineWorkflow or ctx.step.
- Do not rearchitect the durable store around XState.
- Do not require XState for users who prefer code-shaped workflows.
- Do not treat arbitrary XState actions as safe durable side effects.
- Do not rely on in-memory XState timers for durable workflow delays.
Proposed Architecture
TanStack Workflow should support multiple authoring models over the same durable substrate:
TanStack Workflow durable runtime
- runs
- event log
- latest durable state/snapshot
- durable step/effect results
- timers
- signals/events
- leases
- schedules
- tracing
Async/await authoring
- code-shaped workflows
- replay/checkpoint-driven execution
Machine authoring
- data-shaped workflows
- snapshot + event transition interpreter
- durable effects via ctx.step/checkpoints
The machine layer should be snapshot-first and event-log-backed:
- Load the current persisted machine snapshot for the run.
- Accept an incoming event/signal/timer/approval.
- Transition the machine from the current snapshot.
- Persist the new snapshot.
- Execute any immediately runnable invoked actors/effects through Workflow durable step primitives.
- Feed actor completion/error events back into the machine.
- Continue until the machine reaches a final state or is waiting for external input/timer work.
Normal machine execution should not need to replay the entire workflow from the beginning. The event log remains important for auditability, debugging, deterministic tests, migrations, rebuilds, and observability.
Core Durability Rule
XState may describe effects, but TanStack Workflow must execute/checkpoint them.
For example, this XState shape is fine:
invoke: {
src: 'chargeCard',
onDone: 'paid',
onError: 'paymentFailed',
}
But the adapter should execute it through a durable Workflow boundary:
await ctx.step('xstate.actor.chargeCard.<stable-id>', () =>
actors.chargeCard(args),
)
If the process crashes after the payment provider succeeds but before the machine snapshot is fully advanced, the durable step result should prevent the payment actor from being rerun.
Proposed Public API Sketch
const workflow = defineXStateWorkflow({
id: 'order-fulfillment',
version: '1',
machine: orderMachine,
actors: {
chargeCard,
reserveInventory,
shipOrder,
},
inputToEvent: (input) => ({ type: 'ORDER_CREATED', input }),
})
Possible lower-level interpreter API:
await runXStateWorkflow(ctx, {
machine,
actors,
input,
})
Potential event delivery API:
await runtime.deliverSignal({
runId,
name: 'xstate.event',
payload: { type: 'APPROVE', approverId },
})
Or a typed helper:
await runtime.sendMachineEvent({
runId,
event: { type: 'APPROVE', approverId },
})
Possible Runtime Primitive Needed
Machine workflows often wait for one of many valid events, not one specific signal.
We should evaluate adding a primitive like:
const event = await ctx.waitForAny([
'APPROVE',
'REJECT',
'CANCEL',
])
or:
const event = await ctx.waitForEvent()
For machine workflows, the valid event set can come from the machine snapshot/state. This avoids Promise.race([...all possible signals]) style workarounds.
XState Mapping
Machine states
Persist as durable machine snapshots. Prefer XState's native snapshot/persistence APIs where possible.
Events
Map Workflow signals/events/timers/approvals to XState events.
Invoked actors/services
Map to durable Workflow steps. Actor IDs must produce stable step IDs.
Guards
Should be deterministic and side-effect free. Any DB/network/time/random/AI work should be modeled as a durable actor/effect instead.
Actions
Pure actions such as assign are safe. Side-effectful actions are dangerous unless explicitly routed through Workflow durable effects. Docs should strongly recommend keeping XState actions pure.
Delays / after
Must map to Workflow durable timers. Do not rely on in-memory timers.
Final states
Map to Workflow run completion.
Errors
Actor errors should become XState error events and also use Workflow's failure/retry semantics where configured.
Persistence Model
A machine workflow likely needs to persist:
- current XState snapshot
- accepted event log
- durable actor/effect results
- timers scheduled from delayed transitions
- run metadata/status
Open question: should the machine snapshot be stored in the existing run state/events model, or does the durable store contract need explicit machine snapshot methods?
A plausible first version can store the machine snapshot as part of workflow run state without requiring new top-level store primitives, then promote schema/store APIs if dogfooding shows pressure.
Observability
The existing Workflow OTel tracing should naturally wrap machine runs:
- runtime operations
- store boundaries
- drive run
- durable actor/step execution
Machine-specific attributes/events could include:
tanstack.workflow.machine.id
tanstack.workflow.machine.state
tanstack.workflow.machine.event_type
tanstack.workflow.machine.actor_id
Need to preserve the same privacy posture: no event payloads, workflow input, actor result, or context payload values in attributes by default.
Example Dogfood Scenario
Build a demo workflow that benefits from being graph-shaped:
- order approval or content publishing workflow
- starts from input
- waits for any of several events: approve, reject, request changes, cancel
- has at least one durable actor invocation
- has at least one durable timer / timeout transition
- can render a visualization from the same machine definition
- can expose valid next events to UI
- can resume from a persisted snapshot without replaying all prior events
This should be a serious demo, not a toy counter.
Open Questions
- What exact XState v5 APIs should we lean on for snapshot persistence, hydration, actors, and inspection?
- Can invoked actors be cleanly intercepted/executed by Workflow without fighting XState internals?
- How should durable timers map to XState
after transitions?
- Should Workflow expose a generic
waitForAny / waitForEvent primitive independent of XState?
- Are side-effectful XState actions forbidden, warned against, or supported through a special adapter API?
- How do we produce stable step IDs for actors across machine edits?
- How should versioning/migrations work when the machine graph changes while runs are in flight?
- Should machine snapshots live in existing run-state persistence or get explicit store contract support?
- How much type inference can we preserve from XState events/context/actors through the Workflow adapter?
- Should this start as
@tanstack/workflow-xstate experimental, or live in core behind optional peer dependency boundaries?
Proposed First Milestone
Ship an experimental @tanstack/workflow-xstate package that proves:
- an XState machine can be run as a TanStack Workflow
- current machine snapshot is persisted and resumed
- external events wake and transition the machine
- invoked actors execute through durable Workflow steps
- replay/wake-up does not rerun completed actors
- delayed transitions use durable Workflow timers
- final states complete the Workflow run
- docs clearly explain pure transitions/actions vs durable effects
Acceptance Criteria
- Users can bring an XState machine and run it on TanStack Workflow durability primitives.
- The adapter does not require apps to abandon existing async/await workflows.
- Durable side effects remain checkpointed by Workflow.
- Machine workflows can expose valid next events/state for UI/tooling.
- The implementation avoids payload/result/context leakage in telemetry by default.
- The design has review from people familiar with XState/statecharts and durable execution tradeoffs.
Review Request
Would love review especially from folks with XState/statechart experience and folks building long-lived human/event-driven workflows.
Specific feedback wanted:
- Is this the right boundary between XState and TanStack Workflow?
- Which XState concepts map cleanly, and which ones should we intentionally not support at first?
- Is snapshot-first, event-log-backed durability the right model?
- What awkward cases would make this adapter frustrating in real apps?
- What demo would prove this is actually useful?
Summary
Add an optional, first-class machine-workflow layer for TanStack Workflow, initially centered on XState. The goal is to support workflows as data structures without replacing the existing async/await authoring model.
The durable runtime primitives stay the same: run state, events/signals, timers, leases, durable steps/checkpoints, persistence, retries, and observability. The machine layer gives users a graph-shaped workflow representation that can be annotated, analyzed, visualized, diffed, simulated, generated, and used to drive UI.
Proposed package shape:
Motivation
Async/await workflows are excellent for human-authored, procedural workflows:
But they are weaker as a workflow representation when users need:
State machines make the workflow itself inspectable. The core question is whether TanStack Workflow can run those machine definitions durably without making XState or graph workflows the foundation of the entire runtime.
Non-goals
defineWorkfloworctx.step.Proposed Architecture
TanStack Workflow should support multiple authoring models over the same durable substrate:
The machine layer should be snapshot-first and event-log-backed:
Normal machine execution should not need to replay the entire workflow from the beginning. The event log remains important for auditability, debugging, deterministic tests, migrations, rebuilds, and observability.
Core Durability Rule
XState may describe effects, but TanStack Workflow must execute/checkpoint them.
For example, this XState shape is fine:
But the adapter should execute it through a durable Workflow boundary:
If the process crashes after the payment provider succeeds but before the machine snapshot is fully advanced, the durable step result should prevent the payment actor from being rerun.
Proposed Public API Sketch
Possible lower-level interpreter API:
Potential event delivery API:
Or a typed helper:
Possible Runtime Primitive Needed
Machine workflows often wait for one of many valid events, not one specific signal.
We should evaluate adding a primitive like:
or:
For machine workflows, the valid event set can come from the machine snapshot/state. This avoids
Promise.race([...all possible signals])style workarounds.XState Mapping
Machine states
Persist as durable machine snapshots. Prefer XState's native snapshot/persistence APIs where possible.
Events
Map Workflow signals/events/timers/approvals to XState events.
Invoked actors/services
Map to durable Workflow steps. Actor IDs must produce stable step IDs.
Guards
Should be deterministic and side-effect free. Any DB/network/time/random/AI work should be modeled as a durable actor/effect instead.
Actions
Pure actions such as
assignare safe. Side-effectful actions are dangerous unless explicitly routed through Workflow durable effects. Docs should strongly recommend keeping XState actions pure.Delays /
afterMust map to Workflow durable timers. Do not rely on in-memory timers.
Final states
Map to Workflow run completion.
Errors
Actor errors should become XState error events and also use Workflow's failure/retry semantics where configured.
Persistence Model
A machine workflow likely needs to persist:
Open question: should the machine snapshot be stored in the existing run state/events model, or does the durable store contract need explicit machine snapshot methods?
A plausible first version can store the machine snapshot as part of workflow run state without requiring new top-level store primitives, then promote schema/store APIs if dogfooding shows pressure.
Observability
The existing Workflow OTel tracing should naturally wrap machine runs:
Machine-specific attributes/events could include:
tanstack.workflow.machine.idtanstack.workflow.machine.statetanstack.workflow.machine.event_typetanstack.workflow.machine.actor_idNeed to preserve the same privacy posture: no event payloads, workflow input, actor result, or context payload values in attributes by default.
Example Dogfood Scenario
Build a demo workflow that benefits from being graph-shaped:
This should be a serious demo, not a toy counter.
Open Questions
aftertransitions?waitForAny/waitForEventprimitive independent of XState?@tanstack/workflow-xstateexperimental, or live in core behind optional peer dependency boundaries?Proposed First Milestone
Ship an experimental
@tanstack/workflow-xstatepackage that proves:Acceptance Criteria
Review Request
Would love review especially from folks with XState/statechart experience and folks building long-lived human/event-driven workflows.
Specific feedback wanted: