A multi-tool agent must chain function calls across reasoning steps instead of returning after a single model response. This tutorial builds a vercel ai sdk multi-tool agent n4n.ai using the Vercel AI SDK’s generateText with maxSteps, wired to an OpenAI-compatible inference endpoint for model routing.
Prerequisites
- Node.js 20+ (native
fetchandprocess.loadEnvFilesupport) - An API key from n4n.ai (set as
N4N_API_KEYin.env) - Familiarity with TypeScript and Zod
- npm or pnpm
No vector store or framework magic. We use the primitives the SDK ships.
Project setup
Create a directory and install the minimal dependency set. The Vercel AI SDK core, the OpenAI-compatible provider adapter, and Zod for tool parameter validation:
mkdir multi-tool-agent && cd multi-tool-agent
npm init -y
npm install ai@^4 @ai-sdk/openai@^1 zod dotenv
Create .env:
echo "N4N_API_KEY=sk-your-key-here" > .env
We pin ai v4 because maxSteps and the tool() helper are stable there.
Configure the n4n.ai provider
n4n.ai exposes a single OpenAI-compatible endpoint covering 240+ models, so we point the SDK at it with createOpenAI. The SDK treats it like any other OpenAI-compatible backend; we just override baseURL.
// src/client.ts
import { createOpenAI } from '@ai-sdk/openai';
import dotenv from 'dotenv';
dotenv.config();
export const n4n = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY!,
});
// Pick any model the gateway exposes. Model IDs follow provider/name convention.
export const model = n4n('anthropic/claude-3.5-sonnet');
The gateway forwards provider cache-control hints and honors routing directives, but for this tutorial we let it default to the specified model.
Define tools with Zod schemas
Tools are plain objects built with tool(). Each needs a description, a Zod parameter schema, and an execute function. The model uses the description and schema to decide when to call.
We define three intentionally simple tools: weather lookup, arithmetic eval, and character counting.
// src/tools.ts
import { tool } from 'ai';
import { z } from 'zod';
export const getWeather = tool({
parameters: z.object({ city: z.string().describe('City name, e.g. Tokyo') }),
execute: async ({ city }) => {
// Mocked external call. Swap for real API + key.
const fakeDb: Record<string, { tempC: number; conditions: string }> = {
tokyo: { tempC: 21, conditions: 'clear' },
berlin: { tempC: 14, conditions: 'rain' },
};
const data = fakeDb[city.toLowerCase()] ?? { tempC: 20, conditions: 'unknown' };
return { city, ...data };
},
});
export const calc = tool({
parameters: z.object({ expression: z.string().describe('e.g. "(21*9/5)+32"') }),
execute: async ({ expression }) => {
// Demo-only eval. In production use a sandboxed math parser.
const result = Function(`"use strict"; return (${expression});`)();
return { result };
},
});
export const countChars = tool({
parameters: z.object({ text: z.string() }),
execute: async ({ text }) => ({ length: text.length }),
});
Schema quality matters more than the execution logic. The model will not call a tool if the description is ambiguous.
Build the multi-tool agent loop
The Vercel AI SDK handles the reasoning loop when you pass maxSteps > 1. On each step, it sends the conversation (including prior tool results) back to the model. When the model stops emitting tool calls, the loop ends.
// src/agent.ts
import { generateText } from 'ai';
import { model } from './client';
import { getWeather, calc, countChars } from './tools';
export async function runAgent(prompt: string) {
const { text, steps } = await generateText({
model,
tools: { getWeather, calc, countChars },
maxSteps: 5,
prompt,
onStepFinish: (step) => {
console.log(`\n--- Step ${step.stepNumber} ---`);
if (step.toolCalls.length) {
console.log('Tool calls:', JSON.stringify(step.toolCalls, null, 2));
}
if (step.toolResults.length) {
console.log('Tool results:', JSON.stringify(step.toolResults, null, 2));
}
},
});
console.log('\n=== Final response ===');
console.log(text);
return { text, steps };
}
maxSteps: 5 caps the loop. If the task needs more hops, raise it. Each step incurs a model round-trip, so keep it tight.
Run the agent and inspect output
Write an entrypoint and execute a query that forces two tool calls in sequence:
// src/main.ts
import { runAgent } from './agent';
runAgent(
'What is the temperature in Tokyo in Fahrenheit? ' +
'Use the weather tool, then convert with calc.'
);
Run with tsx (install as dev dep) or compile:
npx tsx src/main.ts
Expected console output at checkpoint (abridged):
--- Step 1 ---
Tool calls: [
{
"toolName": "getWeather",
"args": { "city": "Tokyo" }
}
]
Tool results: [
{
"toolName": "getWeather",
"result": { "city": "Tokyo", "tempC": 21, "conditions": "clear" }
}
]
--- Step 2 ---
Tool calls: [
{
"toolName": "calc",
"args": { "expression": "(21*9/5)+32" }
}
]
Tool results: [
{
"toolName": "calc",
"result": { "result": 69.8 }
}
]
=== Final response ===
The temperature in Tokyo is 21°C, which is 69.8°F.
The agent called getWeather, ingested the result, then called calc on the derived expression. That is the vercel ai sdk multi-tool agent n4n.ai loop working as intended.
Handling tool errors
Real tools fail. Wrap execute in try/catch and return a structured error so the model can recover:
execute: async ({ city }) => {
try {
const res = await fetch(`https://api.weather.example/${city}`);
if (!res.ok) throw new Error(`status ${res.status}`);
return await res.json();
} catch (err) {
return { error: `weather lookup failed: ${String(err)}` };
}
}
The model sees the error field and can retry with a corrected argument or call a different tool. Because n4n.ai provides automatic fallback when an upstream provider is rate-limited or degraded, a transient model-side 429 does not crash the loop—the gateway reroutes the request.
Streaming and UI integration
For a chat UI, swap generateText for streamText. Tool calls still arrive in steps, but you forward partial text via result.textStream. The tool execution semantics are identical; only the transport changes.
import { streamText } from 'ai';
const result = streamText({
model,
tools: { getWeather, calc, countChars },
maxSteps: 5,
prompt: 'Count characters in "hello world" then double it with calc.',
});
for await (const delta of result.textStream) {
process.stdout.write(delta);
}
Extending the agent
The pattern scales to dozens of tools. Keep these rules:
- One tool per capability. Don’t overload a single tool with branching
modeargs. - Zod schemas must be as strict as possible;
describe()every field. - Log
onStepFinishin dev. In prod, emit step traces to your observability pipe. - Set
maxStepsbased on the worst-case reasoning depth of your domain.
A vercel ai sdk multi-tool agent n4n.ai deployment is just this loop plus your tool implementations and a model ID. The gateway abstracts provider heterogeneity; the SDK abstracts the conversation mechanics.