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.
  • hidden steps 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

On this page