Skip to main content

Use DBOS With the Vercel AI SDK

You can use DBOS to add durable execution to agents built with the Vercel AI SDK through the @dbos-inc/vercel-ai package.

This package makes AI SDK agents durable, backed by your Postgres database. All you have to do is wrap your model with durableCalls and your tools with durableTools and run your agents inside a DBOS workflow. Then, this integration automatically checkpoints every action your agents take in Postgres. If your process is interrupted, DBOS replays your agent from its checkpoints so it resumes from where it left off.

import { DBOS } from '@dbos-inc/dbos-sdk';
import { generateText, wrapLanguageModel } from 'ai';
import { openai } from '@ai-sdk/openai';
import { durableCalls } from '@dbos-inc/vercel-ai';

const model = wrapLanguageModel({
model: openai('gpt-5'),
middleware: durableCalls({ retriesAllowed: true, maxAttempts: 5 }),
});

const researchAgent = DBOS.registerWorkflow(
async (question: string) => {
const { text } = await generateText({
model,
prompt: question,
system: 'You are a helpful research assistant.',
});
return text;
},
{ name: 'researchAgent' },
);

DBOS.setConfig({ name: 'my-agent', systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL });
await DBOS.launch();

console.log(await researchAgent('Why did the agent cross the road?'));

Installation​

npm install @dbos-inc/vercel-ai @dbos-inc/dbos-sdk ai

Requires DBOS v4.27+ or v5, AI SDK v7+, and a Postgres database for DBOS.

Durable Model Calls​

To durably checkpoint each call you make to a model, wrap your model in durableCalls. Then, call your model or agent from a workflow:

import { DBOS } from '@dbos-inc/dbos-sdk';
import { ToolLoopAgent, wrapLanguageModel } from 'ai';
import { openai } from '@ai-sdk/openai';
import { durableCalls } from '@dbos-inc/vercel-ai';

const model = wrapLanguageModel({ model: openai('gpt-5'), middleware: durableCalls() });
const agent = new ToolLoopAgent({ model, instructions: 'You are a helpful research assistant.', tools });

const researchAgent = DBOS.registerWorkflow(
async (question: string) => {
const result = await agent.stream({ prompt: question });
for await (const delta of result.textStream) process.stdout.write(delta);
return await result.text;
},
{ name: 'researchAgent' },
);

You can parameterize durableCalls to configure model call retries and timeouts:

durableCalls({
name?: string; // step name (default: "<provider>.<modelId>.<operation>")
retriesAllowed?: boolean; // retry failed model calls (default: true)
maxAttempts?: number; // total attempts when retries are allowed (default: 3)
intervalSeconds?: number; // delay before first retry (default: 1)
backoffRate?: number; // exponential backoff multiplier (default: 2)
shouldRetry?: (error: unknown) => boolean; // default: skip provider-declared non-retryable errors and aborts
timeoutMS?: number; // per-attempt timeout
durableStream?: string; // stream each call's output to this durable stream
include?: { requestBody?: boolean; responseBody?: boolean }; // checkpoint raw provider bodies; match generateText's `include` (default: false)
});

Durable Tools​

To durably checkpoint your agents' tool calls, wrap them in durableTools:

import { tool, stepCountIs } from 'ai';
import { durableTools } from '@dbos-inc/vercel-ai';
import { z } from 'zod';

const tools = durableTools({
getWeather: tool({
description: 'Get the weather for a city',
inputSchema: z.object({ city: z.string() }),
execute: ({ city }) => fetchWeather(city),
}),
});

const agent = DBOS.registerWorkflow(async (question: string) => {
const result = await generateText({ model, prompt: question, tools, stopWhen: stepCountIs(10) });
return result.text;
}, { name: 'weatherAgent' });

You can pass step configuration (such as timeouts or retries) to durableTools. You can set defaults for all tools or configure tools individually. Retries are off by default.

const tools = durableTools(myTools, {
timeoutMS: 30_000,
tools: {
getWeather: { retriesAllowed: true, maxAttempts: 3 },
},
});

When using durable tools, to ensure the ordering of parallel tool calls is consistent during recovery, do not await I/O in callbacks that run before a tool executes, such as onToolExecutionStart.

Durable Streams​

You can durably stream agent or model output so it can be read by an external client or UI. To do this, configure durableCalls or durableTools/durableMCPTools with a durable stream name:

import { createUIMessageStreamResponse, streamText } from 'ai';
import { durableCalls, durableTools, readDurableStream } from '@dbos-inc/vercel-ai';

const model = wrapLanguageModel({ model: openai('gpt-5'), middleware: durableCalls({ durableStream: 'ui' }) });
const tools = durableTools(myTools, { durableStream: 'ui' });

const chatTurn = DBOS.registerWorkflow(async (messages: ModelMessage[]) => {
const result = streamText({ model, messages, tools, stopWhen: stepCountIs(10) });
return await result.text;
}, { name: 'chatTurn' });

export async function POST(req: Request) {
const { messages, messageId } = (await req.json()) as { messages: ModelMessage[]; messageId: string };
const handle = await DBOS.startWorkflow(chatTurn)(messages);
return createUIMessageStreamResponse({
stream: readDurableStream({ workflowID: handle.workflowID, key: 'ui', messageId }),
});
}

You can read from a durable stream using readDurableStream, for example to stream it to a UI. It emits a stream of AI SDK UIMessageChunk. You can also pass a DBOSClient into readDurableStream to read it from a different process.

