Defining workflows
Declare a workflow in one object: defineWorkflow takes its name, triggers, retry policy, flow control, and handler, and types ctx.event.data for you.
defineWorkflow
Declares a workflow: its name, what triggers it, how it retries and runs under load, and the handler
that does the work. It returns the definition unchanged - it exists for type inference over your event
data, so a type parameter types ctx.event.data.
const ticketCreated = defineWorkflow<{ ticketId: string }>({
name: "ticket.created",
triggers: [{ event: "ticket.created", if: "event.data.priority == 'high'" }],
retry: { maxAttempts: 3 },
concurrency: { limit: 5, key: "ticketId" },
handler: async (ctx) => {
const { ticketId } = ctx.event.data; // typed as { ticketId: string }
return ctx.step.run("refund", () => issueRefund(ticketId));
},
});Prop
Type
Step manifest (optional)
By default a step exists only once it has executed - Duraton discovers steps at runtime, so
the run view has no plan to draw a progress bar from until each step has run. Declaring an
optional steps manifest gives the UI the plan up front: the declared step names, their
ticket, an optional description, and a hidden flag for bookkeeping steps.
Reach for it only when a workflow has several steps to preview, describe, or hide. A
single-step workflow, or one with nothing to describe or hide, needs no manifest - omit it and
the run view still renders each step as it executes. The manifest's name must match the id you
pass to ctx.step.run; that is the only reason a step id appears twice.
const checkout = defineWorkflow({
name: "checkout",
steps: [
{ name: "validate", description: "Check the cart" },
{ name: "triage" },
{ name: "audit", hidden: true }, // present for admin/debug, hidden from the customer view
{ name: "refund", description: "Return the money" },
],
handler: async (ctx) => {
await ctx.step.run("validate", () => validate());
await ctx.step.run("triage", () => triage());
await ctx.step.run("audit", () => audit());
return ctx.step.run("refund", () => issueRefund());
},
});Prop
Type
The manifest is advisory metadata for rendering, never a constraint on execution:
- It never gates a run, never fails a run for drift, and a workflow with no manifest behaves exactly as before.
- The run view diffs declared vs executed steps by name: a declared step that has not run yet renders as pending (this is what powers "step 12 of 18"); a step that runs but was not declared still renders (discovery wins); a declared step that a conditional path skips simply stays pending / not-reached.
- Steps execute at runtime in whatever ticket the handler runs them, including in parallel - the manifest ticket is presentation ticket only.
hiddensteps are excluded from the customer-facing progress but returned in the read model so an admin/debug view can show them.
The manifest is returned by GET /workflows and the list_workflows MCP tool alongside each
workflow's triggers and flow control. Declaring it in TypeScript today; Python and Go SDKs are
on the roadmap.
Every flow-control field is optional and off by default; each one's semantics, keys, and overflow
behaviour are in the flow-control reference. onFailure is covered in
retries, triggers in triggers.
RetryConfig
The shape of the retry field - the per-workflow retry policy each step inherits.
retry: { maxAttempts: 3 }Prop
Type