Concept
Tool use (also called function calling) is the mechanism that lets Claude trigger real actions in your application, looking up live data, querying a database, calling another API, despite the model itself having no ability to execute anything. Claude only ever emits a structured request to call a named function with specific arguments; your own application code is always the thing that actually runs it and reports back what happened.
Defining a tool
A tool definition is just a name, a description, and a JSON Schema describing its expected input:
const getWeatherTool: Anthropic.Tool = {
name: "get_weather",
description: "Get the current weather for a city. Call this when the user asks about current conditions in a specific location.",
input_schema: {
type: "object",
properties: {
city: { type: "string", description: "City name, e.g. 'Tokyo'" },
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
},
};Claude relies heavily on description to decide whether and when to call a tool, being explicit about the triggering condition ("Call this when the user asks about current conditions...") measurably improves how reliably the model reaches for the right tool, compared to a vague description of only what the tool does.
The single round trip
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 1024,
tools: [getWeatherTool],
messages: [{ role: "user", content: "What's the weather in Tokyo?" }],
});
// response.stop_reason === "tool_use"
// response.content includes a block like:
// { type: "tool_use", id: "toolu_01...", name: "get_weather", input: { city: "Tokyo" } }const response = await client.messages.create({model: 'claude-opus-4-8',tools: [getWeatherTool],messages: [{ role: 'user', content: 'Weather in Tokyo?' }],});
Tool definitions (name, description, input_schema) are sent alongside the messages on EVERY request, Claude doesn't remember tools between calls, since the API is stateless.
stop_reason: "tool_use" means Claude isn't finished with its answer, it's paused, waiting for your code to actually execute get_weather("Tokyo") and report the result back. Tool definitions are sent alongside messages on every request, because the Messages API is fully stateless: Claude has no memory of what tools exist between separate API calls, only what you send it each time.
Sending the result back
const toolResult = await getWeather("Tokyo"); // your real implementation
const followup = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 1024,
tools: [getWeatherTool],
messages: [
{ role: "user", content: "What's the weather in Tokyo?" },
{ role: "assistant", content: response.content }, // the FULL content array, including the tool_use block
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: response.content[0].id, content: JSON.stringify(toolResult) },
],
},
],
Three details are load-bearing here. First, you must append the entire response.content array as the assistant turn, not just the text parts, because that array is what carries the tool_use block Claude needs to see echoed back to understand what it's responding to. Second, the tool_result block's tool_use_id must match the id from the original tool_use block exactly, this is how Claude correlates a result with the specific call that produced it, especially when multiple tools were called in the same turn. Third, if a tool execution fails, return the result with is_error: true rather than silently dropping it or throwing, Claude can often recover gracefully (retry differently, apologize, ask a clarifying question) if it's told the call failed, but has no way to recover from a tool_result that never arrives at all.
Parallel tool calls in a single turn
Claude can request multiple independent tool calls in one turn, the response's content array simply contains more than one tool_use block. Execute them concurrently, but return all their results in a single user message, not split across multiple round trips:
// response.content might be:
// [{ type: "tool_use", id: "toolu_01", name: "search_flights", input: {...} },
// { type: "tool_use", id: "toolu_02", name: "get_weather", input: {...} }]
const toolUseBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
const results = await Promise.all(
toolUseBlocks.map(async (block) => ({
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(await executeTool(block.name, block.input)),
Splitting parallel tool results across separate follow-up messages is a real mistake, not just a style issue, it measurably discourages the model from making parallel calls in future turns.
The manual loop, and the Tool Runner helper that automates it
An agent that may need several sequential tool calls is really just a loop: keep calling the API, executing whatever's requested, feeding results back, until stop_reason is no longer "tool_use".
while (response.stop_reason === 'tool_use') {// execute every requested tool, collect results, loop}
An 'agent' is really just this loop: keep calling the API, executing whatever tools it asks for, and feeding results back, until it stops asking for tools.
let messages: Anthropic.MessageParam[] = [{ role: "user", content: userInput }];
while (true) {
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 4096,
tools,
messages,
});
if (response.stop_reason === "end_turn") break;
messages.push({ role: "assistant", content: response.content });
const toolUseBlocks = response.content.filter(
(b): b