---
title: "The tool loop in a route"
summary: "Match the Tool Calling lab: streamText, Zod tool schemas, and stopWhen in one API route."
track: "The loop"
day: 1
minutes: 16
author: "Akash Panchal"
url: https://ai-sdk-patterns.dev/learn/agents/day-1/tool-loop-route
dateModified: 2026-08-29
---

# The tool loop in a route

Match the Tool Calling lab: streamText, Zod tool schemas, and stopWhen in one API route.

*The loop · Day 1: Agent loops · ~16 min. Written by [Akash Panchal](https://github.com/akashp1712).*

Canonical: https://ai-sdk-patterns.dev/learn/agents/day-1/tool-loop-route

## After this topic

You will have a Route Handler that is the Tool Calling lab: streamText, a Zod tool, a step cap, the same UI stream as chat. You will know what the model sees (the schema) and what it never sees (execute).

## Route handler

Same stream as chat — the new part is the tools map and a step cap.

This is the same pattern as the Tool Calling lab. inputSchema uses Zod directly — the SDK validates arguments before execute runs. If the model invents a field, you never hit your function with garbage (or you fail closed — better than a wrong city in production).

stopWhen: stepCountIs(5) allows up to five model steps (tool rounds plus the final answer). Tune n to the task: weather-and-compare might need three; a research agent might need more, with a higher bill.

toUIMessageStreamResponse() is the same last line as streaming chat. Tool calls show up as parts on the assistant message. The client does not need a second API.

### app/api/chat/route.ts

```ts
import { streamText, tool, stepCountIs } from "ai";
import { z } from "zod";
import { getModel } from "@/lib/model";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: getModel(),
    messages,
    tools: {
      weather: tool({
        description: "Get the current weather for a location",
        inputSchema: z.object({
          location: z.string().describe("City name"),
        }),
        execute: async ({ location }) => ({
          location,
          temperature: "22°C",
          condition: "clear",
        }),
      }),
    },
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}
```

- **tools is a map of functions the model may call.** Each tool has a description (when to use it), inputSchema (arguments), and execute (your code). The model never sees execute — only the schema.
- **stopWhen caps the loop.** stepCountIs(5) allows up to five model steps. Without a cap, a confused model can call tools forever and run up cost.
- **Same stream as Day 3 chat.** toUIMessageStreamResponse() still feeds useChat. Tool calls appear as parts on the message, not as a separate API.

## What runs on which machine

The model, in a datacenter, emits a tool name and JSON arguments. Your Route Handler, on Vercel, runs execute. The browser never runs the tool. That split is the same as the chat key: side effects belong on the server.

Weather in this snippet is fake data so the lesson does not need a third-party key. In a product, execute hits your API, your database, or a vendor. Same shape. Real consequences.

## Trade-offs

Too few steps and the task stops mid-way (one city, no comparison). Too many and a stuck model burns tokens. Start small, log steps, raise n when you see real multi-call tasks.

Use generateText with tools when you are debugging — you get the full step list in one object. Use streamText for the chat UI.

When the same agent must run from a route, a cron, and a job, look at ToolLoopAgent in the official agents docs. For this lesson, one Route Handler is the right size.

- Cap the loop (stopWhen) — always
- Descriptions and schemas decide whether tools get called correctly (next day)
- Destructive execute belongs behind approval

## Further reading

The official Building Agents guide covers ToolLoopAgent when you want a shared agent object across routes and jobs. The catalog Tool Calling lab is this route with a UI.

## Common questions

### What is “The tool loop in a route”?

Match the Tool Calling lab: streamText, Zod tool schemas, and stopWhen in one API route.

### What will I be able to do after this lesson?

You will have a Route Handler that is the Tool Calling lab: streamText, a Zod tool, a step cap, the same UI stream as chat. You will know what the model sees (the schema) and what it never sees (execute).

### How long does this lesson take?

About 16 minutes of reading. It is a free chapter in the AI SDK Patterns TypeScript course.

### When should I not use this?

Too few steps and the task stops mid-way (one city, no comparison). Too many and a stuck model burns tokens. Start small, log steps, raise n when you see real multi-call tasks.
