Durable agent with tools using the AI SDK by Vercel
Last updated Aug 13, 2026
In this example, we show you how to build a durable agent using the AI SDK by Vercel. The agent calls tools backed by Temporal Activities to answer user questions, and it can determine which tools to use based on the input it receives.
This recipe highlights key implementation patterns:
- AI SDK client integration: The Workflow uses
generateTextfromaiandtemporalProviderfrom@temporalio/ai-sdk. This automatically wraps the LLM invocation as an Activity, so it's retried and tracked like any other durable step.temporalProvideris configured forgpt-4o-minihere, but you can point it at any model the AI SDK supports. - Tools-as-Activities:
proxyActivitieswires thegetWeatherandcalculateAreaOfCircleActivities into the Workflow sotoolsAgentcan offer tool schemas to the model, wait for results durably, and retry a tool call if it fails.
Unlike some other Temporal AI integrations — for example, the OpenAI Agents SDK's activity_as_tool helper, which generates a tool schema from a Python function's type hints — the Vercel AI SDK's tool() has no equivalent auto-generation from a TypeScript function signature. Each tool's inputSchema is written by hand as a Zod schema.
Create the Activity
Temporal Activities provide the tools that toolsAgent can call. getWeather demonstrates an Activity that wraps an unreliable external call: it geocodes the city name (via the free Open-Meteo geocoding API) and then queries the National Weather Service API, both of which are free and require no API key. Because the NWS API only covers the United States, use a US city in your prompt. calculateAreaOfCircle shows the opposite case — a tool that runs entirely locally with no external I/O.
File: src/activities.ts
const USER_AGENT = '(temporal-ai-cookbook, cookbook@temporal.io)';
async function geocode(location: string): Promise<{ name: string; latitude: number; longitude: number }> {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1&countryCode=US`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Geocoding request failed for "${location}": ${response.status}`);
}
const data = (await response.json()) as {
results?: Array<{ name: string; latitude: number; longitude: number }>;
};
const match = data.results?.[0];
if (!match) {
throw new Error(`No location found for "${location}". The National Weather Service only covers the US.`);
}
return match;
}
// The National Weather Service API only covers the US and requires a two-step
// lookup: resolve coordinates to a forecast grid, then fetch that grid's forecast.
export async function getWeather(input: {
location: string;
}): Promise<{ city: string; temperatureRange: string; conditions: string }> {
const { name, latitude, longitude } = await geocode(input.location);
const pointsResponse = await fetch(`https://api.weather.gov/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`, {
headers: { 'User-Agent': USER_AGENT },
});
if (!pointsResponse.ok) {
throw new Error(`National Weather Service points lookup failed: ${pointsResponse.status}`);
}
const points = (await pointsResponse.json()) as { properties: { forecast: string } };
const forecastResponse = await fetch(points.properties.forecast, { headers: { 'User-Agent': USER_AGENT } });
if (!forecastResponse.ok) {
throw new Error(`National Weather Service forecast lookup failed: ${forecastResponse.status}`);
}
const forecast = (await forecastResponse.json()) as {
properties: { periods: Array<{ temperature: number; temperatureUnit: string; shortForecast: string }> };
};
const current = forecast.properties.periods[0];
return {
city: name,
temperatureRange: `${current.temperature}${current.temperatureUnit}`,
conditions: current.shortForecast,
};
}
export async function calculateAreaOfCircle(input: { radius: number }): Promise<{ area: number }> {
return { area: Math.PI * input.radius * input.radius };
}
Create the Workflow
The Workflow registers both Activities as tools with a Zod schema so the model can call them when appropriate.
File: src/workflows.ts
import '@temporalio/ai-sdk/lib/load-polyfills';
import type * as activities from './activities';
import { generateText, stepCountIs, tool } from 'ai';
import { temporalProvider } from '@temporalio/ai-sdk';
import { proxyActivities } from '@temporalio/workflow';
import z from 'zod';
const { getWeather, calculateAreaOfCircle } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
retry: {
maximumAttempts: 3,
},
});
export async function toolsAgent(question: string): Promise<string> {
const result = await generateText({
model: temporalProvider.languageModel('gpt-4o-mini'),
prompt: question,
system: 'You are a helpful agent.',
tools: {
getWeather: tool({
description: 'Get the weather for a given city',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: getWeather,
}),
calculateCircleArea: tool({
description: 'Calculate the area of a circle',
inputSchema: z.object({
radius: z.number().describe('The radius of the circle'),
}),
execute: calculateAreaOfCircle,
}),
},
stopWhen: stepCountIs(5),
});
return result.text;
}
Create the Worker
Create the process for executing Activities and Workflows. The Worker uses AiSdkPlugin to configure the OpenAI provider and to keep Workflow code isolated from the Activity environment.
File: src/worker.ts
import { NativeConnection, Worker } from '@temporalio/worker';
import * as activities from './activities';
import { AiSdkPlugin } from '@temporalio/ai-sdk';
import { openai } from '@ai-sdk/openai';
async function run() {
const connection = await NativeConnection.connect({ address: 'localhost:7233' });
const worker = await Worker.create({
plugins: [new AiSdkPlugin({ modelProvider: openai })],
connection,
namespace: 'default',
taskQueue: 'ai-sdk',
workflowsPath: require.resolve('./workflows'),
activities,
});
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Create the Workflow Starter
The starter (src/client.ts) takes the question to ask as a command-line argument, spins up a Temporal client, and starts toolsAgent with a new Workflow Id.
File: src/client.ts
import { Connection, Client } from '@temporalio/client';
import { loadClientConnectConfig } from '@temporalio/envconfig';
import { toolsAgent } from './workflows';
import { nanoid } from 'nanoid';
async function run() {
const question = process.argv.slice(2).join(' ') || 'What is the weather in Seattle right now?';
const config = loadClientConnectConfig();
const connection = await Connection.connect(config.connectionOptions);
const client = new Client({ connection });
const handle = await client.workflow.start(toolsAgent, {
taskQueue: 'ai-sdk',
args: [question],
workflowId: 'workflow-' + nanoid(),
});
console.log(`Started workflow ${handle.workflowId}`);
console.log(await handle.result());
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Running
Start the Temporal Dev Server:
temporal server start-dev
Install all dependencies:
npm install
Set your API Key based on your preferred model provider:
export OPENAI_API_KEY=<KEY>
Run the worker:
npm run start.watch
Start execution with the default question, or supply your own:
npm run workflow
npm run workflow "What is the weather in Chicago?"
npm run workflow "Calculate the area of a circle with radius 5"
Example interactions
Try asking the agent questions like:
- "What is the weather in Seattle right now?"
- "Calculate the area of a circle with radius 5"
- "What is the weather in Chicago and calculate the area of a circle with radius 3"
The agent decides which tools to use. Open the Temporal UI to see the invokeModel, getWeather, and calculateAreaOfCircle Activities recorded in the Event History.