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.

Tool manifest (optional)

The engine has no declarative knowledge of an agent's tools until a run calls one - steps above solves this for steps, and tools is the same fix for tools. Declaring an optional tools manifest lets a UI list a tool - its name, description, and parameter schema - before it has ever run, which is what a Builder-style tool picker needs.

Build it with @duraton/agent-kit's toolManifest(), from the same tools/mcpServers arrays the handler's agent() call uses - one array, two readers, so the manifest can never silently disagree with what the model actually sees:

import { agent, tool, toolManifest } from "@duraton/agent-kit";

const tools = [
  tool({
    name: "charge_card",
    description: "Charges the customer's card",
    inputSchema: { type: "object", properties: { amount: { type: "number" } } },
    integration: "stripe",
    requiresApproval: true,
    handler: (input) => chargeCard(input),
  }),
];

const support = defineWorkflow({
  name: "support.ticket",
  tools: toolManifest({ tools }),
  handler: (ctx) =>
    agent(ctx, "support", { model: "claude-opus-4-8", prompt: "Resolve the ticket", tools }),
});

Prop

Type

The manifest is advisory in the same way steps is - it never gates a run, never fails a run for drift, and a workflow with no manifest behaves exactly as before. AC#11's both directions hold: a tool the agent offers that is absent from the manifest still runs, and an entry the agent no longer offers never blocks a run.

An MCP server's tools are never enumerated here. toolManifest() projects an attached mcpServers entry as a group placeholder - only its name and source: "mcpServer" - because discovery is a durable step at run time, not registration time. Contacting the server up front to fill the manifest would reintroduce the exact replay drift durable discovery exists to prevent (a server that adds or drops a tool mid-run must not change what a replay sees), and would make registration depend on a third party being reachable. A picker reading the manifest sees the server as an unexpanded group whose tools become known once a run has actually discovered them.

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