Skip to content
Docs

How to migrate from DurableAgent to WorkflowAgent

Switch your app from DurableAgent (@workflow/ai) to WorkflowAgent (@ai-sdk/workflow) by updating imports, stream output, message conversion, stop conditions, tool approval, and context.

6 min read
Last updated June 25, 2026

AI SDK 7 introduces WorkflowAgent in the @ai-sdk/workflow package, which replaces DurableAgent from the Workflow SDK.

Both run a durable agent loop within a workflow, so tool calls, agent state, and human approvals persist through process restarts and Vercel Function timeouts. WorkflowAgent moves the class into the AI SDK, tightens its types, and makes tool approval a first-class property instead of a manual hook. Most of your agent code carries over unchanged, with the differences concentrated in imports, stream output, and a few renamed options.

In this guide, you'll learn how to:

  • Install @ai-sdk/workflow and update your imports
  • Stream ModelCallStreamPart and convert it for the client
  • Convert messages at the request and persistence boundaries
  • Replace maxSteps with stopWhen, and experimental_output with output
  • Move tool approval from a hook to the needsApproval property
  • Replace experimental_context with runtimeContext and toolsContext
  • Update the WorkflowChatTransport import on the client

If you're using an AI coding agent like Claude Code or Cursor, give it this prompt, and it'll help you migrate your code from DurableAgent to WorkflowAgent:

AI Assistance

You are migrating this codebase from the Workflow SDK's DurableAgent (@workflow/ai) to AI SDK's WorkflowAgent (@ai-sdk/workflow). Both run a durable agent loop inside a workflow, so most of the agent logic carries over. The changes are concentrated in imports, stream output, message conversion, tool approval, and a few renamed options. Source of truth: Read the official migration guide in full before you touch any code, and treat it as the authoritative list of what changes and what stays the same. It documents every change with before and after diffs: https://vercel.com/kb/guide/durableagent-to-workflowagent.md How to work: - Plan first. Create a to-do list before making changes. Start by mapping every place DurableAgent is used and confirming the project meets the guide's prerequisites, then add one task per change in the guide, plus a final verification pass. Work through the list in order and keep it updated as you go. - Spawn subagents when it helps. If your tooling supports them, delegate independent work, such as migrating separate files or running the verification checks, to subagents so they run in parallel and keep each agent's context focused. - Never assume. Ask. If the codebase does not match a pattern in the guide, an option is ambiguous, or a change could affect behavior you cannot see, stop and ask the user before continuing. Do not guess at intent. - Verify before you finish. Run the checks in the guide's "Verify the migration" section. Then summarize every file you changed and flag anything you could not migrate cleanly. Getting current information: Your knowledge will not be up to date on the AI SDK or the Workflow SDK; both change quickly. If you need anything beyond the guide, search the official sites with the site: operator instead of relying on memory: - site:vercel.com <query> - site:ai-sdk.dev <query> - site:workflow-sdk.dev <query> For example: `site:ai-sdk.dev WorkflowAgent needsApproval.` Prefer these sources over general web results or your own recollection of the APIs.

Before you begin, make sure you have:

  • An existing agent built with DurableAgent from Workflow SDK
  • The ai package (v7) and workflow runtime installed

Most options keep their names and behavior.

The table below maps the APIs that change between DurableAgent and WorkflowAgent.

AreaDurableAgent (@workflow/ai)WorkflowAgent (@ai-sdk/workflow)
Package and classimport { DurableAgent } from '@workflow/ai/agent'import { WorkflowAgent } from '@ai-sdk/workflow'
Stream outputWrites UIMessageChunk to getWritable()Writes ModelCallStreamPart, converted with createModelCallToUIChunkTransform()
Step limitmaxSteps: 10stopWhen: isStepCount(10)
Structured outputexperimental_output, read from result.experimental_outputoutput, read from result.output
Tool approvalHook awaited inside executeneedsApproval on the tool definition
Server-side contextexperimental_contextruntimeContext and toolsContext
Accumulated UI messagescollectUIMessages, read from result.uiMessagesRemoved. Use result.messages and convert
Non-streaming outputgenerate()Removed. Use stream()
Client transportWorkflowChatTransport from @workflow/aiWorkflowChatTransport from @ai-sdk/workflow
Step callbackonStepFinishonStepEnd (onStepFinish is deprecated)

