Home › Study guides › CCDV-F › Domain 8 › Lesson 8.1
CCDV-F · Domain 8 · 10.6% of the exam · Lesson 8.1 · 22 min read
Implementing tools: definitions, dispatch, errors and approvals
How Claude calls your code: the tool_use round trip, descriptions and schemas, is_error results, client versus server tools, and approval gates.
Written against skill 8.1 of the official CCDV-F exam guide (Version 1.0, effective July 2026). An independent resource, not affiliated with Anthropic; the practice questions are written from scratch.
8.1.1 Why a chat model cannot book you a flight
Picture the request every travel site hopes for: "Find me a flight from Amsterdam to Lisbon next Friday, under 200 euros, and book the cheapest one." A good travel agent looks up live fares, holds a seat while you decide, and charges your card only once you say yes. If the fare system is slow, they tell you and try again. They never invent a flight.
A language model on its own can do none of that. It can write a convincing paragraph about flights to Lisbon, but every price in it comes from training data, not from today's seats. It cannot reach an airline's reservation system, let alone charge a card.
Solveig, the only back-end developer at Faretrail, a small online travel agency, is building this assistant for Pieter, who runs the product. The mechanism that closes the gap is tool use, also called function calling. You describe functions to Claude: their names, what they do and the inputs they take. Claude decides which function would help and replies with a structured request to call it; your code runs the real function and hands back the result. Faretrail's assistant gets three tools: search_flights, hold_seat and book_flight, which takes payment.
That split of work is the whole lesson. Claude sees only your descriptions and the results you return. Everything else is yours: the connection to the airline, what happens when the search API times out, and who must say yes before money moves.
A plain chat call versus a call with tools
Plain call text in, text out
With tools
search_flights8.1.2 The round trip: tool_use out, tool_result back
Here is the question that trips up developers new to LLMs: when Claude "calls" search_flights, what actually runs, and where? Claude never executes anything on its own. It emits a structured request and waits, and your code does the rest in four steps.
- SEND. Your request carries a
toolsarray. Each entry has aname, adescriptionand aninput_schema, which describes the arguments in JSON Schema, the standard format for describing the shape of JSON. With the defaulttool_choiceofauto, Claude decides whether any tool is needed. - REQUEST. If one is, the response has
stop_reason: "tool_use"and atool_useblock. The block holds a uniqueid, the tool'snameand aninputobject shaped by your schema, such as{"origin_airport": "AMS", "destination_airport": "LIS", "departure_date": "2026-10-09"}. - EXECUTE. Your code finds the handler, the function you wrote for that tool name, and it calls the airline partner's API with those inputs.
- RETURN. Your next request appends Claude's reply, then a user message with a
tool_resultblock. Itstool_use_idrepeats theid, and itscontentcarries the fares. Claude then asks for another tool, such ashold_seat, or answers the traveller.
One tool call, start to finish
toolstool_use block: id, name, inputtool_result with the same idThink of a waiter and a kitchen. The waiter writes the order on a ticket; the kitchen cooks it and sends the plate out with the ticket number. The waiter never touches the stove. The API enforces the ticket rules. The tool_result must arrive in the very next user message, with the result blocks before any text. When Claude asks for two tools in one reply, both results go back in that one message.
Step 3 is also where you configure the tool for an external system. Every detail of that connection lives in the handler: the partner's base URL and the timeout, the API key loaded from an environment variable or a secret manager, and the mapping from Claude's inputs to the partner's format. None of it belongs in the description or the prompt, and Claude never needs it: Solveig can switch partners or rotate keys without Claude noticing, as long as the contract holds.
8.1.3 Descriptions, schemas and a small tool set
Solveig's first definitions were one-liners. search_flights said "Searches flights" and took parameters named from, to and date. In testing, Claude passed "Amsterdam" where the partner wanted the airport code AMS. Asked only to compare prices, it once called hold_seat too, because nothing said that a hold blocks a seat for 20 minutes. Claude used everything it had been told, which was almost nothing.
The definition is all Claude knows about your tool, and Anthropic's documentation calls detailed descriptions by far the most important factor in tool performance. Write it like a brief to a new colleague: what the tool does, when to use it and when not, what each parameter means, and what it does not return. Aim for at least three or four sentences. Name parameters unambiguously (origin_airport, not from), use an enum for closed sets, and list required fields in required.
Here is the rewrite. Look at the description, which says when to use the tool and what it will not do, and at the parameter descriptions that name the exact format. The strict line makes every call's input match the schema, which then needs additionalProperties: false.
search_flights = {
"name": "search_flights",
"description": (
"Search live one-way fares from Faretrail's airline partners. Use it whenever the "
"traveller asks about availability or prices; never quote a fare from memory. "
"Returns up to 10 offers sorted by price, each with an offer_id that hold_seat needs. "
"It does not hold or book a seat, and it does not cover trains or buses."), # when, and when NOT
"input_schema": {
"type": "object",
"properties": {
"origin_airport": {"type": "string", "description": "IATA code, e.g. AMS"},
"destination_airport": {"type": "string", "description": "IATA code, e.g. LIS"},
"departure_date": {"type": "string", "format": "date", "description": "YYYY-MM-DD"},
"max_price_eur": {"type": "number", "description": "Upper limit, in euros"},
},
"required": ["origin_airport", "destination_airport", "departure_date"],
"additionalProperties": False,
},
"strict": True, # every input matches the schema
}
Notice what strict: true does not promise. It guarantees shape, so departure_date is always a date, but not sense: the date may be in the past. That check stays in your handler. For inputs that are hard to describe, the optional input_examples field shows Claude a few valid calls.
The same thinking applies to the whole tool set. Claude chooses from a menu, and every definition is sent, and billed as input tokens, on every request. More tools do not mean better results; overlapping ones make the choice ambiguous. The partner API has a dozen endpoints, but Solveig does not wrap each one: her search_flights handler calls the fare and availability endpoints and returns only the fields Claude needs next. Build a few tools with distinct purposes, shaped around the traveller's tasks. Grouping related operations behind a typed action parameter is fine; a vague catch-all that takes free text is not. When tools span several services, a prefix per service, such as github_ or slack_, keeps the menu clear.
8.1.4 When the search times out: errors Claude can act on
At peak times the partner's search API times out a few times an hour. Solveig's first handler failed in two ways. When the exception escaped, the loop crashed and the traveller saw an error page. When she caught it and returned an empty list, Claude said "There are no flights to Lisbon on Friday", which was false. Claude reasons only about what the result says, and an empty success looks exactly like "no flights exist".
The API has a flag for this. Set is_error: true on the tool_result and explain the failure in its content. Claude reads both and adapts: it can retry, correct its input or tell the traveller honestly what happened. Make the message instructive. Anthropic's documentation contrasts a bare "failed" with a message that says what went wrong and what to try next. Retrying a timeout once inside the handler is sound engineering; an endless retry loop that keeps the traveller waiting is not.
| What happened | What the handler returns | What Claude can do next |
|---|---|---|
| Search timed out twice | is_error: true, "Flight search timed out; the partner is slow. Safe to retry in a minute." |
Retry once, or tell the traveller results are delayed |
| Date in the past | is_error: true, "departure_date 2025-10-09 is in the past. Confirm the year with the traveller." |
Ask the traveller, then call again |
| No flights match | A normal result: "0 offers under 200 EUR on 2026-10-09; 3 offers on 2026-10-10." | Say so truthfully and suggest the next day |
| Card declined at booking | is_error: true, "Card declined by the issuer. Do not retry this card; ask for another payment method." |
Stop and ask the traveller |
Memorise the third row. "No results" is a valid answer and "the search failed" is a failure, so the two must look different to Claude. Keep stack traces, authorization headers and internal hostnames in your logs; Claude gets a short, safe explanation it can act on.
Invalid calls work the same way. Return an is_error result that names the missing or wrong parameter, and Claude will usually retry with a correction. If the same mistake keeps coming back, fix the description.
8.1.5 Client-side or server-side: who runs the tool
Pieter now wants the assistant to answer "Is there a strike at Lisbon airport on Friday?", so Solveig enables Anthropic's web search tool. Then she wonders: does dispatch now need a web_search handler, the way search_flights has one? It does not, and the reason is the biggest difference between tools: where their code runs.
A client tool runs in your application. That covers your own tools, like search_flights, and Anthropic-schema tools such as bash and text_editor: Anthropic publishes their schemas and trains Claude on them, but your code still does the work. Either way you receive a tool_use block and owe a tool_result. A server tool runs on Anthropic's infrastructure, so it reaches only what Anthropic's machines can reach, such as the public web, and never Faretrail's partner API. You enable it in tools with a versioned type, and the response shows a server_tool_use block followed by its result block. You never write a handler or a tool_result for it.
| Kind of tool | Examples | Who executes, and what you send back |
|---|---|---|
| User-defined client tool | search_flights, book_flight |
Your code; you return a tool_result |
| Anthropic-schema client tool | bash, text_editor, memory |
Your code, with a schema Claude was trained on; you return a tool_result |
| Server tool | web_search, web_fetch, code_execution, tool_search |
Anthropic; results arrive in the response, no tool_result from you |
Memorise the last column: it tells your dispatcher which blocks are its job. Recognise the middle row.
Server tools run their own loop inside Anthropic's platform, so one request may trigger several searches before the response reaches you. If that loop hits its iteration limit, the response has stop_reason: "pause_turn". You append the paused response, send the request again, and Claude continues where it stopped. You configure a server tool in its tools entry, not in a handler: web search, for example, accepts max_uses to cap searches per request, and allowed_domains or blocked_domains to limit the sites it reaches.
8.1.6 The harness: dispatch every call, gate the risky ones
Who decides that book_flight needs the traveller's yes? Pieter's first idea was a line in the system prompt: "Always confirm before booking." That is a request to the model. Claude will usually follow it, but "usually" is not a policy for spending someone's money.
The harness is the code around the model that turns requests into actions: your loop and what it calls. Its dispatch step takes each tool_use block, looks up the handler by name and returns exactly one tool_result. Every call passes through it, so rules that must always hold live there: validation, logging and approval. Solveig sorts the tools by what a mistake would cost. The search only reads, so it runs at once. A hold is free and expires after 20 minutes, so it runs at once too. A booking charges a card and cannot be undone, so it waits for a person.
Here is the dispatch function. Look at NEEDS_APPROVAL and the ask_traveller gate, the declined branch that still returns a tool_result, and the is_error branch from the error table.
HANDLERS = {"search_flights": search_flights_api, "hold_seat": hold_seat_api,
"book_flight": book_flight_api}
NEEDS_APPROVAL = {"book_flight"} # irreversible: money moves
def result(block, text, is_error=False):
return {"type": "tool_result", "tool_use_id": block.id, "content": text, "is_error": is_error}
def dispatch(block): # one tool_use in, one tool_result out
handler = HANDLERS.get(block.name)
if handler is None:
return result(block, f"No tool named {block.name}.", is_error=True)
if block.name in NEEDS_APPROVAL and not ask_traveller(block.input): # a person, in your UI
return result(block, "The traveller declined this booking. Ask what they want to change.")
try:
return result(block, json.dumps(handler(**block.input))) # calls the partner API
except ToolFailure as err: # timeouts, declines: raised by handlers
return result(block, err.instructive_message, is_error=True)
# In the loop: one result per client tool call, all in the next user message
results = [dispatch(b) for b in response.content if b.type == "tool_use"]
The gate shows the exact input Claude produced, such as the flight, the price and the card's last four digits, so the traveller approves precisely what will run. In a web app the pause is not a blocking prompt. You store the conversation with its unanswered tool_use, show a Confirm button, and resume with the tool_result when the traveller answers. The yes must come from the person. Think of a card terminal in a shop: the cashier can key in any amount, but only the customer's PIN completes the payment. A confirmed: true parameter is the cashier typing the PIN, because Claude is the one who fills it in.
You do not always write this loop by hand. The SDKs' tool runner, in beta, runs it for you, but its documentation sends you to a manual loop when you need human approval. The Claude Agent SDK has a built-in gate: a canUseTool callback (can_use_tool in Python). It fires for any tool call your permission rules have not already approved, and it pauses that call until your application returns allow or deny. Claude sees the denial message and can change course.
8.1.7 The exam traps
Almost every mistake in this skill puts a rule in the wrong place: in the prompt instead of the definition, in the model's judgement instead of your code, or in an empty result instead of an error.
- ✗ One-line descriptions and vague parameter names. ✓ Detailed descriptions (what, when, when not, what comes back) and typed, unambiguous parameters. Claude has nothing else to choose by.
- ✗ One tool per API endpoint, or one catch-all tool that takes free text. ✓ A few tools with distinct purposes, shaped around tasks, each returning only what Claude needs.
- ✗ Returning an empty result, or letting the exception crash the loop, when the external API fails. ✓ Return
is_error: truewith an instructive message, so "no results" and "the call failed" stay distinct. - ✗ Asking for approval in the system prompt, or through a
confirmedparameter. ✓ Gate irreversible tools in the harness, with the user's explicit yes collected by your application. - ✗ Writing handlers or
tool_resultblocks for server tools. ✓ Let Anthropic runweb_searchand the other server tools; dispatch only clienttool_useblocks. - ✗ Putting API keys or connection details in the prompt or the tool description. ✓ Keep them in the handler, loaded from the environment or a secret manager.
Four ways to guard the booking tool, one that works
confirmed parameterClaude fills it in8.1.8 Put it together: build the booking assistant and break it
You now have every piece: the round trip, definitions Claude can choose from, errors it can act on, client versus server tools, and a harness that gates the risky calls. The fastest way to make them stick is to build a small version and break each part on purpose.
The rest of this domain builds on these calls. MCP servers (8.2) package tools like these behind a standard protocol so several applications can share one implementation. Choosing between built-in tools, custom tools, Skills and MCP (8.3) is the decision one level up. Hooks (7.3) run checks like your approval gate before a tool call in Claude Code and the Agent SDK, and treating tool results as untrusted input belongs to application security (7.1).
Key takeaways
- ✓ Tool use is a round trip: Claude returns a
tool_useblock withstop_reason: "tool_use", your code runs the call, and you send atool_resultwith the matchingtool_use_idin the next user message. - ✓ Connection details for external systems (URLs, keys, timeouts) live in your handler; Claude sees only the schema and the results.
- ✓ Descriptions are the biggest lever on tool performance: say what the tool does, when to use it and when not, and what it returns, with typed parameters and a small set of distinct tools.
- ✓ A failed call returns
is_error: truewith an instructive message; an empty success is reserved for "there really were no results". - ✓ Client tools run in your code and need a
tool_result; server tools such asweb_searchrun on Anthropic's infrastructure and return finished results. - ✓ The harness dispatches every call, so it is where irreversible tools wait for a person's explicit yes; a prompt instruction or a model-filled flag is not approval.
Check your understanding
4 questions written for this lesson, then one from the CCDV-F question bank on the same topic. Every answer option is explained, including the ones you did not pick. Nothing is stored.
18 CCDV-F questions on Domain 8, free
Every question in the bank is tagged to a domain, so you can drill 18 questions on Tools and MCPs alone, or sit the full 53-question timed simulator.
Open the CCDV-F question bank → Back to Domain 8 →
The question bank is free. It asks for an account only because the quiz engine has to store answers to score them and show which domains are weak. The questions on this page need nothing.