> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Defining workflows

> Declare a workflow in one object: name, triggers, retry policy, flow control, and handler - and get ctx.event.data typed for you.

## workflow

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`.

```ts theme={null}
const ticketCreated = workflow<{ 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));
  },
});
```

<ResponseField name="name" type="string" required>
  Unique workflow name.
</ResponseField>

<ResponseField name="handler" type="(ctx: StepContext<TData>) => Promise<unknown>" required>
  The function that does the work; receives the run context.
</ResponseField>

<ResponseField name="triggers" type="Trigger[]">
  What starts the workflow: event triggers (\{ event, if? }, where event may end in a trailing "\*" wildcard and if is a CEL filter) and cron triggers (\{ cron }, with an optional "TZ=Area/City" prefix). Omit for an implicit event trigger matching the workflow name.
</ResponseField>

<ResponseField name="retry" type="RetryConfig">
  Per-workflow retry policy each step inherits (\{ maxAttempts, backoff?, initialDelayMs?, maxDelayMs? }).
</ResponseField>

<ResponseField name="concurrency" type="ConcurrencyConfig">
  Cap on runs executing at once in a scope (\{ limit, key? }); over-limit runs wait and retry as slots free.
</ResponseField>

<ResponseField name="throttle" type="RateConfig">
  Smooth cap on run starts (\{ limit, perMs, key? }); overflow is delayed into the future, never dropped.
</ResponseField>

<ResponseField name="rateLimit" type="RateConfig">
  Shedding cap on run starts (\{ limit, perMs, key? }); overflow is dropped and the event response reports dropped: true.
</ResponseField>

<ResponseField name="debounce" type="DebounceConfig">
  Collapse a burst to its last event (\{ periodMs, key? }); each new event slides the deadline and replaces the payload.
</ResponseField>

<ResponseField name="batch" type="BatchConfig">
  Fold many events into one run (\{ maxSize, timeoutMs, key? }), flushing on whichever comes first; the run reads them as ctx.events.
</ResponseField>

<ResponseField name="priority" type="PriorityConfig">
  Dequeue this workflow's runs ahead of others (\{ shiftMs }) by treating them as enqueued that many ms earlier.
</ResponseField>

<ResponseField name="singleton" type="SingletonConfig">
  At most one non-terminal run per key (\{ key?, mode? }); mode "cancel" (the default) cancels the running run, "skip" drops the new trigger.
</ResponseField>

<ResponseField name="idempotency" type="IdempotencyConfig">
  One run per derived key within a window (\{ key?, periodMs? }, periodMs defaulting to 24h); a duplicate is dropped and the response reports deduped: true.
</ResponseField>

<ResponseField name="cap" type="CapConfig">
  Hard per-run AI spend ceiling (\{ maxCost?, maxTokens? }); a run halts before the step.ai call that would cross it and fails with a BudgetError.
</ResponseField>

<ResponseField name="tokenThrottle" type="TokenThrottleConfig">
  Token-denominated rate limit on the workflow's AI spend (\{ tokens, perMs, key? }); each step's actual tokens are debited and new run starts for a drained key are spread into the future, holding no worker.
</ResponseField>

<ResponseField name="steps" type="StepManifestEntry[]">
  Optional advisory manifest of the workflow's steps in declaration order, so a UI can render a progress bar ("step 12 of 18") and mark bookkeeping steps hidden. Advisory only: it never constrains execution and is diffed against the actually-executed steps.
</ResponseField>

<ResponseField name="tools" type="ToolManifestEntry[]">
  Optional advisory manifest of the workflow's tools, so a UI can list a tool that has never run. Advisory only: it never constrains what an agent actually calls.
</ResponseField>

<ResponseField name="agents" type="AgentManifestEntry[]">
  Optional advisory manifest of the workflow's agent loops - what bounds each one, never what it says. Advisory only: it never constrains execution. See Agent manifest below.
</ResponseField>

<ResponseField name="onFailure" type="(ctx: StepContext<TData>) => Promise<unknown>">
  Runs durably after the run has exhausted its retries and failed; receives the original event plus ctx.error. It cannot un-fail the run.
</ResponseField>

## 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
order, 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.

```ts theme={null}
const checkout = workflow({
  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());
  },
});
```

<ResponseField name="name" type="string" required>
  The step id. A declared step is matched to an executed step by this name.
</ResponseField>

<ResponseField name="description" type="string">
  Optional human-readable label for the step.
</ResponseField>

<ResponseField name="hidden" type="boolean">
  Excludes the step from the customer-facing progress view; it stays present for admin/debug views.
</ResponseField>

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 order the handler runs them, including in parallel - the
  manifest order is presentation order 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.

## 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:

```ts theme={null}
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 = workflow({
  name: "support.ticket",
  tools: toolManifest({ tools }),
  handler: (ctx) =>
    agent(ctx, "support", { model: "claude-opus-4-8", prompt: "Resolve the ticket", tools }),
});
```

<ResponseField name="name" type="string" required>
  The tool name the model calls.
</ResponseField>

<ResponseField name="description" type="string">
  Optional human-readable label.
</ResponseField>

<ResponseField name="inputSchema" type="Record<string, unknown>">
  The tool's JSON Schema input, so a UI can render a parameter form.
</ResponseField>

<ResponseField name="outputSchema" type="Record<string, unknown>">
  The tool's JSON Schema output, if declared.
</ResponseField>

<ResponseField name="annotations" type="Record<string, unknown>">
  MCP behaviour hints (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). They inform a default; they never gate.
</ResponseField>

<ResponseField name="requiresApproval" type="boolean">
  Whether this tool parks a run on a human before it executes.
</ResponseField>

<ResponseField name="integration" type="string">
  The integration this tool reaches, e.g. "google-calendar" - a free-text label, not a Credential id. A code-declared tool is the same code for every tenant, so it names an integration, never a specific tenant's credential.
</ResponseField>

<ResponseField name="agentId" type="string">
  The agent() call this tool belongs to, for a workflow with more than one agent. Undefined for the common one-agent-per-workflow case.
</ResponseField>

<ResponseField name="source" type="&#x22;declared&#x22; | &#x22;mcpServer&#x22;" required>
  A hand-declared tool, or an attached MCP server.
</ResponseField>

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](/core/flow-control). `onFailure` is covered in
[retries](/core/retries), triggers in [triggers](/core/triggers).

## RetryConfig

The shape of the `retry` field - the per-workflow retry policy each step inherits.

```ts theme={null}
retry: { maxAttempts: 3 }
```

<ResponseField name="maxAttempts" type="number" required>
  Total attempts before the run fails, counting the first one.
</ResponseField>

<ResponseField name="backoff" type="&#x22;fixed&#x22; | &#x22;linear&#x22; | &#x22;exponential&#x22;" default="&#x22;fixed&#x22;">
  How the delay between attempts grows: fixed is constant; linear is initialDelayMs \* attempt; exponential is initialDelayMs \* 2^(attempt-1).
</ResponseField>

<ResponseField name="initialDelayMs" type="number" default="1000">
  The base delay before the second attempt, and the unit the backoff shape multiplies.
</ResponseField>

<ResponseField name="maxDelayMs" type="number" default="30000">
  An upper bound the computed delay is capped at, so exponential backoff cannot grow without limit.
</ResponseField>

## Agent manifest (optional)

Everything that shapes an agent - its model, its ceilings, its guardrails, the conditions
that stop it - lives in your code, so nothing about it is visible until a run has happened.
Declaring an `agents` manifest reflects that configuration onto the workflow definition, so
a console can list the agent, its model and what bounds it before it has ever run.

Build it with the kit's `agentManifest()` from the same options object the handler passes
to `agent()`, so the two cannot drift:

```ts theme={null}
import { agent, agentManifest, tool, toolManifest } from "@duraton/agent-kit";
import { workflow, wallClock } from "@duraton/sdk";

const tools = [tool({ name: "refund", inputSchema: { type: "object" }, handler: issueRefund })];
const options = {
  model: "claude-opus-5",
  prompt: "Resolve the ticket.",
  maxIterations: 8,
  stopWhen: [wallClock({ within: "5m" })],
  tools,
};

export const triage = workflow({
  name: "triage",
  tools: toolManifest({ tools }),
  agents: [agentManifest("triage", options)],
  handler: (ctx) => agent(ctx, "triage", { ...options, prompt: ctx.event.data.body }),
});
```

The entry's `id` is the `agent()` step id, which is also the `agentId` its tools carry in
the tool manifest - so the tools belonging to an agent are recoverable without listing
their declarations twice.

<Note>
  The manifest deliberately has no `prompt` and no `instructions` field. Prompt text never
  enters the durable record, and a definition row an operator can read is that same
  audience.
</Note>

Like the step and tool manifests it is one-way and advisory. Duraton stores it, re-emits it
on the workflow read, and never reads it back to drive execution: an agent that no longer
matches its entry still runs, and a workflow that omits it behaves exactly as before.
