Skip to content

What is WorkflowAgent?

Build durable AI agents with WorkflowAgent: tool calls that retry as workflow steps, suspend for human approval, and resume after timeouts or page refreshes.

6 min read
Last updated May 21, 2026

You can build AI agents that survive process restarts, function timeouts, and long pauses for human approval by combining the AI SDK with Workflow SDK. WorkflowAgent from @ai-sdk/workflow runs the same agent loop as ToolLoopAgent, but each tool call is marked with 'use step', thus becoming a durable step inside a workflow. This allows progress to be persisted, a failed step retries from the last checkpoint, and a tool marked needsApproval: true can suspend the agent for hours or days until a user responds, without a custom state store or polling.

This guide will walk you through what WorkflowAgent adds on top of ToolLoopAgent, how to define tools that suspend for approval with needsApproval, and how WorkflowChatTransport keeps chat streams resumable across function timeouts and page refreshes. You'll also see how to migrate an existing DurableAgent to WorkflowAgent, and which constructor options carry over unchanged.

A standard ToolLoopAgent runs entirely in memory. If the process crashes, the function times out, or the user refreshes the page, the agent's progress is lost. For short, single-tool interactions, that's usually fine. For production agents that chain multiple tool calls (e.g., booking a flight, processing a refund, running a research task across several APIs, etc.), losing state mid-loop is expensive.

WorkflowAgent addresses four specific gaps:

  • Statefulness. Agent state persists across process boundaries, so a multi-step loop doesn't have to fit inside a single function invocation.
  • Resumability. Each tool call is a discrete workflow step. If a step fails, it retries automatically (default: 3 attempts) instead of restarting the whole agent.
  • Human-in-the-loop. Tools marked with needsApproval pause the agent until the user responds. Because the workflow is durable, the user can approve hours or days later, and the agent picks up where it left off.
  • Observability. Every tool call appears as a discrete step in the workflow dashboard, with inputs, outputs, retries, and timing.

These come from running inside the Workflow SDK runtime, not from the agent itself.

Both classes implement the same agent loop and accept the same generation settings (temperature, maxOutputTokens, topP, and so on). The differences are about where and how the loop runs.

ToolLoopAgentWorkflowAgent
Packageai@ai-sdk/workflow
RuntimeIn-memoryWorkflow runtime
DurabilityLost on crashSurvives restarts
Tool retriesManualAutomatic, per step
Human approvaltoolApproval optionneedsApproval on the tool, survives suspension
generate()AvailableNot available
stream()Returns a stream you consumeWrites to a writable provided by the workflow
Stream output typestreamText return valueModelCallStreamPart chunks

The rule of thumb: reach for ToolLoopAgent first. Move to WorkflowAgent when a tool call needs to outlive its request, an approval might take longer than a function timeout, or you want each tool call traced as a separate retryable unit.

The constructor takes the same shape as ToolLoopAgent (model, instructions, tools) plus workflow-specific options.

To get durability, the agent has to run inside a function marked with 'use workflow', and tool execute functions are marked with 'use step':

workflow/agent-chat.ts
import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow';
import { convertToModelMessages, tool, type UIMessage } from 'ai';
import { getWritable } from 'workflow';
import { z } from 'zod';
async function searchFlightsStep(input: {
origin: string;
destination: string;
date: string;
}) {
'use step';
const response = await fetch(`https://api.flights.example/search?...`);
return response.json();
}
export async function chat(messages: UIMessage[]) {
'use workflow';
const modelMessages = await convertToModelMessages(messages);
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-4-6',
instructions: 'You are a flight booking assistant.',
tools: {
searchFlights: tool({
description: 'Search for available flights',
inputSchema: z.object({
origin: z.string(),
destination: z.string(),
date: z.string(),
}),
execute: searchFlightsStep,
}),
},
});
const result = await agent.stream({
messages: modelMessages,
writable: getWritable<ModelCallStreamPart>(),
});
return { messages: result.messages };
}

Two things to note.

First, WorkflowAgent.stream() expects ModelMessage[], so messages from useChat need to be converted with convertToModelMessages. Second, the agent doesn't return a stream you read from. It writes ModelCallStreamPart chunks to the workflow's writable, and the route handler converts those to UI chunks at the edge with createModelCallToUIChunkTransform(). This keeps the durable stream in a provider-shaped format and leaves the UI protocol at the boundary.

Tool execute functions don't have to use 'use step', but without it, they run as regular in-memory functions with no durability guarantees. The 'use step' directive is what gives a tool call automatic retries, persistence, and its own line in the workflow dashboard.

Approval is a first-class property on the tool definition in WorkflowAgent. When a tool with needsApproval is called, the agent emits an approval request to the writable stream and the workflow suspends. The user can respond seconds or hours later, and the durable workflow holds the state until they do.

workflow/agent-chat.ts
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-4-6',
tools: {
bookFlight: tool({
description: 'Book a flight',
inputSchema: z.object({
flightId: z.string(),
passengerName: z.string(),
}),
needsApproval: true,
execute: bookFlightStep,
}),
cancelBooking: tool({
description: 'Cancel a booking',
inputSchema: z.object({ bookingId: z.string() }),
needsApproval: async (input) => input.bookingId.startsWith('VIP-'),
execute: cancelBookingStep,
}),
},
});

