Skip to main content
RapidDev - Software Development Agency
build-ai-agent

How to Build an AI Agent with TypeScript

The Vercel AI SDK's ToolLoopAgent packages the tool-calling loop as a single TypeScript class: define tools with tool() and a Zod inputSchema, set a model string and a stopWhen limit, then call agent.generate() or agent.stream(). This guide builds a deploy-watch agent that checks a service's deployment status and posts an incident summary to Slack, fully typed from input schema to tool result. Budget 35-45 minutes and a provider-agnostic model string you can swap in one line.

What you'll learn

  • Define tools with the Vercel AI SDK's tool() helper and a Zod inputSchema instead of hand-written JSON Schema
  • Build a ToolLoopAgent with a step cap (stopWhen: isStepCount) so a confused agent fails fast
  • Read a typed result (text, steps, tool calls) from agent.generate() without parsing anything yourself
  • Swap model providers by changing a single string, since the SDK's model format is provider-agnostic
Book a free consultation
4.9Clutch rating ⭐
1,000+Happy partners
20+Countries served
200+Team members
Intermediate9 min read35-45 minNode.js 20+, TypeScript 5+, ai (Vercel AI SDK) v7, zod, Claude or OpenAI API keyLast updated November 2026RapidDev Engineering Team
TL;DR

The Vercel AI SDK's ToolLoopAgent packages the tool-calling loop as a single TypeScript class: define tools with tool() and a Zod inputSchema, set a model string and a stopWhen limit, then call agent.generate() or agent.stream(). This guide builds a deploy-watch agent that checks a service's deployment status and posts an incident summary to Slack, fully typed from input schema to tool result. Budget 35-45 minutes and a provider-agnostic model string you can swap in one line.

Quick facts about this guide
FactValue
ToolTypeScript
DifficultyIntermediate
Time required35-45 min
CompatibilityNode.js 20+, TypeScript 5+, ai (Vercel AI SDK) v7, zod, Claude or OpenAI API key
Last updatedNovember 2026

What you'll build

You'll build a deploy-watch agent: ask it about a service, and it checks that service's current deployment status, and if something's degraded, posts an incident summary to a Slack channel. It's the kind of small ops agent that fits naturally into a Next.js API route or a scheduled job, which is exactly where TypeScript teams tend to run agents in practice.

The Vercel AI SDK's ToolLoopAgent is the current high-level entry point for this: you hand it a model string, a set of tools, and a stopWhen condition, and it runs the call-tool-check-repeat loop internally. Each tool is defined with the tool() helper, a description the model reads, a Zod inputSchema that both validates the model's arguments and gives you full TypeScript types inside execute(), and an async execute function that does the real work and returns a typed result.

Pick the AI SDK when you're building in a Next.js or Node TypeScript stack and want the model, the tool definitions, and the final result all typed end to end with no 'any' escape hatches. Pick LangChain or the Claude Agent SDK (also available in TypeScript) when you specifically want their broader middleware ecosystems. The loop underneath is the same either way; this guide is about what TypeScript's own type system buys you when the SDK leans into it.

Prerequisites

  • Node.js 20+ and a TypeScript project (npm init -y && npm i -D typescript tsx)
  • An Anthropic or OpenAI API key with available credits
  • npm i ai zod
  • Comfort with async/await and basic Zod schemas

Step-by-step guide

1

Install the SDK and set your API key

Install ai (the Vercel AI SDK) and zod, which the SDK uses for tool input validation. Export your provider's API key as an environment variable; the SDK's provider strings read it the same way the underlying client would. Model strings in v7 are provider-agnostic: 'anthropic/claude-sonnet-5.5' or 'openai/gpt-6.1-sol' both work with the same agent code.

typescript
1npm i ai zod
2export ANTHROPIC_API_KEY="sk-ant-..."

Expected result: npx tsx -e "import('ai')" resolves with no module errors.

2

Define tool input schemas with Zod

Each tool's inputSchema is a Zod object. z.object({...}).describe() comments give the model extra context on top of the tool's own description. Define checkDeployStatus (takes a service name) and postIncidentSummary (takes a service name and a summary string).

typescript
1import { z } from 'zod';
2
3const checkDeployStatusInput = z.object({
4 service: z.string().describe('The service name, e.g. checkout-service'),
5});
6
7const postIncidentSummaryInput = z.object({
8 service: z.string(),
9 summary: z.string().describe('A one-paragraph incident summary'),
10});

Expected result: Two Zod schemas you could validate sample input against directly, with no agent involved yet.

3

Write the tool execute functions

