Claude API Agent With Tool Calling: A Working Example
Building a Claude API agent with tool calling means writing code that lets Claude decide when to call a function you've defined — like get_weather or search_database — then feeding the result back so Claude can finish its answer. This article walks through a complete, runnable example: the request structure, the response parsing, and the loop that ties it together.
If you've only used Claude for single-turn text generation, tool calling is the piece that turns it into an agent: something that can take actions, observe results, and decide what to do next instead of just producing one block of text.
What "tool calling" actually is
You send Claude a list of tools (name, description, JSON schema for inputs) alongside your prompt. Claude doesn't execute anything — it returns a response that says "call this tool with these arguments." Your code runs the actual function, then sends the result back to Claude in a follow-up message. Claude reads that result and either calls another tool or returns a final answer.
This request/response/execute/respond cycle is the entire mechanism behind every Claude-based agent, no matter how complex the end product looks.
Defining a tool
A tool is a JSON Schema object describing what Claude can call:
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
The description matters more than most people expect — Claude uses it to decide whether to call the tool at all, not just how to fill in the arguments. Vague descriptions lead to tools being ignored or misused.
The request
Here's a minimal request to the Claude Messages API with one tool attached:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"tools": [{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius","fahrenheit"]}
},
"required": ["city"]
}
}],
"messages": [
{"role": "user", "content": "What is the weather in Lisbon right now?"}
]
}'
If Claude decides to use the tool, the response contains a content block with "type": "tool_use", a name, an id, and input (the arguments it filled in). There's no plain text to show the user yet — you need to run the tool and reply.
The agent loop
The core of any tool-calling agent is a loop: send messages, check if Claude wants a tool, run it, append the result, repeat until Claude returns plain text.
async function runAgent(userMessage, tools, executeTool) {
let messages = [{ role: "user", content: userMessage }];
while (true) {
const response = await callClaude(messages, tools);
const toolUses = response.content.filter(b => b.type === "tool_use");
if (toolUses.length === 0) {
return response.content.find(b => b.type === "text")?.text;
}
messages.push({ role: "assistant", content: response.content });
const toolResults = [];
for (const call of toolUses) {
const result = await executeTool(call.name, call.input);
toolResults.push({
type: "tool_result",
tool_use_id: call.id,
content: JSON.stringify(result)
});
}
messages.push({ role: "user", content: toolResults });
}
}
A few details matter here:
tool_use_idmust match exactly. Claude correlates results to calls by this ID, especially important when multiple tools are called in the same turn.- Tool results go in a
usermessage, not a system message — this trips up a lot of first implementations. - The loop has no hard stop unless you add one. In production, cap iterations (5–8 is usually enough) to avoid infinite loops if a tool keeps returning data Claude wants to act on again.
Handling multiple tool calls in one turn
Claude can request several tools at once — for example, checking weather in three cities in parallel. Your toolUses array will have multiple entries, and you must return a result for every one of them before the next request, even if some fail:
const result = await executeTool(call.name, call.input).catch(err => ({
error: err.message
}));
Returning an error object as the tool result (rather than throwing) lets Claude see the failure and decide whether to retry, try a different approach, or tell the user it can't complete the request. This is usually better than crashing the loop.
Running this through SubToAPI
If you're already using Claude through SubToAPI, the agent loop above works unchanged — you just swap the endpoint and auth header. SubToAPI exposes the same Messages-style API with sub_live_ application keys instead of raw Anthropic credentials, which matters once you have more than one app or teammate hitting Claude:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"tools": [ /* same tool schema */ ],
"messages": [ /* same messages */ ]
}'
Tool definitions, tool_use blocks, and tool_result messages all follow the same shape — see /docs/tools for the full reference. The practical benefit is usage metadata per application key and per seat, so if your agent is shared across a team, you can see exactly which key is driving tool calls and token usage without scraping logs yourself.
Streaming tool-calling agents
Tool use works with streaming too, but you need to buffer input_json deltas until the tool_use block closes before you have a complete, parseable input object. Partial JSON isn't safe to execute against. See /docs/streaming for the event sequence if you want to stream intermediate text while a tool call is still being assembled.
Common mistakes to avoid
- Not returning a result for every
tool_useblock. Claude will stall or error if any call in a turn is left unanswered. - Overly broad tool descriptions. If two tools sound similar, Claude may pick the wrong one or call both unnecessarily.
- Treating tool calls as guaranteed JSON. Validate
inputagainst your schema before executing — Claude is good but not infallible, especially with nested objects. - No iteration cap. Always bound the agent loop; a tool that returns ambiguous data can cause repeated unnecessary calls.
Start with /docs/quickstart if you want the full request/response contract, or /docs/messages for everything the Messages endpoint accepts beyond tools (system prompts, stop sequences, multi-turn history). A free trial is available at /signup, and plan details — including team seats if multiple developers will be building against the same agent — are on /pricing.
Questions
Does Claude execute the tool itself? No. Claude only returns the tool name and arguments it wants to use. Your application code is responsible for actually running the function and sending the result back.
Can an agent call multiple tools in a single turn? Yes. Claude can return several tool_use blocks in one response, and you must return a tool_result for each one, matched by tool_use_id, before continuing.
How do I stop an agent loop from running forever? Add a hard iteration limit (5–8 is typical) and treat hitting the limit as a failure case — return a message to the user rather than looping indefinitely on ambiguous tool results.