Tool Use with Claude
Claude is brilliant but locked in a windowless room — no clock, no internet, no database. Function calling hands it a panel of labeled buttons your code controls. Four concepts, from the mental model to error handling.
The 4 concepts of function calling
Tap any concept to jump to its 5-card cluster.
Claude proposes. You execute.
Function calling lets Claude reach beyond its training data and interact with the real world — a weather API, your database, a deployment script. But Claude itself never runs your code. Instead, when it decides a tool would help, it returns a structured tool_use block that says "I'd like to call get_weather with city='Tokyo'."
Your application reads that request, runs the actual function on your server, and sends the output back as a tool_result. Claude uses the result to write its final answer. This split — deciding belongs to Claude, executing belongs to you — is what makes tool use safe enough for production systems.
The brilliant assistant in a windowless room
BEFORE: Picture a super-smart assistant who can write beautifully, reason through hard problems, and hold a conversation — but is locked in a windowless room with no phone, no internet, no clock. Every answer about the outside world is a guess from what they already know.
PAIN: Ask "What's the weather in Tokyo right now?" and the assistant has no choice but to make something up or refuse. They can't query an API, check a database, or look anything up. Their intelligence is trapped — brilliant but blind to the real world.
MAPPING: Function calling hands the assistant a panel of labeled buttons — get_weather(), calculate(), get_time() — each wired to the outside world. Claude pushes the button it wants by returning a tool_use block; you are the wiring on the other side of the wall who actually performs the action and slides the result back under the door.
The six-step round-trip
- User asks a question"What's the weather in Tokyo?" arrives at your app.
- You send message + tools to ClaudeThe API call includes your
toolsarray describing what's available. - Claude returns a tool_use blockInstead of text, the response carries
{name: "get_weather", input: {city: "Tokyo"}}andstop_reason: "tool_use". - YOUR code executes the functionYou read the block, validate, run the real function, capture the result.
- You send tool_result backA new user message containing the result — tagged with the matching
tool_use_id. - Claude writes the final answer
stop_reason: "end_turn"means it has everything it needs.
When does Claude need a tool?
# Each user message: should Claude reach for a tool? IF answer needs live data (weather, prices, stock): USE tool # training data is stale ELIF answer needs private data (your DB, files): USE tool # Claude never saw it ELIF answer requires precise math / code execution: USE tool # LLMs estimate; calculators compute ELIF answer requires side effects (send email, create ticket, deploy): USE tool # text alone changes nothing ELSE: JUST ANSWER # no tool, no round-trip
You don't decide this each time — Claude does, by reading your tool descriptions. Your job is to make the descriptions clear enough that Claude picks correctly.
Misconceptions
Tool use splits two jobs: Claude decides what to do, your code does it. Claude returns a structured request; you execute and return a structured result.
This separation is what makes the pattern safe. You always control what code runs, with what permissions, on what data — Claude only chooses from the menu you wrote.
Three fields. Description rules.
A tool definition is a JSON object with three fields: name (the snake_case identifier your code dispatches on), description (the plain-English explanation Claude reads to decide when to use it), and input_schema (a JSON Schema specifying parameter names, types, and which are required).
Of the three, the description matters by far the most. Claude doesn't keyword-match — it reads your descriptions semantically. "How hot is it in Tokyo?" matches a tool described as "Get current weather for a city" even though the word "weather" never appears in the question. A bad description silently degrades selection accuracy across every call.
The restaurant menu
BEFORE: Picture a menu listing only dish names — "Item A. Item B. Item C." Even an experienced diner has no idea what to order.
PAIN: A guest asks the server "I want something light and citrusy." The server can't recommend anything because the menu reveals nothing about ingredients, flavor, or portion. They guess — and guess wrong.
MAPPING: Tool descriptions are menu copy for Claude. The name is the dish title. The description tells Claude what's actually in it — what data source, what format the result comes in, when to pick this tool over another. A tool named search with description "Search" is "Item A" energy. Write descriptions like a great menu writes its dishes: precise, vivid, useful.
How Claude reads your tools
- Tools enter the system contextThe whole
toolsarray is added to Claude's context for that request — not stored anywhere persistent. - Claude scans every descriptionFor each tool, Claude evaluates "does this match the user's intent?" semantically — not by keyword.
- Best match wins (or none does)Claude either calls the best-matching tool or answers directly without one. There's no scoring you can see — just the choice.
- Schema fills the argumentsOnce a tool is chosen, Claude generates an input object that conforms to
input_schema.requiredparams must appear; types must match.
Anatomy: name (snake_case identifier) · description (semantic selector — the lever) · input_schema.properties (each with its own description!) · required (Claude must provide these).
Tool schema pseudocode
Shape every tool definition like this:
DEFINE TOOL get_weather: name: "get_weather" # snake_case id description: # the lever for selection "Get current weather for a city. Returns temperature in Celsius, condition, and humidity. Use when the user asks about today's weather, NOT forecasts." input_schema: type: "object" properties: city: type: "string" description: "City name, e.g. 'Tokyo'" required: ["city"]
Spend more time on the description than on the implementation. Include: what it does · what data source · output format · when to use vs not use.
Misconceptions
search with description "Search" tells Claude almost nothing — web? database? files? Selection accuracy collapses with vague descriptions.input_schema.properties deserves its own description with an example value. That's how Claude knows the format you expect ("Tokyo" vs "tokyo, jp" vs "35.68N, 139.69E").Description is the steering wheel. Name and schema are required, but the description decides whether Claude picks your tool over the other four in the array.
A great description names what the tool does, where the data comes from, what format the output takes, and when to use it instead of alternatives.
A while loop driven by stop_reason
The tool use loop is what turns Claude from a chatbot into an agent. In code, it's a while loop that pivots on one field: stop_reason. If the response says "tool_use", Claude is asking for a tool result — you execute the function, append both Claude's message and your tool_result to the conversation, and call the API again. If it says "end_turn", Claude is finished — you extract the text and return it to the user.
One user question can trigger 1, 5, or 10+ round-trips depending on how many tools Claude needs. Each iteration appends to the same messages array, so Claude always sees the full history of what it's already tried. The loop only exits on "end_turn" — or when your safety cap kicks in.
The detective and the runner
BEFORE: Picture a detective in an interrogation room. They can't leave the room, but they can send a runner out into the city to fetch evidence: phone records, surveillance footage, witness statements.
PAIN: The detective doesn't know up front how many trips the runner needs. After reading the phone records, a new lead might require pulling bank statements. After seeing those, maybe a witness needs to be re-interviewed. A single case may take 1 trip or 10 — the detective only stops asking when the picture is complete.
MAPPING: Claude is the detective; your code is the runner; the tools are the evidence sources around the city. stop_reason: "tool_use" means "go fetch this." stop_reason: "end_turn" means "I have enough — I can write the report now." Your while loop keeps the runner moving until the detective signals done.
One iteration, end to end
- Send messages + tools to ClaudeThe conversation history so far, plus your
toolsarray. - Inspect stop_reasonIf
"end_turn": extract text, return, done. If"tool_use": continue. - Execute every tool_use block in the responseClaude can request multiple tools in one turn — loop through them and run each.
- Append assistant message AND tool_resultsBoth go onto
messages[]. Forget either and the API errors. - Call the API againSame loop. Hit a safety cap (e.g.
max_iterations = 10) so a runaway prompt can't burn your budget.
The agent loop in pseudocode
One function. One while loop. The whole agent.
FUNCTION run_agent(user_message): messages = [{role: "user", content: user_message}] iterations = 0 WHILE iterations < MAX_ITERATIONS: response = CLAUDE.send(messages, tools) IF response.stop_reason == "end_turn": RETURN response.text # done! # stop_reason == "tool_use" tool_results = [] FOR EACH block IN response.content: IF block.type == "tool_use": result = DISPATCH(block.name, block.input) tool_results.append({ tool_use_id: block.id, content: result }) messages.append(response) # assistant turn messages.append(tool_results) # user turn iterations += 1 RAISE "max iterations exceeded"
Critical: append both the assistant response AND the tool_results before the next API call. Skip either one and the API rejects the request.
Misconceptions
stop_reason field is the only reliable signal. Scanning text for "I'm finished" or "here's your answer" is fragile and breaks across model versions and prompt changes.messages array. Claude sees every prior tool call and result, which is exactly how it knows what it's already tried and what's still missing.The loop is the agent. While stop_reason is "tool_use": execute, append, repeat. When it's "end_turn": return the text.
Always cap iterations (10 is a sane default) so a misbehaving prompt can't burn through your API budget. M12 builds on this exact pattern with the formal ReAct loop.
Errors are data, not crashes
Tools fail. APIs time out, arguments are wrong, the database is down. The worst response is to let an exception bubble up and kill the agent loop. Instead, catch the error inside your tool function, format it as a structured object like {isError: true, errorCategory: "timeout", isRetryable: true}, and return it as the tool_result.
Claude reads that result, reasons about what went wrong, and decides what to do next — tell the user, try a different tool, retry with different args, or escalate. Because the "caller" is a language model rather than your own code, your error messages need to be descriptive enough that Claude can make a good decision from them.
The intern's note back to the manager
BEFORE: An intern is sent to fetch a file from the records room. The manager (Claude) needs the file to write a report.
PAIN: The records room is locked. A bad intern walks back and stands silently, or quits in frustration — the manager has no idea what happened and the report stalls. A worse intern gives a vague mumble: "It didn't work." The manager still can't decide what to do.
MAPPING: Your tool function is the intern. Returning a clear, structured note back — "Records room locked. Need access from Bob. Probably retryable in 10 minutes." — is what lets Claude (the manager) decide: ask the user for credentials, try a different lookup, or wait and retry. That note is the tool_result. Empty results, raw stack traces, and silent crashes all break the loop.
The four failure modes
- Tool timeoutSet a per-call deadline (e.g., 10 seconds). On expiry, return
{isError: true, errorCategory: "timeout", isRetryable: true}so Claude can ask the user to try again. - Tool failureThe function ran but blew up — DB unreachable, downstream API 500. Return a human-readable message, NOT a raw stack trace. Claude needs to understand, not debug.
- Invalid argumentsClaude sometimes sends
"city": 123or omits a required field. Validate inputs first; return a helpful error explaining the format you expected. - Non-existent toolRare, but Claude can hallucinate a tool name. Catch the missing dispatch entry and return
{isError: true, errorCategory: "unknown_tool", context: "..."}instead of crashing.
Critical distinction: {isError: false, results: []} means "I checked, found nothing." {isError: true, errorCategory: "access_denied"} means "couldn't even check." Claude makes very different decisions based on which one you return.
What to return when a tool fails
FUNCTION safe_tool_call(name, args): TRY: validate(args) # type-check first result = TOOL[name](**args) RETURN {isError: false, data: result} EXCEPT ValidationError AS e: RETURN {isError: true, errorCategory: "bad_args", isRetryable: true, context: e.message} # Claude can fix & retry EXCEPT TimeoutError: RETURN {isError: true, errorCategory: "timeout", isRetryable: true} EXCEPT AuthError AS e: RETURN {isError: true, errorCategory: "access_denied", isRetryable: false, # needs human context: e.message} EXCEPT KeyError: # tool not in dispatcher RETURN {isError: true, errorCategory: "unknown_tool", isRetryable: false} EXCEPT Exception AS e: # last-resort net RETURN {isError: true, errorCategory: "internal", context: str(e)[:200]}
Notice: every branch returns — nothing raises. The agent loop keeps running; Claude reads each error and decides what to do next.
Misconceptions
tool_result. The loop keeps running; only an unhandled exception in YOUR code kills it.{results: []} tells Claude "I checked, nothing matched" — it stops looking. {isError: true, errorCategory: "access_denied"} tells Claude "I never even ran" — it asks the user for help. Conflate them and Claude makes catastrophic decisions.Errors are messages to Claude, not exceptions to your code. Catch every failure inside the tool function and return a structured object describing what went wrong.
Always include isError, errorCategory, isRetryable, and a short context string. That's enough for Claude to retry, escalate, or apologize gracefully — instead of dying mid-conversation.
Tap a card to reveal the answer
tool_use block describing which function to call and with what arguments. Your application reads the block, runs the function, and sends the output back as a tool_result. This separation keeps execution under your control and is what makes tool use safe for production.search with description "Search" gives Claude almost nothing. Spend more time writing descriptions than implementations: include what it does, the data source, the output format, and when to pick it over alternatives.stop_reason field. "tool_use" means Claude wants you to run something and call back. "end_turn" means Claude has produced the final answer. Never parse Claude's text for phrases like "I'm done" — that is unreliable across model versions and prompt changes.tool_use, what TWO things must your code append to messages[] before the next API call?tool_result objects (each tagged with the matching tool_use_id). Skip either and the API rejects the next call — Claude needs the full conversation to know where it left off.{results: []} dangerously different from {isError: true, errorCategory: "access_denied"}?access_denied error tells Claude "I couldn't even check" — so it asks for credentials or escalates. Conflating the two leads to catastrophically wrong decisions in regulated domains (auth, healthcare, finance).Open the full module on desktop
The desktop version walks through a complete multi-tool agent: runnable Python and Node.js code, animated diagrams of the loop, the full JSON Schema anatomy, structured-error patterns, and a hands-on lab where you build a working tool-using agent end to end.
Open M05 on Desktop → Full code · Multi-tool agent lab · ~60 minModule 5 of 30 · Track 2: Tool Use