tool() takes a description, the inputSchema, and an async execute function whose parameter is fully typed from that schema, no casting required. checkDeployStatus returns a mock status object here; in production it calls your deployment platform's API. postIncidentSummary would call the Slack API's chat.postMessage.

typescript
1import { tool } from 'ai';
2
3const checkDeployStatus = tool({
4 description: 'Check the current deployment status for a service.',
5 inputSchema: checkDeployStatusInput,
6 execute: async ({ service }) => {
7 return { service, status: 'degraded', errorRatePct: 12.4 };
8 },
9});
10
11const postIncidentSummary = tool({
12 description: 'Post an incident summary to the #incidents Slack channel.',
13 inputSchema: postIncidentSummaryInput,
14 execute: async ({ service, summary }) => {
15 // await slackClient.chat.postMessage({ channel: '#incidents', text: summary });
16 return { posted: true, service };
17 },
18});

Expected result: Two tool() objects ready to pass into a ToolLoopAgent's tools map.

4

Create the ToolLoopAgent with a step cap

Instantiate ToolLoopAgent with a model string, instructions (the v7 name for what used to be called system), the tools map, and stopWhen: isStepCount(n) so a confused agent fails after a fixed number of steps instead of looping indefinitely. 10 is a reasonable default for a two-tool agent.

typescript
1import { ToolLoopAgent, isStepCount } from 'ai';
2
3const agent = new ToolLoopAgent({
4 model: 'anthropic/claude-sonnet-5.5',
5 instructions:
6 'You are an on-call ops assistant. Always check deploy status before ' +
7 'posting a summary. Only post a summary if the status is degraded or worse.',
8 tools: { checkDeployStatus, postIncidentSummary },
9 stopWhen: isStepCount(10),
10});

Expected result: A fully typed agent instance; TypeScript now knows the exact shape of every tool call it can make.

5

Call agent.generate() and read the typed result

agent.generate() runs the loop and returns an object with text (the final answer), steps (every step the agent took, including tool calls and their results), and more. Use result.steps when you need to show or log what happened, not just the final text.

typescript
1const result = await agent.generate({
2 prompt: 'Check checkout-service and post a summary if anything is wrong.',
3});
4
5console.log(result.text);
6console.log(`${result.steps.length} steps taken`);

Expected result: result.text contains a short written summary, and result.steps shows checkDeployStatus running before postIncidentSummary.

6

Stream it, handle errors, and ship it

For a UI that shows progress live, call agent.stream() instead of generate() and consume the stream. Wrap each tool's execute() in try/catch so a failed Slack call or API timeout returns a typed error object instead of throwing into the loop. In a Next.js app, wrap this whole agent in a Route Handler (app/api/incident-check/route.ts) rather than calling it from a Client Component, so your API key never ships to the browser.

typescript
1export async function POST(req: Request) {
2 const { service } = await req.json();
3 const result = await agent.generate({ prompt: `Check ${service} and post a summary if degraded.` });
4 return Response.json({ text: result.text, steps: result.steps.length });
5}

Expected result: An agent that fails gracefully on a bad tool call and never exposes its API key to a browser.

Complete working example

deploy-watch-agent.ts
1// Deploy-watch agent built with the Vercel AI SDK's ToolLoopAgent.
2// Run: npx tsx deploy-watch-agent.ts (npm i ai zod; set ANTHROPIC_API_KEY)
3import { ToolLoopAgent, tool, isStepCount } from 'ai';
4import { z } from 'zod';
5
6const checkDeployStatusInput = z.object({
7 service: z.string().describe('The service name, e.g. checkout-service'),
8});
9
10const postIncidentSummaryInput = z.object({
11 service: z.string(),
12 summary: z.string().describe('A one-paragraph incident summary'),
13});
14
15const checkDeployStatus = tool({
16 description: 'Check the current deployment status for a service.',
17 inputSchema: checkDeployStatusInput,
18 execute: async ({ service }) => {
19 // In production: call your deployment platform's status API here.
20 return { service, status: 'degraded', errorRatePct: 12.4 };
21 },
22});
23
24const postIncidentSummary = tool({
25 description: 'Post an incident summary to the #incidents Slack channel.',
26 inputSchema: postIncidentSummaryInput,
27 execute: async ({ service, summary }) => {
28 try {
29 // In production: await slackClient.chat.postMessage({ channel: '#incidents', text: summary });
30 return { posted: true, service };
31 } catch (err) {
32 return { posted: false, service, error: String(err) };
33 }
34 },
35});
36
37const agent = new ToolLoopAgent({
38 model: 'anthropic/claude-sonnet-5.5',
39 instructions:
40 'You are an on-call ops assistant. Always check deploy status before ' +
41 'posting a summary. Only post a summary if the status is degraded or worse.',
42 tools: { checkDeployStatus, postIncidentSummary },
43 stopWhen: isStepCount(10),
44});
45
46async function main() {
47 const result = await agent.generate({
48 prompt: 'Check checkout-service and post a summary if anything is wrong.',
49 });
50 console.log(result.text);
51 console.log(`${result.steps.length} steps taken`);
52}
53
54main();
55

