// LOADING
// LOADING
// LOADING_ARTICLE
Text generation alone only gets you so far. The moment an LLM can call a weather API, query a database, or send an email, it transforms from a text predictor into an agent that can actually accomplish tasks.
Tool use (also called function calling) is the mechanism that enables this. This post explains exactly how it works, how to define tools, and how to handle the tool call loop correctly in a Node.js or Next.js backend.
The protocol is a structured request-response loop:
tool_use block instead of prose.This loop can repeat multiple times within a single conversation turn if the model needs to chain several tool calls.
Tools are described as JSON Schema objects. Each tool needs a name, a description the model uses to decide when to call it, and an input_schema that describes its parameters.
tsimport Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "get_current_weather", description: "Returns the current temperature and conditions for a given city. Use this when the user asks about weather.", input_schema: { type: "object", properties: { city: { type: "string", description: "City name, e.g. 'Berlin'", }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "Temperature unit", }, }, required: ["city"], }, }, ];
The description is not boilerplate — it is the primary signal the model uses to decide whether to invoke the tool. Write it the way you would write a docstring for a junior developer: be precise about when it applies and what it returns.
A production tool loop handles the multi-step cycle cleanly:
tstype Message = Anthropic.MessageParam; async function runWithTools(userMessage: string): Promise<string> { const messages: Message[] = [ { role: "user", content: userMessage }, ]; while (true) { const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 1024, tools, messages, }); // Push the assistant turn into history messages.push({ role: "assistant", content: response.content }); if (response.stop_reason === "end_turn") { // Final text response const textBlock = response.content.find((b) => b.type === "text"); return textBlock?.type === "text" ? textBlock.text : ""; } if (response.stop_reason === "tool_use") { const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type !== "tool_use") continue; const result = await dispatchTool(block.name, block.input); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result), }); } messages.push({ role: "user", content: toolResults }); } } } async function dispatchTool( name: string, input: Record<string, unknown> ): Promise<unknown> { if (name === "get_current_weather") { // Replace with a real weather API call return { temperature: 18, conditions: "partly cloudy", unit: input.unit ?? "celsius" }; } throw new Error(`Unknown tool: ${name}`); }
Several things make this implementation robust:
stop_reason is "end_turn", covering multi-tool chains without special-casing.messages. This keeps the conversation history consistent and is required by the API.tool_result blocks with the matching tool_use_id, not as plain text.Claude can emit multiple tool_use blocks in a single response, meaning it wants to call several tools in parallel. The loop above handles this correctly because it iterates over all tool_use blocks in response.content before sending results back. Run the actual tool functions concurrently with Promise.all to avoid unnecessary latency:
tsconst toolResults = await Promise.all( response.content .filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use") .map(async (block) => ({ type: "tool_result" as const, tool_use_id: block.id, content: JSON.stringify(await dispatchTool(block.name, block.input)), })) );
Tool use executes real code based on model output. Treat the model's input like untrusted user input:
block.input at runtime.Wrap runWithTools in a Route Handler and expose it as a POST endpoint. If the tool loop is long-running, consider streaming the intermediate steps so the UI stays responsive rather than waiting for the final turn.
For a broader look at how Route Handlers fit into API design, see Route Handlers vs Server Actions in Next.js.
Once you have several tools and a loop that keeps running until the model decides it is done, you have a basic agent. The same pattern — define tools, loop until end_turn, dispatch tool calls — underpins more sophisticated architectures like ReAct and Plan-and-Execute. See Building AI Agents: Architecture and Patterns for the next level.
You can also browse /interview-prep for practice questions on agent design and tool-calling patterns.
Tool use is the bridge between language models and real-world systems. The protocol is simple: describe your tools in JSON Schema, run the loop until end_turn, dispatch tool calls to your actual implementations, and pass results back as tool_result blocks. Get the loop right and you have a solid foundation for everything from single-step lookups to multi-step autonomous agents.