| id | middleware |
|---|---|
| title | Middleware |
Middleware extends ctx with typed fields. Workflows declare them as an array — extensions accumulate.
import { createMiddleware } from '@tanstack/workflow-core'
const requireUser = createMiddleware().server<{
user: { id: string; email: string }
}>(async ({ next }) => {
const user = await loadUser()
if (!user) throw new Error('unauthorized')
return next({ context: { user } })
})The generic on .server<...> is the extension shape. TS uses it to add ctx.user everywhere the middleware is registered.
const wf = createWorkflow({ id: 'wf' })
.middleware([requireUser])
.handler(async (ctx) => {
ctx.user.id // typed
})const traced = createMiddleware().server<{ trace: Trace }>(async ({ next }) => {
const trace = startTrace()
try {
return await next({ context: { trace } })
} finally {
trace.end()
}
})next is called once. Code before runs pre-handler; code after runs post.
const requireUser = createMiddleware().server<{ user: User }>(
async ({ next }) => next({ context: { user: await loadUser() } }),
)
// Reaches ctx.user — type the inbound ctx with the generic on createMiddleware.
const requirePro = createMiddleware<{ user: User }>().server<{ tier: 'pro' }>(
async ({ ctx, next }) => {
if (ctx.user.tier !== 'pro') throw new Error('pro required')
return next({ context: { tier: 'pro' } })
},
)
createWorkflow({ id: 'wf' })
.middleware([requireUser, requirePro]) // order matters
.handler(async (ctx) => {
ctx.user // from requireUser
ctx.tier // from requirePro
})import type { WorkflowCtx } from '@tanstack/workflow-core'
async function sendReceipt(
ctx: WorkflowCtx<{ user: User }>,
amount: number,
) {
await ctx.step('send-receipt', () => mailer.send(ctx.user.email, amount))
}Pass the typed ctx to the helper — the constraint documents which middleware fields must be in scope.
.middleware([a, b])runsafirst, thenb, then the handler.- Each middleware must call
next()exactly once. Twice throwsRUN_ERRORED. - Middleware extensions cannot shadow reserved ctx fields (
input,state,runId,signal,step,sleep,sleepUntil,waitForEvent,approve,now,uuid,emit). Type system rejects them; runtime guards too.
- Implicit ctx inference fails. The
.server<TExtension>(...)generic is mandatory; bare.server(fn)defaultsTExtensiontounknownand ctx fields aren't visible. - Middleware errors abort the run. A throw before
next()skips the handler entirely; status becomeserrored.