# Agent Work Steps (/docs/ai/workflow-ai-steps)



The [chat agent](/docs/ai/chat-agent) is the *interactive* face of Ductor's agent
loop. This page is the *headless* one: two step types that run AI inside a
[DAG workflow run](/docs/concepts/dag-workflow-model), driven by workers rather than a
browser. Same underlying machinery, different host — a workflow step has no operator
watching a stream, so it is execution-bound, resumable, and metered as part of the run.

| Step type   | What it does                                                                                                                   |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `ai_action` | A single LLM inference through the [inference proxy](/docs/ai/inference-proxy), with optional structured-output validation.    |
| `ai_agent`  | A workflow host for the shared durable agent runtime, with preset resolution, governed tools, approvals, and execution bounds. |

## ai\_action — one inference [#ai_action--one-inference]

`ai_action` (executed by `AIActionExecutor`) is the simple case: it parses the step
args, builds an inference request, calls `RouteInference` on the AI service, and — when
the step declares an output JSON Schema — validates the response against it before
returning a structured result. Use it when you want the model to transform or classify
some data as one deterministic step in a pipeline, not to run a multi-turn agent.

## ai\_agent — a durable runtime host [#ai_agent--a-durable-runtime-host]

`ai_agent` is executed by `DurableAgentExecutor`. It maps a DAG node onto the shared
[durable agent runtime](/docs/ai/durable-agent-runtime): the model proposes tool calls,
the governed broker authorizes and executes them, results are journaled, and the
runtime continues until completion, a durable input request, or an execution bound.

* **Preset resolution.** The step can reference a tenant
  [agent preset](/docs/ai/agent-presets) (by id, optionally version-pinned); the
  `agent_preset_resolver` loads and resolves it before the loop starts.
* **Governed tool execution.** The canonical registry resolves a content-addressed
  tool definition; the policy receipt and argument hash are bound to the invocation
  before the connector action runs.
* **Resumable approval state.** An approval-gated invocation becomes a workflow
  approval wait. The node persists the agent session, turn, request id, and expected
  journal sequence. Resume must match all four before the approved invocation runs.
* **Execution bounds.** `max_tool_calls` / `max_requests` cap the loop, and crossing a
  bound is recorded distinctly.
* **Typed external input.** Approval, question, budget extension, external-auth, and
  reconciliation pauses use the same actor-bound, expiring, single-use protocol.
* **Bounded delegation.** Inline definitions may declare exact/versioned specialist
  presets, tool intersections, depth/fanout/concurrency ceilings, child budgets, and
  JSON output schemas. A model supplies only task ids and prompts.
* **Scoped memory.** A pinned memory policy may recall only verified
  tenant/subject/entity records. Recalled text is always untrusted user-role context.

```mermaid
flowchart TD
  R["DAG run"] --> AA["step: ai_action"]
  R --> AG["step: ai_agent"]
  AA --> RI["RouteInference"]
  RI --> V["Validate schema"]
  V --> R1["result"]
  AG --> RP["Resolve preset"]
  RP --> LP["Shared agent runtime<br/>(journaled, bounded, resumable)"]
  LP --> R2["result"]
```

## How it relates to the rest of the surface [#how-it-relates-to-the-rest-of-the-surface]

* **Versus durable chat.** Shared runtime, different host adapter. Chat exposes
  committed journal events over resumable SSE. A workflow maps the same outcomes to
  node completion, failure, or an approval wait in durable run state.
* **Versus agent presets.** An `ai_agent` step resolves a tenant agent preset to get
  its model, instructions, tool set, and bounds — the same presets a chat session
  references. The compile-time *preset templates* are a separate thing: they run as
  ordinary connector actions, which an `ai_action`-style step invokes directly.
* **Versus the inference proxy.** Both step types issue every LLM call through the one
  provider-agnostic proxy, so provider choice and cost guards are uniform with the rest
  of Ductor's AI surface.

## Observability [#observability]

The executors emit dedicated metric families — `ai_action_*` (request counts, token
totals and per-request distribution, duration) and `ai_agent_*` (tool-call counts,
LLM-iteration counts, bound-exceeded counts, total duration). Model, provider, and tool
labels are normalized to a closed enum so a misconfigured workflow cannot inflate
metric cardinality.

Inference usage is also written to the durable usage plane with workflow run and
step correlation. When a step is part of an experiment, pass the experiment id
and immutable exposure id as inference metadata so request, token, cost, latency,
and error events can be attributed to the exact variant. See
[Durable usage and attribution](/docs/ai/inference-proxy#durable-usage-and-attribution)
and [Experiments](/docs/management/experiments).

## Enable the durable workflow host [#enable-the-durable-workflow-host]

```yaml title="configs/ductor.yaml"
workflow_runtime:
  enabled: true

ai_inference:
  enabled: true
  providers:
    primary:
      type: gemini
      api_key_env: GEMINI_API_KEY
      models: [gemini-2.5-flash-lite]
      enabled: true
```

There is no legacy workflow-agent flag or fallback executor. When workflow inference
and connector actions are enabled, startup wires the canonical journal, model-policy,
processor, tool-policy, memory, and delegation services; missing required dependencies
fail startup rather than selecting another loop.

Specialist definitions are resolved and pinned before parent admission. Child tools
are the exact intersection of parent tools, child tools, and the declared allowlist.
Children receive only their task prompt and content-addressed lineage—not the parent
transcript or memory—and join in stable declared order even when they finish in
parallel. Undeclared children, duplicate task ids, recursive depth overflow, schema
failure, or aggregate budget escape fail before child admission.

<Callout type="warn" title="An approval is not a fresh tool proposal">
  Resume uses the invocation that was already authorized and parked. A stale sequence,
  wrong request id, different session, or different turn is rejected. The executor
  never asks the model to recreate an approval-gated mutation after the operator acts.
</Callout>

## Related [#related]

<Cards>
  <Card title="DAG Workflow Model" href="/docs/concepts/dag-workflow-model">
    How steps, dependencies, and run state fit together.
  </Card>

  <Card title="Agent Presets" href="/docs/ai/agent-presets">
    The tenant presets ai\_agent resolves, and the preset-template actions.
  </Card>

  <Card title="Inference Proxy" href="/docs/ai/inference-proxy">
    The provider-agnostic router both step types call.
  </Card>
</Cards>
