@tanstack/workflow-core is the replay engine and authoring API.
Creates a workflow definition.
import { createWorkflow } from '@tanstack/workflow-core'
const workflow = createWorkflow({
id: 'checkout',
version: 'v1',
input,
output,
state,
}).handler(async (ctx) => {
return { ok: true }
})import { createWorkflow } from '@tanstack/workflow-core'
const workflow = createWorkflow({
id: 'checkout',
version: 'v1',
input,
output,
state,
}).handler(async (ctx) => {
return { ok: true }
})Important config:
| Option | Purpose |
|---|---|
| id | Stable workflow ID. |
| version | Optional version persisted with started runs. |
| input | Optional Standard Schema validator for workflow input. |
| output | Optional Standard Schema validator for workflow output. |
| state | Optional Standard Schema validator for workflow state. |
Runs a side effect durably.
const result = await ctx.step('charge-card', async (stepCtx) => {
return stripe.charges.create(
{ amount: ctx.input.amount },
{ idempotencyKey: stepCtx.id },
)
})const result = await ctx.step('charge-card', async (stepCtx) => {
return stripe.charges.create(
{ amount: ctx.input.amount },
{ idempotencyKey: stepCtx.id },
)
})On replay, a completed step returns the recorded result and does not call the function again.
Use stepCtx.id as the idempotency key for external systems.
Pauses until a signal is delivered.
const now = await ctx.now()
const payment = await ctx.waitForEvent<{ paymentId: string }>(
'payment-received',
{
id: 'payment-webhook',
deadline: now + 24 * 60 * 60_000,
meta: { orderId: ctx.input.orderId },
},
)const now = await ctx.now()
const payment = await ctx.waitForEvent<{ paymentId: string }>(
'payment-received',
{
id: 'payment-webhook',
deadline: now + 24 * 60 * 60_000,
meta: { orderId: ctx.input.orderId },
},
)Resume with runWorkflow({ signalDelivery }) or runtime.deliverSignal(...).
Pauses until an approval is delivered.
const decision = await ctx.approve({
id: 'refund-approval',
title: 'Approve refund?',
description: `Refund ${ctx.input.amount}`,
})const decision = await ctx.approve({
id: 'refund-approval',
title: 'Approve refund?',
description: `Refund ${ctx.input.amount}`,
})Resume with runWorkflow({ approval }) or runtime.deliverApproval(...).
Pauses until a timer deadline.
await ctx.sleep(30_000)
const now = await ctx.now()
await ctx.sleepUntil(now + 30_000, { id: 'cooldown' })await ctx.sleep(30_000)
const now = await ctx.now()
await ctx.sleepUntil(now + 30_000, { id: 'cooldown' })In the runtime package, due timers are resumed by runtime.sweep().
Records deterministic values.
const now = await ctx.now({ id: 'started-at' })
const id = await ctx.uuid({ id: 'correlation-id' })const now = await ctx.now({ id: 'started-at' })
const id = await ctx.uuid({ id: 'correlation-id' })Use these instead of Date.now() and crypto.randomUUID() when the value affects workflow control flow or output.
For sleep, sleepUntil, waitForEvent, approve, now, and uuid, the optional id is the stable durable-operation ID used for replay. If omitted, the engine generates a positional internal ID.
Low-level engine entrypoint.
for await (const event of runWorkflow({
workflow,
input,
runId,
runStore,
signalDelivery,
approval,
})) {
console.log(event.type)
}for await (const event of runWorkflow({
workflow,
input,
runId,
runStore,
signalDelivery,
approval,
})) {
console.log(event.type)
}Use this directly for tests, custom runtimes, or advanced embedding. Use defineWorkflowRuntime for the production runtime/store/sweep model.
Stateless one-invocation helper around the same engine.
await handleWorkflowWebhook({
workflow,
runStore,
payload: {
runId,
signalDelivery,
},
})await handleWorkflowWebhook({
workflow,
runStore,
payload: {
runId,
signalDelivery,
},
})Extends workflow context.
const auth = createMiddleware().server<{
user: { id: string }
}>(async ({ next }) => {
return next({ context: { user: await loadUser() } })
})
const workflow = createWorkflow({ id: 'authed' })
.middleware([auth])
.handler(async (ctx) => {
return { userId: ctx.user.id }
})const auth = createMiddleware().server<{
user: { id: string }
}>(async ({ next }) => {
return next({ context: { user: await loadUser() } })
})
const workflow = createWorkflow({ id: 'authed' })
.middleware([auth])
.handler(async (ctx) => {
return { userId: ctx.user.id }
})import type {
WorkflowInput,
WorkflowOutput,
WorkflowState,
} from '@tanstack/workflow-core'
type Input = WorkflowInput<typeof workflow>
type Output = WorkflowOutput<typeof workflow>
type State = WorkflowState<typeof workflow>import type {
WorkflowInput,
WorkflowOutput,
WorkflowState,
} from '@tanstack/workflow-core'
type Input = WorkflowInput<typeof workflow>
type Output = WorkflowOutput<typeof workflow>
type State = WorkflowState<typeof workflow>For full generated types and interfaces, see Generated core reference.