needsApproval accepts a boolean for blanket approval or an async function for per-input decisions. This is specific to WorkflowAgent. For generateText, streamText, and ToolLoopAgent, the equivalent feature is the toolApproval option.

Workflow functions can hit timeouts or be interrupted by network failures. WorkflowChatTransport is a ChatTransport implementation for useChat that handles those interruptions automatically. It posts messages to your chat endpoint, reads the x-workflow-run-id response header, and if the stream closes without a finish event, reconnects to {api}/{runId}/stream to resume from the last received chunk.

app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { WorkflowChatTransport } from '@ai-sdk/workflow';
import { useMemo } from 'react';
export default function Chat() {
const transport = useMemo(
() =>
new WorkflowChatTransport({
api: '/api/chat',
maxConsecutiveErrors: 5,
initialStartIndex: -50, // On page refresh, fetch last 50 chunks
}),
[],
);
const { messages, sendMessage } = useChat({ transport });
// ...render chat UI
}

The transport requires two server endpoints: a POST handler at api that returns an x-workflow-run-id header, and a GET handler at {api}/{runId}/stream that accepts a startIndex query parameter. A negative initialStartIndex (like -50) is useful for page refreshes, since the client reconnects without replaying the whole conversation. The server is expected to return x-workflow-stream-tail-index so subsequent retries can compute their position. If that header is missing, the transport falls back to replaying from the start.

WorkflowAgent replaces the Workflow SDK's DurableAgent. The core idea is the same; but the class lives in the AI SDK now, types are tighter, and tool approval is first-class. The main changes when migrating:

  • Import path. DurableAgent came from the Workflow SDK. WorkflowAgent and its helpers come from @ai-sdk/workflow, the package you install alongside workflow.
  • Stream payload. DurableAgent wrote UI message chunks directly to the workflow's writable. WorkflowAgent writes lower-level model stream parts and converts them to UI chunks at the response boundary with createModelCallToUIChunkTransform().
  • Stop conditions. The maxSteps option is replaced by the AI SDK's shared stop conditions, such as isStepCount(10).
  • Structured output. The experimental_output option is now simply output, and the result reads from the matching property.
  • Approval. Tool approval is configured with the needsApproval property on the tool itself, not a Hook call inside the execute function.
  • UI messages. The collectUIMessages flag is gone, and the stream result now returns model messages instead. Convert them at the edge with convertToUIMessages if your client needs UI messages.
  • No generate() method. WorkflowAgent only exposes stream(). Read the messages and output once the promise resolves.
  • Context split. The old experimental_context is replaced by two separate options: runtimeContext for shared agent state, and toolsContext for per-tool context that each tool receives as its context argument.

Other options carry over with the same names: prepareStep, onStepFinish, onFinish, onError, toolChoice, activeTools, timeout, and the standard generation settings.

One thing to keep in mind when working with runtimeContext and toolsContext. Both can cross workflow and step boundaries, so they need to be serializable. Strings, numbers, arrays, plain objects, dates, and other Workflow-supported structured data are safe. Functions, class instances, database clients, and SDK clients are not. Pass identifiers or configuration data instead, and recreate non-serializable resources inside step functions.

A durable agent is one whose state persists across process boundaries. A standard in-memory agent loses everything if the process crashes, the function times out, or the connection drops. A durable agent stores its progress so the same loop can pause, suspend, retry, or resume later without restarting from the beginning. In the AI SDK, you build durable agents with WorkflowAgent, which runs the agent loop inside a workflow so each tool call becomes a persisted step with automatic retries, observability, and the ability to suspend for human approval that survives restarts.

Both run the same agent loop and accept the same generation settings, but they differ in where the loop runs. ToolLoopAgent from the ai package runs entirely in memory, so progress is lost if the process crashes or the function times out. By contrast, WorkflowAgent from @ai-sdk/workflow runs inside a workflow, where each tool call marked with 'use step' becomes a durable step that persists across process boundaries and retries automatically on failure. Reach for ToolLoopAgent first, and switch to WorkflowAgent when a tool call needs to outlive its request, an approval might take longer than a function timeout, or you want each tool call traced as its own retryable unit.

Yes. WorkflowAgent replaces DurableAgent and is where new development happens. The core idea is the same (a durable agent loop that runs inside a workflow), but the class now lives in the AI SDK rather than the Workflow DevKit, types are tighter, and tool approval is a first-class property (needsApproval) instead of a Hook call. See the migration section above for the full list of changes, including the new package, the switch to ModelCallStreamPart on the writable, and the replacement of experimental_context with runtimeContext and toolsContext.

Add needsApproval: true to any tool that needs approval before it runs. When the agent tries to call that tool, it emits an approval request to the writable stream and the workflow suspends. The user can respond minutes, hours, or days later, and because the workflow is durable, the agent picks up exactly where it left off when the response arrives. You can also pass an async function to needsApproval to decide per-input whether approval is required, which is useful for things like requiring approval only for high-value or sensitive operations.

Use WorkflowChatTransport from @ai-sdk/workflow as the transport for useChat. It posts your messages to your chat endpoint, reads the x-workflow-run-id response header, and if the stream closes without a finish event (because of a timeout, network error, or refresh), it reconnects to {api}/{runId}/stream and resumes from the last received chunk. Set initialStartIndex to a negative number like -50 to fetch only the most recent chunks on reconnection, which avoids replaying the full conversation after a refresh.

Was this helpful?

supported.