Install @ai-sdk/workflow, then change the import and class name.

@ai-sdk/workflow requires the ai package and zod as peer dependencies, plus the workflow package for the runtime.

Terminal
pnpm i @ai-sdk/workflow
- import { DurableAgent } from '@workflow/ai/agent';
+ import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow';
- const agent = new DurableAgent({
+ const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-4-6',
instructions: 'You are a helpful assistant.',
tools: {
/* ... */
},
});

If your project imported DurableAgent from a different path, update that import to point at @ai-sdk/workflow.

WorkflowAgent writes raw ModelCallStreamPart chunks to the writable stream, where DurableAgent wrote UIMessageChunk. Change the type on getWritable() inside your workflow, then convert the stream to UI chunks at the response boundary with createModelCallToUIChunkTransform(). This keeps the durable stream provider-shaped and avoids baking a UI protocol into the workflow payload.

// Inside the workflow
await agent.stream({
messages,
- writable: getWritable<UIMessageChunk>(),
+ writable: getWritable<ModelCallStreamPart>(),
});
// Inside the route handler
+ import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
return createUIMessageStreamResponse({
- stream: run.readable,
+ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
});

WorkflowAgent.stream() expects ModelMessage[]. When your workflow receives UIMessage[] from the client (for example, through useChat), convert them with convertToModelMessages() before calling stream().

import { convertToModelMessages, type UIMessage } from 'ai';
import { getWritable } from 'workflow';
import { type ModelCallStreamPart } from '@ai-sdk/workflow';
export async function chat(messages: UIMessage[]) {
'use workflow';
const modelMessages = await convertToModelMessages(messages);
// `agent` is your existing WorkflowAgent instance
const result = await agent.stream({
messages: modelMessages,
writable: getWritable<ModelCallStreamPart>(),
});
return { messages: result.messages };
}

result.messages is now an array of ModelMessage. Persist those, and convert them back with convertToUIMessages() when you need to render a saved conversation.

Replace the maxSteps option with a stopWhen condition. For the same step-count limit, use isStepCount() from the ai package.

+ import { isStepCount } from 'ai';
await agent.stream({
messages,
- maxSteps: 10,
+ stopWhen: isStepCount(10),
});

To let the agent run until it finishes calling tools, use isLoopFinished(). Pass an array of conditions to cap the total number of steps, stopping the loop when either condition is met.

import { isLoopFinished, isStepCount } from 'ai';
await agent.stream({
messages,
stopWhen: [isLoopFinished(), isStepCount(20)],
});

Rename experimental_output to output, import Output from @ai-sdk/workflow, and read the parsed result from result.output.

- import { DurableAgent, Output } from '@workflow/ai/agent';
+ import { Output } from '@ai-sdk/workflow';
const result = await agent.stream({
messages,
- experimental_output: Output.object({ schema }),
+ output: Output.object({ schema }),
});
- console.log(result.experimental_output);
+ console.log(result.output);

Replace hook-based approval with the needsApproval property on the tool definition. WorkflowAgent emits the approval request, suspends the workflow, and resumes automatically when the user responds, so you no longer await a hook inside execute. Because the workflow is durable, the request survives process restarts, and the user can approve hours later.

bookFlight: tool({
description: 'Book a flight',
inputSchema: z.object({ flightId: z.string() }),
+ needsApproval: true,
- execute: async (input) => {
- const approved = await waitForApprovalHook(input);
- if (!approved) throw new Error('Denied');
- return bookFlightStep(input);
- },
+ execute: bookFlightStep,
})

