Claude Certification Program · v1.0 · Effective July 2026 · All four tracks open

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

Traveller asks for a flight
The model answers from memory
Prices are a guess

With tools

Traveller asks for a flight
Claude requests search_flights
Your code queries the airline
Claude answers from live fares
Without tools the model can only describe flights from memory. With tools it asks your code to fetch live fares and answers from them.

8.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.

  1. SEND. Your request carries a tools array. Each entry has a name, a description and an input_schema, which describes the arguments in JSON Schema, the standard format for describing the shape of JSON. With the default tool_choice of auto, Claude decides whether any tool is needed.
  2. REQUEST. If one is, the response has stop_reason: "tool_use" and a tool_use block. The block holds a unique id, the tool's name and an input object shaped by your schema, such as {"origin_airport": "AMS", "destination_airport": "LIS", "departure_date": "2026-10-09"}.
  3. 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.
  4. RETURN. Your next request appends Claude's reply, then a user message with a tool_result block. Its tool_use_id repeats the id, and its content carries the fares. Claude then asks for another tool, such as hold_seat, or answers the traveller.

One tool call, start to finish

SENDthe conversation plus tools
REQUESTa tool_use block: id, name, input
EXECUTEyour code calls the airline API
RETURNa tool_result with the same id
RETURN → SEND · while stop_reason is tool_use
Claude only requests the call. Your code runs it against the real system and returns the result, matched to the request by its id.

Think 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: true with an instructive message, so "no results" and "the call failed" stay distinct.
  • ✗ Asking for approval in the system prompt, or through a confirmed parameter. ✓ Gate irreversible tools in the harness, with the user's explicit yes collected by your application.
  • ✗ Writing handlers or tool_result blocks for server tools. ✓ Let Anthropic run web_search and the other server tools; dispatch only client tool_use blocks.
  • ✗ 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

"Always confirm" in the prompta request, not a control
A confirmed parameterClaude fills it in
A more capable modelstill a probability
Remove the toolthe feature is gone
Gate it in the harnessthe traveller's yes, from your UI
Prompt wording, a model-filled flag and a bigger model all leave the decision with the model, and removing the tool loses the feature; only a gate in your code guarantees a human yes.

8.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_use block with stop_reason: "tool_use", your code runs the call, and you send a tool_result with the matching tool_use_id in 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: true with 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 as web_search run 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.

Sources