Week 7: Tool Use — Giving Claude Real Actions to Take

Everything so far has been text in, text out. Tool use is the feature that turns Claude from something that describes what it would do into something that can actually trigger real code in your application — looking up live data, running a calculation, querying a database. This week covers defining a tool, the request-execute-return loop that powers it, chaining multiple tool calls together, and what makes a tool reliable versus one Claude keeps misusing.

Module 3 of 14 Week 7 of 14 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Define a tool with a JSON Schema Claude can reliably call correctly
  • Implement the full tool-use loop: request, execute, return, repeat
  • Design tools with clear scope and useful error messages, not silent failures

1. Defining Tools with JSON Schema

A tool definition tells Claude three things: a name, a description of what it does and when to use it, and a JSON Schema describing its inputs. Claude reads the description the same way a junior engineer would read a function's docstring — vague descriptions produce misuse, specific ones don't.

a tool definition
tools = [
    {
        "name": "get_weather",
        "description": (
            "Get the current weather for a specific city. Use this "
            "whenever the user asks about current conditions, temperature, "
            "or forecast for a named location."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city name, e.g. 'Kolkata' or 'Tokyo'."
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit. Default to celsius."
                }
            },
            "required": ["city"]
        }
    }
]

You pass this tools list alongside the normal messages request. Claude decides on its own whether a given user message calls for a tool at all — passing tools doesn't force their use, it just makes them available.

Description quality is the whole game

"Get weather" as a description leaves Claude guessing at when it applies and what format the city name should take. The version above states the trigger condition (current conditions/forecast requests) and gives an example format — exactly the kind of specificity from Week 2 applied to a tool instead of a prompt.

2. The Tool-Use Loop

Claude never executes a tool itself — it only ever requests that your code run one, with specific arguments. Your application executes the real function, sends the result back, and Claude continues from there. This request-execute-return cycle is the whole mechanism behind tool use.

the loop, in Python
messages = [{"role": "user", "content": "What's the weather in Kolkata right now?"}]

response = client.messages.create(
    model="claude-sonnet-4-5", max_tokens=1024, tools=tools, messages=messages
)

while response.stop_reason == "tool_use":
    messages.append({"role": "assistant", "content": response.content})

    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = run_the_real_tool(block.name, block.input)  # YOUR code
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": str(result),
            })

    messages.append({"role": "user", "content": tool_results})
    response = client.messages.create(
        model="claude-sonnet-4-5", max_tokens=1024, tools=tools, messages=messages
    )

print(response.content[0].text)  # the final, tool-informed answer

Note the tool_use_id — it links a specific result back to the specific call that requested it, which matters once there's more than one tool call to track. The loop keeps going as long as Claude keeps asking for more tools; it ends when stop_reason comes back as end_turn instead.

The security boundary lives in "run_the_real_tool"

Claude decides WHAT to call and with WHAT arguments — your code decides whether that call is actually allowed to happen. Never skip validation on tool inputs just because "Claude requested it"; treat them like any other untrusted input to your system.

3. Chaining & Parallel Tool Calls

A single Claude turn can request more than one tool call at once — asking for the weather in three cities produces three tool_use blocks in one response, which your loop should execute and return together. Multi-step tasks (search, then calculate using the search result) chain naturally: each pass through the loop can trigger a different tool based on what came before.

handling multiple tool_use blocks in one response
# response.content might contain THREE tool_use blocks in one turn:
# [tool_use(get_weather, city="Kolkata"),
#  tool_use(get_weather, city="Tokyo"),
#  tool_use(get_weather, city="Berlin")]
#
# Your loop already handles this correctly because it iterates over
# EVERY block in response.content, not just the first one — execute
# each, collect all three tool_results, and send them back together.

This is a strong argument for making the loop generic (iterate over all blocks) rather than hardcoding "expect exactly one tool call" — real usage very quickly produces multi-tool turns once you give Claude more than one capability at a time.

This is the exact mechanism behind agents

An "agent" (Week 13) is largely just this loop, run for longer, with more tools and less human review of each step. Everything you're building this week is the engine underneath every agent example you'll see later in the course.

4. Designing Tools Claude Can Use Reliably

A few concrete habits separate tools Claude uses correctly from ones it keeps misusing:

design habits that actually matter
# 1. Narrow, single-purpose tools beat one giant do-everything tool
BAD:  manage_database(action, table, data)   # "action" hides 5 behaviors
GOOD: get_record(table, id) / create_record(table, data) / ...

# 2. Return structured, informative errors — not silent failures
BAD:  return None   # Claude has no idea it failed or why
GOOD: return {"error": "city not found", "suggestion": "check spelling"}

# 3. Descriptions state WHEN to use the tool, not just what it does
BAD:  "Searches the database."
GOOD: "Searches the customer database by email or name. Use this before
       claiming you don't have information about a specific customer."

Informative errors matter more than they seem: if a tool fails silently, Claude has no signal to try a different approach and may confidently report success anyway. A clear error message gives Claude something to react to — retry with different arguments, try a different tool, or tell the user what actually went wrong.

Never give Claude a raw shell/SQL execution tool

A tool like run_shell_command(cmd) or run_sql(query) with no validation hands over your whole system's blast radius to whatever Claude generates. Always narrow to specific, safe operations — this exact tension is why Claude Code's permission model (Week 10) exists at all.

5. Hands-on Exercise

Hands-on

Build a working tool-use loop with a real function behind it

Wire up a genuine round trip, not a mocked example.

Part 1 — One tool, one round trip:

  1. Write one real Python or TypeScript function (a calculator, a unit converter, a lookup against a small hardcoded dataset).
  2. Define its tool schema and send a message that should trigger it.
  3. Confirm Claude requests the tool with the arguments you expect, execute it, and print the final answer after sending the result back.
Hint

Print response.stop_reason and the raw tool_use block at each step while you're building this — seeing exactly what Claude requested makes debugging the loop far easier than guessing.

Part 2 — Make the loop actually loop:

  1. Add a second tool and craft a request that plausibly needs both in sequence (look something up, then compute something from it).
  2. Confirm your while loop correctly handles more than one pass before reaching end_turn.

Part 3 — Break it on purpose:

  1. Make one tool return a structured error for a bad input (e.g. an unknown city).
  2. Trigger that error case and observe how Claude's final answer changes because of the error message vs. a silent empty result.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Does Claude ever execute a tool directly? What does it actually do instead?

No — Claude only requests that a tool be called, with specific arguments, via a tool_use block. Your own application code is responsible for actually executing the function and returning the result.

Q2

What does tool_use_id do, and why does it matter more once you have multiple tool calls?

It links a specific tool_result back to the specific tool_use call that requested it. With a single tool call it's trivial, but once a turn requests multiple tool calls at once, this id is how Claude (and your code) knows which result answers which request.

Q3

Why is a structured error message better than a silent failure (returning nothing) from a tool?

A silent failure gives Claude no signal that anything went wrong — it may confidently report success anyway. A structured error gives Claude something concrete to react to: retry with different arguments, try another tool, or accurately tell the user what happened.

Q4

Why is a narrow, single-purpose tool generally better than one large tool with an "action" parameter?

A single "action" parameter hides multiple distinct behaviors behind one ambiguous interface, making it harder for Claude's description-driven decision of when and how to call it to be reliable. Separate, narrowly-scoped tools with clear individual descriptions are much easier for Claude to use correctly.