needsApproval also accepts an async function, so you can require approval based on the tool input.

cancelBooking: tool({
description: 'Cancel a booking',
inputSchema: z.object({ bookingId: z.string() }),
needsApproval: async (input) => input.bookingId.startsWith('VIP-'),
execute: cancelBookingStep,
}),

needsApproval is specific to WorkflowAgent. For generateText, streamText, and ToolLoopAgent, use toolApproval instead.

Replace experimental_context with runtimeContext for shared agent state and toolsContext for per-tool state. Each tool's execute receives only its own validated entry as context.

const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-4-6',
tools: { weather: weatherTool },
- experimental_context: { tenantId: 'tenant_123', apiKey: 'sk-...' },
+ runtimeContext: { tenantId: 'tenant_123' },
+ toolsContext: { weather: { apiKey: 'sk-...' } },
});

A tool reads its context from the second argument of execute. Add a contextSchema to validate the entry before the tool runs.

const weatherTool = tool({
description: 'Get the weather for a city.',
inputSchema: z.object({ city: z.string() }),
contextSchema: z.object({ apiKey: z.string() }),
execute: async ({ city }, { context }) => {
return getWeather(city, context.apiKey);
},
});

runtimeContext flows through prepareStep, the lifecycle callbacks, and onEnd. Treat it as immutable, and return a new value from prepareStep to update it for the current and later steps.

WorkflowAgent runs inside the Workflow runtime, so context values can be persisted and replayed across step boundaries. Keep runtimeContext, toolsContext, and anything returned from prepareStep serializable. Use plain data such as strings, numbers, booleans, arrays, plain objects, dates, URLs, maps, and sets. Do not put functions, class instances, symbols, database clients, or SDK clients in context. Pass identifiers or configuration instead, and recreate those resources inside step functions.

On the client, WorkflowChatTransport now comes from @ai-sdk/workflow instead of @workflow/ai. The transport options and your useChat setup stay the same.

- import { WorkflowChatTransport } from '@workflow/ai';
+ import { WorkflowChatTransport } from '@ai-sdk/workflow';

Your reconnection route should pipe the resumed stream through createModelCallToUIChunkTransform() and return the x-workflow-run-id response header, the same transform you added in step 2. See WorkflowChatTransport for the full client-server setup.

WorkflowAgent exposes only stream(). If your DurableAgent code called generate(), switch to stream() and read result.messages and result.output once the promise resolves.

collectUIMessages and result.uiMessages are also removed. Read result.messages, an array of ModelMessage, and convert it with convertToUIMessages() when you need UI messages, as shown in step 3.

const result = await agent.stream({
messages: modelMessages,
writable: getWritable<ModelCallStreamPart>(),
- collectUIMessages: true,
});
- return { uiMessages: result.uiMessages };
+ return { messages: result.messages };

The following options keep the same names and behavior, so you don't need to change them:

  • prepareStep
  • onEnd
  • onError
  • toolChoice
  • activeTools
  • timeout
  • experimental_repairToolCall

Along with the generation settings (temperature, maxOutputTokens, topP, and the rest).

WorkflowAgent also adds prepareCall, which runs once before the agent loop starts, along with the following lifecycle callbacks:

  • onStart
  • onStepStart
  • onToolExecutionStart
  • onToolExecutionEnd

If you used onStepFinish, switch to onStepEnd. onStepFinish still works as a deprecated fallback, but onStepEnd is the supported name.

After you finish, confirm the agent works end-to-end:

  • The project installs and builds with @ai-sdk/workflow in place.
  • The client renders streamed output, which confirms that the response pipes through createModelCallToUIChunkTransform().
  • Tools that run as steps still execute and retry, and approval-gated tools pause and resume on the user's response.
  • Structured output parses into result.output.
  • Saved conversations load correctly, which confirms messages convert in both directions.

Was this helpful?

supported.