Common mistakes

Why it's a problem: Omitting stopWhen and letting the default step count hide a looping agent

How to avoid: Set stopWhen: isStepCount(n) explicitly to a number that fits your agent's tool count, and treat hitting it as a bug to investigate, not a fluke.

Why it's a problem: Calling the agent directly from a Client Component in a Next.js app

How to avoid: Model API keys live server-side only. Wrap the agent in a Route Handler or Server Action and call that from the client instead.

Why it's a problem: Letting a tool's execute() throw instead of returning a typed error object

How to avoid: An uncaught throw inside execute() surfaces as a confusing failure to the model. Catch it and return { error: ... } so the agent can react.

Why it's a problem: Writing a Zod schema with no .describe() calls on ambiguous fields

How to avoid: A field named 'service' with no description is clear to you and ambiguous to the model. Add .describe() wherever the field name alone doesn't explain the expected value.

Why it's a problem: Using old v6 names (system, stepCountIs, maxSteps) in new v7 code

How to avoid: v7 renamed these to instructions, isStepCount, and the ToolLoopAgent constructor's stopWhen option. The old names may still work as aliases but shouldn't appear in new code.

Best practices

  • Always set stopWhen explicitly; don't rely on an SDK default to catch a looping agent for you.
  • Give every Zod field a .describe() unless its name alone fully explains the expected value.
  • Keep execute() thin: call a typed API client you can mock, don't inline business logic in the tool definition.
  • Return typed error objects from execute() instead of throwing, so the agent can see and react to a failure.
  • Run the agent from a Route Handler or Server Action, never from client-side code, to keep API keys server-only.
  • Read result.steps during development, not just result.text, to confirm tools ran in the order you expect.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Vercel AI SDK ToolLoopAgent with checkDeployStatus and postIncidentSummary tools (code below). Add a third tool, rollbackDeploy, gated so the agent only calls it after explicit confirmation in the prompt, not automatically. [paste complete_code]

Build AI Agents Prompt

In Cursor, open deploy-watch-agent.ts and ask: 'Wrap this agent in a Next.js Route Handler at app/api/incident-check/route.ts that accepts a service name and returns the agent's text and step count as JSON.'

Frequently asked questions

Why use TypeScript instead of Python to build an AI agent?

Mainly because your app is already TypeScript, a Next.js or Node backend, and you want the agent typed end to end with the rest of the codebase. The Vercel AI SDK's tool() plus Zod gives you that; functionally it runs the same tool-calling loop Python frameworks do.

What's the difference between agent.generate() and agent.stream()?

generate() waits for the full loop to finish and returns one result object. stream() returns the same information incrementally as it happens, which is what you want for a chat UI that should show progress instead of a long blank wait.

Why did my code break after upgrading the Vercel AI SDK?

v7 renamed several APIs: system became instructions, stepCountIs and maxSteps became isStepCount, and onFinish/onStepFinish became onEnd/onStepEnd. Old names are often kept as aliases for a transition period, but new code should use the current names.

Can I swap models without rewriting the agent?

Yes, that's the point of the provider-agnostic model string. Changing 'anthropic/claude-sonnet-5.5' to 'openai/gpt-6.1-sol' is a one-line change as long as the new model supports the tool-calling behavior your agent relies on.

How do I stop the agent from looping forever on a bad tool result?

Set stopWhen: isStepCount(n) to a number that fits your tool count. The agent will stop after n steps regardless of whether it finished, which turns an infinite loop into a bounded, investigable failure.

Can someone build this for me instead of doing it myself?

Yes. RapidDev builds production TypeScript agents on the Vercel AI SDK, the Claude Agent SDK, and the OpenAI Agents SDK, including the Route Handler wiring and error handling this guide introduces. A free 30-minute consultation is the fastest way to scope your version.

RapidDev

Talk to an Expert

Our team has built 1,000+ apps. Get personalized help with your project.

Book a free consultation
Matt Graham

Written by

Matt Graham · CEO & Founder, RapidDev

1,000+ client projects delivered. Columbia University & Harvard Business School alumnus, U.S. Navy veteran. About the author →

Learning is great. Shipping is faster with help.

Our engineers have built 1,000+ apps on your platform and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.