Structured Outputs & Tool Calling
A chatbot that writes paragraphs is a demo. An app needs the model to hand back data your code can trust, and to actually do things: look up an order, file a ticket, search your docs. That's structured outputs Structured Outputs Forcing a model to answer in an exact shape, usually JSON that matches a schema you define, so your code can use the result without fragile parsing. It's generally available in the Claude API via `output_config.format` (plus `"strict": true` on tools); in the AI SDK you use `Output.object({ schema })`. "Like handing someone a form with labeled boxes instead of a blank page. You always know where the answer goes." Tool Use (Function Calling) Giving a model a list of functions it may call, each with a name, description, and input schema. The model replies with "call getWeather with city=Paris", your code runs it and sends back the result, and the model continues. This is how chatbots check databases, send emails, or browse. "Like a manager who can't leave the office but can phone the right department and ask for exactly what they need." Agent Loop The core cycle behind every AI agent: the model decides on an action, calls a tool, reads the result, and repeats until the task is done or a stop condition hits. Claude Code runs this loop for you; in the AI SDK, `ToolLoopAgent` with `stopWhen: isStepCount(10)` runs it with a safety cap. "Like a detective's routine: follow a lead, check what it turned up, decide the next lead, until the case is closed."
1Three ideas, one sentence each
Structured output
You give the model a schema Schema The structure of your database — what tables exist, what columns they have, and how they relate to each other. "Like the blueprint of a building. It defines the shape before you add the furniture (data)."
Tool calling
You describe functions; the model asks to call one with specific arguments; your code runs it and returns the result.
Agent loop
Call the model, run any tools it asked for, feed results back, repeat until it answers or hits a stop condition.
2Mental model: a form, a phone list, and a timer
Structured output is a form instead of an essay. Asking "tell me about this email" gets you prose. Handing over a form with a dropdown for category, a 1 to 5 box for urgency, and one line for summary gets you something you can put in a database.
Tools are an approved phone list for a new intern. The intern (the model) can't touch your systems. It can only say "please call lookupOrders with this email." You decide which numbers are on the list and what each call is allowed to do.
The stop condition is a kitchen timer. Without one, a confused intern can keep dialing forever, and every call costs tokens Tokens The units AI uses to process text. Roughly 1 token = 4 characters. You pay per token, and context windows are measured in tokens. "Like words on a meter. The more you write (or the AI writes), the more tokens tick by."
3Structured output with a zod schema
Terminal
npm install ai zod
# Only if you also call the Claude API directly:
npm install @anthropic-ai/sdkIn AI SDK 7 you use the normal generateText call and add an output. The result lands in result.output, fully typed from your schema. (If a tutorial tells you to use generateObject, it's out of date: that's been deprecated since v6.)
lib/triage.ts
import { generateText, Output } from 'ai';
import { z } from 'zod';
const ticketSchema = z.object({
category: z.enum(['billing', 'bug', 'feature-request', 'other']),
urgency: z.number().int().min(1).max(5).describe('5 = on fire'),
summary: z.string().describe('One sentence, plain English'),
customerEmail: z.string().email().nullable(),
});
const emailBody = 'Hi, I was charged twice this month...';
const { output } = await generateText({
model: 'anthropic/claude-haiku-4.5',
output: Output.object({ schema: ticketSchema }),
prompt: `Triage this support email:\n\n${emailBody}`,
});
// output is fully typed: output.category is 'billing' | 'bug' | ...
if (output.urgency >= 4) {
console.log('Page someone:', output.summary);
}.describe() strings are sent to the model, so use them as mini-instructions. Two other shapes save you writing wrapper objects:
lib/classify.ts
import { generateText, Output } from 'ai';
import { z } from 'zod';
const review = 'Great tacos, slow delivery.';
const { output: sentiment } = await generateText({
model: 'anthropic/claude-haiku-4.5',
output: Output.choice({ options: ['positive', 'neutral', 'negative'] }),
prompt: `Classify the sentiment of this review: ${review}`,
});
const { output: tags } = await generateText({
model: 'anthropic/claude-haiku-4.5',
output: Output.array({ element: z.object({ tag: z.string(), confidence: z.number() }), maxItems: 5 }),
prompt: `Suggest tags for: ${review}`,
});
console.log(sentiment, tags.length);streamText takes the same output option and exposes partialOutputStream, so you can render fields as they fill in.4Tools and multi-step loops
A tool is three things: a description (when to use it), an inputSchema (what arguments are valid), and an execute function (what actually happens). The model only ever sees the first two.
lib/order-bot.ts
import { generateText, isStepCount, tool } from 'ai';
import { z } from 'zod';
const lookupOrders = tool({
description: 'Look up a customer\'s recent orders by email address.',
inputSchema: z.object({
email: z.string().email().describe('The customer email address'),
}),
execute: async ({ email }) => {
const orders = await db.orders.findByEmail(email);
return orders.slice(0, 5).map((o) => ({ id: o.id, status: o.status }));
},
});
const result = await generateText({
model: 'anthropic/claude-sonnet-5',
instructions: 'You help customers track orders. Use tools; never guess order data.',
tools: { lookupOrders },
stopWhen: isStepCount(5), // at most 5 model calls, then stop no matter what
prompt: 'Where is my order? My email is sam@example.com',
});
console.log(result.text);
console.log(`Took ${result.steps.length} steps, ${result.usage.totalTokens} tokens total`);lookupOrders. The SDK runs execute and sends back the result. Step 2, the model writes the answer. Two steps, two model calls.stopWhen, generation ends after a tool call and you never get the final text. With it, you allow up to n rounds and cap cost.In v7, result.usage is the total across all steps, which is the number you care about for billing. Per-step detail lives in result.steps.
5Reusable agents, finish tools, and human approval
Once the same model + instructions + tools show up in three routes, wrap them in a ToolLoopAgent. Two patterns worth stealing here: a finish tool (a tool with no execute that the agent calls to say "I'm done," paired with hasToolCall), and approval for anything with side effects.
lib/agents/support.ts
import { hasToolCall, isStepCount, ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
export const supportAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5',
instructions: 'You are Taco Tracker support. Search the docs before answering.',
tools: {
searchDocs: tool({
description: 'Search the help center',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => searchDocs(query),
}),
refundOrder: tool({
description: 'Refund an order. Only when the customer explicitly asks.',
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => refundOrder(orderId),
}),
finalAnswer: tool({
description: 'Call this with your final reply to the customer.',
inputSchema: z.object({ reply: z.string() }),
}),
},
// Pause for a human before any refund runs.
toolApproval: { refundOrder: 'user-approval' },
// Stop when the agent calls finalAnswer, or after 10 steps as a safety net.
stopWhen: [hasToolCall('finalAnswer'), isStepCount(10)],
});
const result = await supportAgent.generate({ prompt: 'How do I change my delivery address?' });
console.log(result.steps.length);With Human-in-the-Loop Designing an AI workflow so a person approves, corrects, or chooses at key moments, especially before risky or irreversible actions like sending money, emailing customers, or deleting data. Claude Code's permission prompts are a built-in example. "Like a pilot on autopilot who still has to confirm before landing."toolApproval set, the refund doesn't run. The result contains a tool approval request instead, and in a chat UI you answer it from the client. This is human-in-the-loop
app/chat/page.tsx (excerpt)
// In your useChat component: approval requests arrive as tool parts.
const { messages, addToolApprovalResponse } = useChat({ /* transport... */ });
// When the user clicks a button next to the pending refund:
addToolApprovalResponse({ id: approvalId, approved: true });
// or: addToolApprovalResponse({ id: approvalId, approved: false, reason: 'Not eligible' });Need the approval to wait for hours, or survive a deploy? That's a job for a durable workflow, covered in Durable Agents.
6Doing it natively with the Claude API
If you use Anthropic's own SDK, structured outputs are generally available with no beta header. The parameter is output_config.format (the older output_format is deprecated). The TypeScript SDK's messages.parse helper plus zodOutputFormat validates the reply against your zod schema for you.
lib/extract-invoice.ts
import Anthropic from '@anthropic-ai/sdk';
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
import { z } from 'zod';
const Invoice = z.object({
vendor: z.string(),
total: z.number(),
currency: z.string(),
dueDate: z.string().describe('ISO date, YYYY-MM-DD'),
});
const client = new Anthropic(); // reads ANTHROPIC_API_KEY
const invoiceText = '...';
const response = await client.messages.parse({
model: 'claude-sonnet-5',
max_tokens: 2000,
messages: [{ role: 'user', content: `Extract the invoice fields:\n\n${invoiceText}` }],
output_config: { format: zodOutputFormat(Invoice) },
});
// parsed_output is null if parsing failed, so guard it
const invoice = response.parsed_output;
if (invoice) console.log(invoice.vendor, invoice.total);For tools, add "strict": true to the tool definition and Claude's arguments are guaranteed to match the schema. Strict schemas need required and additionalProperties: false:
Tool definition (Claude API)
{
"name": "book_table",
"description": "Book a restaurant table",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"date": { "type": "string", "format": "date" },
"partySize": { "type": "integer", "enum": [1, 2, 3, 4, 5, 6] }
},
"required": ["date", "partySize"],
"additionalProperties": false
}
}Reference: Claude structured outputs docs
7Common traps
Trusting tool arguments
The model picks the arguments, and a crafty user can steer it. Inside execute, check the signed-in user owns that order ID. Never let the model choose whose data to read.
Vague descriptions
“gets data” is useless. Say what the tool does, when to use it, and when not to. Descriptions are prompts.
No stop condition
Tools without stopWhen end after one call. With a huge limit, a confused model burns tokens in circles. Pick a small number and raise it only with evidence.
Giant schemas
Twenty nested optional fields make the model guess. Split into two calls or simplify. Prefer .nullable() with a clear describe() over lots of optional fields.
Output + tools step budget
When you combine tools and output, producing the final object counts as a step. Leave room in isStepCount for it.
Copying v4/v5 tutorials
Old names are deprecated, removed, or behave differently now. See the rename table below.
| If a tutorial says | AI SDK 7 uses | Why |
|---|---|---|
| generateObject / streamObject | generateText / streamText with output: Output.object({ schema }) | deprecated since v6 |
| stepCountIs(n) | isStepCount(n) | v7 rename |
| system: '...' or a system message in messages | instructions: '...' | system messages in messages are rejected by default |
| needsApproval on the tool | toolApproval: { toolName: 'user-approval' } on the call or agent | v7 moves approval out of the tool |
| onFinish + totalUsage | onEnd; result.usage is already the all-steps total | v7 usage change |
8Which one do I need?
Turn messy input into clean data (extract, classify, tag)
Structured output only. One call, no tools.
Answer questions that need your live data
Tools + a small isStepCount (3 to 5).
Open-ended tasks where the model plans its own steps
ToolLoopAgent with a finish tool and a step cap.
Anything that sends, buys, deletes, or refunds
Tools + toolApproval, so a human clicks yes first.
Runs longer than a request, waits on people, or must survive crashes
A durable workflow wrapping the agent.
Before you ship any of these, write a few test cases. The evals and guardrails guide shows how to check that the schema output is actually right, not just valid.
Your model can act now. Keep it safe and alive.
Tools are where prompt injection starts to hurt, and multi-step agents are where serverless timeouts start to bite.