Steps
A step is a named, reusable unit of work. You define steps once and reference them by id inside flows.
Defining a Step
app.step('step-id', {
handler: (ctx) => {
// your logic here
},
})The handler receives the workflow context as its first argument. You can mutate it directly:
app.step('increment', {
handler: (ctx) => {
ctx.counter++
},
})Step IDs Must Be Unique
Steps are registered on a router, and the first registration of an id wins — registering the same id again is ignored and the later handler is never reached. A duplicate logs a warning so the mistake is visible instead of silent:
WF step "increment" registered more than once — the first registration wins.By default the router is a process-global singleton, shared across every workflow app created without an explicit Wooks instance. So ids can collide across apps, not just within one — this most often bites tests that build a fresh app per case and reuse an id.
Fail loudly with strictStepIds
Pass strictStepIds: true to turn a duplicate id into a thrown error instead of a warning — handy in CI where a collision should fail the build:
const app = createWfApp({ strictStepIds: true })
app.step('process', { handler: () => {} })
app.step('process', { handler: () => {} })
// throws: WF step "process" already registered. Step ids must be unique (strictStepIds enabled).Resetting the router between tests
Because the router is shared, reset it before each test so ids need not be globally unique and each test stays hermetic:
import { clearGlobalWooks } from 'wooks'
beforeEach(() => clearGlobalWooks())After the reset every test gets a fresh router, so re-registering the same ids across cases is fine. The same clearGlobalWooks() call is what HMR / dev-restart flows use to re-register steps cleanly.
Accessing State with useWfState
Inside any step handler, call useWfState() to access the full workflow execution state:
import { useWfState } from '@wooksjs/event-wf'
app.step('process', {
handler: () => {
const { ctx, input, schemaId, stepId, indexes, resume } = useWfState()
const context = ctx<MyContext>() // typed workflow context
const stepInput = input<string>() // input provided for this step (if any)
const flowId = schemaId // id of the running flow
const currentStep = stepId() // normalized step id, e.g. '/add/5' (null before the first step)
const position = indexes() // position in the flow schema
const isResumed = resume // true if this is a resumed execution
},
})useWfState() works from anywhere in the call stack — it uses AsyncLocalStorage under the hood, so you can call it from utility functions, not just directly in the handler.
Parametric Steps
Step ids support route-style parameters. This lets you create generic steps that receive values through their id.
import { useRouteParams } from '@wooksjs/event-core'
app.step('add/:n', {
handler: (ctx) => {
const n = Number(useRouteParams().get('n'))
ctx.result += n
},
})Now you can call this step with different values in your flow:
app.flow('calculate', ['add/5', 'add/10', 'add/3'])Supported Routing Patterns
Step ids use @prostojs/router syntax:
| Pattern | Example | Matches |
|---|---|---|
| Static | validate | Exactly validate |
| Named parameter | add/:n | add/5, add/100 |
| Multiple parameters | move/:from/:to | move/inbox/archive |
| Optional parameter | log/:level? | log and log/debug |
| Wildcard | notify/* | notify/email, notify/slack/general |
Access parameters with useRouteParams().get('paramName') — use get('*') for wildcard captures.
String Handlers
For lightweight, serializable steps, you can use a JavaScript expression string instead of a function:
app.step('add', {
input: 'number',
handler: 'ctx.result += input',
})
app.step('double', {
handler: 'ctx.result *= 2',
})String handlers run in a restricted sandbox with only ctx (the workflow context), input (the step input), and StepRetriableError available. They cannot access Node.js APIs, imports, or composables.
A string handler signals a retriable failure by returning (not throwing) a StepRetriableError:
app.step('check-balance', {
handler: 'ctx.balance < 0 ? new StepRetriableError(new Error("negative balance")) : undefined',
})Use string handlers when you need steps to be serializable (e.g., stored in a database or sent over the wire). Use function handlers for everything else.
Step with Required Input
A step can declare that it requires input. If the input is not provided when the step runs, the workflow pauses and waits for it.
app.step('get-approval', {
input: 'boolean', // declares that this step needs input
handler: (ctx) => {
const { input } = useWfState()
ctx.approved = input<boolean>() ?? false
},
})See Input & Resume for the full pause/resume pattern.
Handling Retriable Errors
If a step fails but can be retried, throw a StepRetriableError:
import { StepRetriableError } from '@wooksjs/event-wf'
app.step('call-api', {
handler: async (ctx) => {
const res = await fetch(ctx.apiUrl)
if (!res.ok) {
throw new StepRetriableError(new Error(`API returned ${res.status}`))
}
ctx.data = await res.json()
},
})The workflow pauses with the error available on the output. You can retry by resuming:
const output = await app.start('my-flow', initialContext)
if (!output.finished && output.error) {
console.log(output.error.message) // "API returned 503"
// retry the failed step:
const retried = await output.retry()
// equivalent: await app.resume(output.state)
}Regular (non-retriable) errors propagate normally and are thrown from start() / resume().