Module 5 of 30
Function Calling
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.

Track 2: Tool Use ⏱ ~12 min read 5 / 30
Concept 1 of 4

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.

Concept 1 of 4

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.

Concept 1 of 4

The six-step round-trip

  1. User asks a question"What's the weather in Tokyo?" arrives at your app.
  2. You send message + tools to ClaudeThe API call includes your tools array describing what's available.
  3. Claude returns a tool_use blockInstead of text, the response carries {name: "get_weather", input: {city: "Tokyo"}} and stop_reason: "tool_use".
  4. YOUR code executes the functionYou read the block, validate, run the real function, capture the result.
  5. You send tool_result backA new user message containing the result — tagged with the matching tool_use_id.
  6. Claude writes the final answerstop_reason: "end_turn" means it has everything it needs.
Concept 1 of 4

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.

Concept 1 of 4

Misconceptions

"Claude actually executes the function."
No. Claude returns a JSON block that says "I'd like to call this function with these arguments." Your code reads it, decides whether to honor it, runs the function, and sends back the result. Claude is the decision-maker; your application is the executor.
"More tools = a more capable agent."
The opposite, often. Tool selection accuracy starts to degrade past ~5-6 tools in one call — more descriptions to weigh, more chances to pick the wrong one, slower responses. M06 covers strategies for managing many tools.

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.

Concept 2 of 4

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.

Concept 2 of 4

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.

Concept 2 of 4

How Claude reads your tools

  1. Tools enter the system contextThe whole tools array is added to Claude's context for that request — not stored anywhere persistent.
  2. Claude scans every descriptionFor each tool, Claude evaluates "does this match the user's intent?" semantically — not by keyword.
  3. 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.
  4. Schema fills the argumentsOnce a tool is chosen, Claude generates an input object that conforms to input_schema. required params 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).

Concept 2 of 4

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.

Concept 2 of 4

Misconceptions

"The name is the label — that's what Claude picks on."
Names help, but Claude leans on the description. A tool named search with description "Search" tells Claude almost nothing — web? database? files? Selection accuracy collapses with vague descriptions.
"Properties don't need their own descriptions — the name is obvious."
They do. Each property in 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.

Concept 3 of 4

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.

Concept 3 of 4

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.

Concept 3 of 4

One iteration, end to end

  1. Send messages + tools to ClaudeThe conversation history so far, plus your tools array.
  2. Inspect stop_reasonIf "end_turn": extract text, return, done. If "tool_use": continue.
  3. Execute every tool_use block in the responseClaude can request multiple tools in one turn — loop through them and run each.
  4. Append assistant message AND tool_resultsBoth go onto messages[]. Forget either and the API errors.
  5. Call the API againSame loop. Hit a safety cap (e.g. max_iterations = 10) so a runaway prompt can't burn your budget.
Concept 3 of 4

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.

Concept 3 of 4

Misconceptions

"I can just parse Claude's text to know when it's done."
Never. The 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.
"Each loop iteration is a fresh conversation."
No — each iteration appends to the SAME 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.

Concept 4 of 4

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.

Concept 4 of 4

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.

Concept 4 of 4

The four failure modes

  1. 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.
  2. 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.
  3. Invalid argumentsClaude sometimes sends "city": 123 or omits a required field. Validate inputs first; return a helpful error explaining the format you expected.
  4. 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.

Concept 4 of 4

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.

Concept 4 of 4

Misconceptions

"If a tool fails, the agent crashes — that's just how it is."
Only if you let it. Catch exceptions inside every tool function, format as structured errors, return them as tool_result. The loop keeps running; only an unhandled exception in YOUR code kills it.
"An empty results array and a permission failure look the same."
They must NOT. {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

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 min

Module 5 of 30 · Track 2: Tool Use