You can write your own data to a stream with writeDurableStream(key, chunks). Tools can also write chunks with toolWriter(), which returns a UIMessageStreamWriter bound to the current tool call. Chunks from a tool call are written when the tool call succeeds (transient: true data parts are written live instead). To also include them in the response message your workflow builds, pass your createUIMessageStream writer to durableTools:

import { consumeStream, createUIMessageStream, streamText } from 'ai';
import { durableTools, toolWriter } from '@dbos-inc/vercel-ai';

const search = tool({
inputSchema: z.object({ q: z.string() }),
execute: async ({ q }) => {
const writer = toolWriter();
writer.write({ type: 'data-progress', data: { pct: 50 }, transient: true });
writer.write({ type: 'source-url', sourceId: 's1', url: 'https://example.com' });
return runSearch(q);
},
});

const chatTurn = DBOS.registerWorkflow(async (messages: ModelMessage[]) => {
const stream = createUIMessageStream({
execute: ({ writer }) => {
const tools = durableTools({ search }, { durableStream: 'ui', writer });
writer.merge(streamText({ model, messages, tools, stopWhen: stepCountIs(10) }).toUIMessageStream());
},
onFinish: ({ responseMessage }) => saveMessage(responseMessage),
});
await consumeStream({ stream });
}, { name: 'chatTurn' });

Your streams are closed when your workflow finishes; you can also close a stream early using closeDurableStream.

If a workflow is interrupted during a model call, when the workflow recovers, it restarts the model call and streams its output again. Readers that connect afterwards see the model's output once; live readers, including readers resumed from an offset, receive a transient data-dbos-superseded chunk indicating the model call has been restarted. Likewise, if a tool call is re-executed and writes different chunks, readers that connect afterwards see only the re-execution's chunks; live readers receive a transient data-dbos-tool-superseded chunk naming the toolCallId whose earlier chunks to discard.

Durable MCP Tools​

durableMCPTools wraps an MCP client (for example, from @ai-sdk/mcp) so both the tool listing and every tool call run as durable steps:

import { createMCPClient } from '@ai-sdk/mcp';
import { durableMCPTools } from '@dbos-inc/vercel-ai';

const agent = DBOS.registerWorkflow(async (question: string) => {
const mcpClient = await createMCPClient({ transport: { type: 'http', url: MCP_URL } });
try {
const tools = await durableMCPTools(mcpClient);
const result = await generateText({ model, prompt: question, tools, stopWhen: stepCountIs(10) });
return result.text;
} finally {
await mcpClient.close();
}
}, { name: 'mcpAgent' });

To use the client's explicit-schema mode (tool subsetting, typed inputs, output schemas), pass toolOptions; it is forwarded to client.tools() for both the listing and each tool call:

const tools = await durableMCPTools(mcpClient, {
toolOptions: { schemas: { 'get-weather': { inputSchema: z.object({ city: z.string() }) } } },
});

Durable Subagents​

You can delegate complex tasks to subagents, which act as tools for their "parent" agent. To create a durable subagent, wrap your agent in agentTool, then pass it into durableTools just like any other tool:

import { ToolLoopAgent } from 'ai';
import { agentTool, durableTools } from '@dbos-inc/vercel-ai';

const researcher = new ToolLoopAgent({ model, instructions: 'Research thoroughly.', tools: researchTools });

const research = agentTool({
name: 'research', // subagent name
description: 'Research a question in depth',
inputSchema: z.object({ question: z.string() }),
agent: researcher,
prompt: ({ question }) => question, // tool input → prompt (or ModelMessage[])
});

const tools = durableTools({ research, getWeather }, { durableStream: 'ui' });
const orchestrator = new ToolLoopAgent({ model, tools });

Internally, subagents are implemented as child workflows of the parent agent workflow, so each call has its own checkpoints and parallel calls are safe. Call agentTool before DBOS.launch(), since it registers that workflow. By default, the tool returns the subagent's final text; you can configure this with the output parameter.

Durable Embedding Models​

durableEmbeddingCalls enables durable calls to embedding models:

import { embedMany, wrapEmbeddingModel } from 'ai';
import { durableEmbeddingCalls } from '@dbos-inc/vercel-ai';

const embeddingModel = wrapEmbeddingModel({
model: openai.textEmbeddingModel('text-embedding-3-small'),
middleware: durableEmbeddingCalls({ retriesAllowed: true }),
});

const embedChunks = DBOS.registerWorkflow(async (chunks: string[]) => {
const { embeddings } = await embedMany({ model: embeddingModel, values: chunks });
return embeddings;
}, { name: 'embedChunks' });

Durable Image Models​

durableImageCalls makes image generation durable:

import { generateImage, wrapImageModel } from 'ai';
import { durableImageCalls } from '@dbos-inc/vercel-ai';

const imageModel = wrapImageModel({ model: openai.imageModel('gpt-image-1'), middleware: durableImageCalls() });

const drawImage = DBOS.registerWorkflow(async (prompt: string) => {
const { images } = await generateImage({ model: imageModel, prompt });
return images.map((image) => image.base64);
}, { name: 'drawImage' });

Learn More​

For more details on building agents with the Vercel AI SDK, see the Vercel AI SDK documentation. For information about durable execution and workflow design, see the DBOS programming guide.