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.
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.
"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.
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.
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.
# 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.
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:
# 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.
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
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:
- Write one real Python or TypeScript function (a calculator, a unit converter, a lookup against a small hardcoded dataset).
- Define its tool schema and send a message that should trigger it.
- Confirm Claude requests the tool with the arguments you expect, execute it, and print the final answer after sending the result back.
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:
- Add a second tool and craft a request that plausibly needs both in sequence (look something up, then compute something from it).
- Confirm your
whileloop correctly handles more than one pass before reachingend_turn.
Part 3 — Break it on purpose:
- Make one tool return a structured error for a bad input (e.g. an unknown city).
- 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?
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?
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?
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?
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.