Tool Calling Patterns
Tool calling lets models invoke functions you define — query APIs, run code, update tickets — instead of guessing answers. The hard part is schema design and safe execution, not the API call.
Last reviewed: June 2026
Tool APIs differ between OpenAI, Anthropic, and the Vercel AI SDK. Pin versions and verify provider docs.
The problem
Without tools, models hallucinate database rows and API responses. With tools, you execute code — the model only proposes structured calls. Production quality depends on:
- Small, well-named tools with strict schemas
- Validation before side effects
- Clear error messages fed back to the model
- Limits on call count and parallelism
Basics: LLM APIs and Tool Calling. MCP variant: Building MCP Servers.
Tool schema design
import { z } from "zod";
export const lookupOrderTool = {
description: "Look up order status by order ID. Read-only.",
parameters: z.object({
orderId: z.string().uuid().describe("UUID of the order"),
}),
execute: async ({ orderId }: { orderId: string }) => {
const order = await db.orders.findById(orderId);
if (!order) return { error: "Order not found" };
return { status: order.status, updatedAt: order.updatedAt };
},
};
| Rule | Why |
|---|---|
| One action per tool | Model picks correctly |
.describe() on every field | Reduces wrong arg types |
| Return errors as JSON | Model can retry or explain |
| Read-only default | Writes need separate tools + gates |
AI SDK tool loop
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";
const result = streamText({
model: openai("gpt-4o"),
tools: {
lookup_order: tool({
description: "Look up order status by UUID",
parameters: z.object({ orderId: z.string().uuid() }),
execute: async ({ orderId }) => {
const order = await db.orders.findById(orderId);
return order ?? { error: "Not found" };
},
}),
},
maxSteps: 5,
messages,
});
maxSteps caps agent loops — prevents runaway tool chains.
Parallel vs sequential tools
| Pattern | Use when |
|---|---|
| Parallel | Independent lookups (user + orders + prefs) |
| Sequential | Output of A required for input of B |
| Single batch | Provider supports parallel tool calls in one turn |
Return partial results on failure — do not fail entire batch silently.
Human confirmation pattern
For destructive tools:
execute: async ({ orderId, confirm }) => {
if (!confirm) {
return {
status: "needs_confirmation",
message: `Call again with confirm:true to cancel order ${orderId}`,
};
}
await db.orders.cancel(orderId);
return { status: "cancelled" };
},
Require explicit confirm: true in schema — model must ask user first.
Error handling loop
Model calls tool → execute throws or returns error object
→ append tool result to messages
→ model explains or retries with corrected args
→ maxSteps prevents infinite retry
Log tool name, latency, and error class — not full PII payloads.
Tool calling vs MCP
| In-app tools (API route) | MCP (IDE agent) | |
|---|---|---|
| Host | Your Next.js server | Cursor, Claude Code |
| Use | User-facing product features | Developer workflows |
| Auth | User session | Developer env vars |
Same design principles apply to both.
Production concerns
| Concern | What to do |
|---|---|
| Cost | Limit maxSteps; cheap model for routing |
| Latency | Parallel reads; cache hot lookups |
| Failure modes | Timeouts on execute; circuit breakers on external APIs |
| Security | Allowlist tools per route; never eval model output |
| Injection | User content in user role; tools never run raw SQL from model strings |
See Security and Prompt Injection and Structured Outputs.