AI agents

Build AI agents that survive a crash - step.ai turns model calls, tool calls, and agent loops into durable steps, plus the classic agent patterns.

A model call is slow, expensive, and can fail halfway. Wrapping it in a durable step turns it into a checkpoint: the call happens once, its result is saved, and a crash or retry resumes from after it rather than paying for it again. That is all step.ai is - the step API for model calls, embeddings, and agent loops. See AI steps for the full reference, or the AI quickstart to run your first one end to end.

The building block

The unit is a single augmented model call - an LLM with tools and structured output. In Duraton that is step.ai.generate:

const { output } = await ctx.step.ai.generate<Triage>("classify", {
  model: "claude-opus-4-8",
  prompt: `Classify this ticket: ${subject}`,
  output: triageSchema,
});

The call is memoized under "classify", so on replay the workflow skips it and reuses the saved answer. Composing calls means composing checkpoints:

export const triageTicket = defineWorkflow<{ subject: string }>({
  name: "ticket.created",
  handler: async (ctx) => {
    const { output } = await ctx.step.ai.generate<{ category: string; priority: string }>("classify", {
      model: "claude-opus-4-8",
      prompt: `Classify this ticket: ${ctx.event.data.subject}`,
      output: {
        type: "object",
        properties: { category: { type: "string" }, priority: { type: "string" } },
        required: ["category", "priority"],
      },
    });

    // A crash here replays the handler from the top: "classify" returns its recorded
    // answer without calling the model, and only "draft-reply" is actually charged.
    const { text } = await ctx.step.ai.generate("draft-reply", {
      model: "claude-opus-4-8",
      prompt: `Write a reply for a ${output?.priority} ${output?.category} ticket.`,
    });

    return { category: output?.category, reply: text };
  },
});

The agent loop

When the model should decide its own next step - call a tool, look at the result, call another - use step.ai.loop. Each turn is a durable step and each tool call is a durable step, so a long-running agent survives a restart and resumes at the last committed turn instead of starting over.

const agent = await ctx.step.ai.loop("agent", {
  prompt: `Resolve the ticket about: ${subject}`,
  maxIterations: 6,
  tools: {
    "search-kb": { handler: (q) => searchKb(q) },
    "lookup-order": { workflow: "orders.lookup", app: "orders" },
  },
  turn: (ctx, i) => callModel(ctx.prompt, ctx.history, i),
});

A tool can be a local handler or another workflow. A workflow tool becomes a linked child run - a full durable run with its own retries and steps - so the agent can delegate real work, not just call a function. That linkage is what makes the orchestrator pattern below durable end to end.

Agent patterns

Anthropic's Building effective agents distinguishes composable workflows (the model follows a fixed structure) from agents (the model directs itself). Each maps onto Duraton primitives, and because every model call is a step, each becomes crash-safe for free.

The pattern names and definitions below are Anthropic's; the mapping to step.ai is the durable implementation.

Prompt chaining

Decompose a task into a fixed sequence of calls, each consuming the previous one's output. Every call is its own step, so a failure in the third link resumes from the saved output of the second.

const outline = await ctx.step.ai.generate("outline", { model, prompt: `Outline: ${topic}` });
const draft = await ctx.step.ai.generate("draft", { model, prompt: `Expand:\n${outline.text}` });
const final = await ctx.step.ai.generate("polish", { model, prompt: `Tighten:\n${draft.text}` });

Routing

Classify the input, then send it to a specialized follow-up. Classify with a structured generate, then branch to the workflow that handles that class:

const { output } = await ctx.step.ai.generate<{ queue: string }>("route", {
  model,
  prompt: `Which queue handles: ${subject}`,
  output: { type: "object", properties: { queue: { type: "string" } }, required: ["queue"] },
});
const result = await ctx.step.runWorkflow("handle", { name: `support.${output?.queue}` });

Parallelization

Run independent calls at once and aggregate the results (sectioning), or run the same call several times and combine the answers (voting). Fan out with Promise.all - each branch is its own durable step and the run joins when all have committed:

const [safety, topic, sentiment] = await Promise.all([
  ctx.step.ai.generate("safety", { model, prompt: `Unsafe? ${text}` }),
  ctx.step.ai.generate("topic", { model, prompt: `Topic of: ${text}` }),
  ctx.step.ai.generate("sentiment", { model, prompt: `Sentiment of: ${text}` }),
]);

Orchestrator-workers

A central model breaks a task down, delegates to workers, and synthesizes the results. This is step.ai.loop with workflow tools: the loop is the orchestrator, each workflow tool is a worker, and every delegation is a linked child run that retries and checkpoints on its own.

await ctx.step.ai.loop("orchestrate", {
  prompt: `Ship the change described in: ${brief}`,
  maxIterations: 12,
  tools: {
    "search-code": { workflow: "repo.search" },
    "open-pr": { workflow: "repo.open-pr", app: "ci" },
  },
  turn: (ctx, i) => planNextStep(ctx.prompt, ctx.history, i),
});

Evaluator-optimizer

One call generates, another evaluates and feeds back, in a loop until the check passes. For a single output against a schema, generate's built-in durable re-ask is this pattern - validate is the evaluator and each re-ask is a durable step:

const { output } = await ctx.step.ai.generate("summary", {
  model,
  prompt: `Summarize in under 40 words: ${doc}`,
  output: summarySchema,
  validate: (v) => (wordCount(v) <= 40 ? undefined : "too long, tighten it"),
  reask: 3,
});

For a free-form generate/critique cycle, run the two calls as steps inside step.ai.loop and stop when the evaluator is satisfied.

Keeping it replay-safe

The model results are memoized, but the code around them - your turn, validate, and stop functions, and any branching on a result - runs again on every replay. Keep them deterministic (a pure function of their inputs) so a resumed run takes the same path it took the first time. The durable-execution rules for regular steps apply unchanged.

On this page