Lesson 2 - Defining Tools and Schemas
Welcome to Defining Tools and Schemas
In Lesson 1 you saw the whole tool-use loop in miniature: the model asked for a tool, you ran it, it answered. But that loop only works if the model can understand the tools you offer. A tool definition is the entire contract between you and the model — get it right and the model calls the correct function with valid arguments; get it vague and the model guesses, calls the wrong tool, or doesn’t call anything at all.
In this lesson you’ll write definitions the model can actually use, watch how a single word in a description changes its behavior, and learn to take direct control of when it reaches for a tool.
By the end of this lesson, you will be able to:
- Write a clear
name, a prescriptivedescription, and a preciseinput_schema - Explain why the description is the part the model reads to decide when to call
- Build richer schemas with typed properties,
enum, arrays, andrequired - Offer multiple tools and steer selection with
tool_choice
You’ll reuse the SDK setup from Lesson 1. Let’s begin.
The Three Parts of a Tool
Every tool definition is a small dictionary with exactly three keys. You met them in Lesson 1; now we’ll treat each as a craft.
{
"name": "...", # what the function is called
"description": "...", # what it does and WHEN to use it
"input_schema": {...}, # the shape of its arguments (JSON Schema)
}The model never sees your actual code — only this definition. So these three fields have to carry everything: enough for the model to decide whether the tool is relevant, and enough for it to fill in correct arguments. Think of it as documentation written for a very literal reader who will act on exactly what you say.
The name
The name identifies the function. Make it a clear, verb-like phrase in snake_case: get_weather, create_calendar_event, lookup_order. A good name reads like the action it performs. Avoid bare nouns (weather) and vague labels (tool_a) — the name is the first hint the model gets about what the tool is for.
The Description Decides When
Here is the field beginners underestimate the most: the description. The model reads it to decide whether this tool is the right one for the user’s request. A description that only says what the function does leaves the model guessing about when to use it. A prescriptive description — one that spells out the situations the tool is for — removes the guesswork.
Let’s prove it. We’ll define the same two tools twice, changing nothing but the descriptions, and ask the same question. First, the vague version — names that say nothing, descriptions that say nothing:
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from your environment
vague_tools = [
{"name": "tool_a", "description": "Does a thing.",
"input_schema": {"type": "object",
"properties": {"q": {"type": "string"}}, "required": ["q"]}},
{"name": "tool_b", "description": "Does another thing.",
"input_schema": {"type": "object",
"properties": {"q": {"type": "string"}}, "required": ["q"]}},
]
question = "Where is my order A1043?"
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=300, tools=vague_tools,
messages=[{"role": "user", "content": question}],
)
picked = next((b.name for b in resp.content if b.type == "tool_use"), None)
print("vague:", resp.stop_reason, "picked:", picked)vague: end_turn picked: NoneWith nothing to go on, the model can’t tell which tool fits the request — so it calls neither. The stop_reason is end_turn, not tool_use. Now the prescriptive version. Same names, same schemas, same question — only the descriptions change, and each one says when to use the tool:
prescriptive_tools = [
{"name": "tool_a",
"description": "Search the product catalog for items to buy. Use this when the user wants to find or browse products.",
"input_schema": {"type": "object",
"properties": {"q": {"type": "string", "description": "Search terms."}}, "required": ["q"]}},
{"name": "tool_b",
"description": "Look up the status of an order the user already placed. Use this when the user asks where their order is.",
"input_schema": {"type": "object",
"properties": {"q": {"type": "string", "description": "The order ID, e.g. 'A1043'."}}, "required": ["q"]}},
]
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=300, tools=prescriptive_tools,
messages=[{"role": "user", "content": question}],
)
picked = next((b.name for b in resp.content if b.type == "tool_use"), None)
print("prescriptive:", resp.stop_reason, "picked:", picked)prescriptive: tool_use picked: tool_bSame model, same question, opposite outcome. The prescriptive descriptions let the model recognize that “Where is my order” is an order status request and call tool_b — the right tool — confidently. The lesson is blunt: write descriptions that say when to use the tool, not just what it does. Start them with a phrase like “Use this when the user asks about…” and name the situations out loud.
Describe the trigger, not just the action
A good rule of thumb: if you removed the tool’s name entirely, could the model still tell from the description alone when to call it? If not, the description is too thin. Spell out the user requests the tool is meant to handle.
The Input Schema
The input_schema describes the arguments your function accepts, using JSON Schema — a standard, language-neutral way to describe the shape of data. The model uses it to construct valid arguments. The more precise your schema, the more reliably the model fills it in.
A schema is an object with properties (the arguments), a description on each property, and a required list naming the arguments that must be present.
Types, descriptions, and required
Here is a richer tool — create_calendar_event — with four typed properties. Notice that strings, integers, and arrays each declare their type, and every property carries its own description so the model knows exactly what to put there:
calendar_tools = [{
"name": "create_calendar_event",
"description": "Create a calendar event. Use this when the user asks to schedule or book a meeting or appointment.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "Short event title."},
"date": {"type": "string", "description": "Event date as YYYY-MM-DD."},
"duration_minutes": {"type": "integer", "description": "Length of the event in minutes."},
"attendees": {"type": "array", "items": {"type": "string"},
"description": "Email addresses of the people to invite."},
},
"required": ["title", "date", "duration_minutes"],
},
}]
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=400, tools=calendar_tools,
messages=[{"role": "user", "content":
"Schedule a 45-minute project kickoff on 2026-07-09 with "
"[email protected] and [email protected]."}],
)
print("stop_reason:", resp.stop_reason)
for block in resp.content:
if block.type == "tool_use":
print("input:", block.input)stop_reason: tool_use
input: {'title': 'Project Kickoff', 'date': '2026-07-09', 'duration_minutes': 45, 'attendees': ['[email protected]', '[email protected]']}Look at what the model produced. It pulled a clean title out of the sentence, formatted the date exactly as the description asked (YYYY-MM-DD), set duration_minutes to the integer 45 rather than the string "45 minutes", and gathered both email addresses into the attendees array. Each property’s description and type did real work — they’re what kept the model from handing you a malformed event.
The schema toolbox
A few schema features you’ll reach for constantly:
type—string,integer,number(decimals allowed),boolean,array, orobject. Useintegerfor whole counts andnumberfor amounts like19.99; getting this right means the model returns45, not"45".- A
descriptionon every property — this is where you tell the model the expected format (YYYY-MM-DD), units (minutes), or an example ('A1043'). Don’t skip it. required— list only the arguments the function truly needs. Anything left out is optional, and the model may omit it.enum— restrict a property to a fixed set of choices, e.g."enum": ["celsius", "fahrenheit"]. The model can only pick a listed value, which eliminates a whole class of invalid inputs.- Arrays and nested objects —
arraywith anitemstype (as inattendees), or a nestedobjectwith its ownproperties, lets you model structured inputs like a list of line items.
Offering Multiple Tools
Real applications give the model a set of tools and let it choose. With well-written descriptions, selection is exactly what you’d hope: the model reads the question, matches it to the right tool, and ignores the rest. Let’s hand it three unrelated tools and ask three different questions:
multi_tools = [
{"name": "get_weather",
"description": "Get the current weather for a city. Use this for questions about temperature, rain, or conditions.",
"input_schema": {"type": "object",
"properties": {"city": {"type": "string", "description": "City name."}}, "required": ["city"]}},
{"name": "convert_currency",
"description": "Convert an amount of money from one currency to another. Use this for exchange-rate questions.",
"input_schema": {"type": "object",
"properties": {
"amount": {"type": "number", "description": "Amount to convert."},
"from_currency": {"type": "string", "description": "3-letter source currency code."},
"to_currency": {"type": "string", "description": "3-letter target currency code."}},
"required": ["amount", "from_currency", "to_currency"]}},
{"name": "create_calendar_event",
"description": "Create a calendar event. Use this when the user asks to schedule or book a meeting.",
"input_schema": {"type": "object",
"properties": {
"title": {"type": "string", "description": "Event title."},
"date": {"type": "string", "description": "Date as YYYY-MM-DD."},
"duration_minutes": {"type": "integer", "description": "Length in minutes."}},
"required": ["title", "date", "duration_minutes"]}},
]
for q in ["What's the weather in Cairo right now?",
"How much is 200 euros in US dollars?",
"Book a 30-minute design review on 2026-07-02."]:
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=400, tools=multi_tools,
messages=[{"role": "user", "content": q}],
)
picked = next((b.name for b in resp.content if b.type == "tool_use"), None)
print(f"{q}\n -> {picked}")What's the weather in Cairo right now?
-> get_weather
How much is 200 euros in US dollars?
-> convert_currency
Book a 30-minute design review on 2026-07-02.
-> create_calendar_eventThree for three. Each question routed to its matching tool. This is the payoff of prescriptive descriptions: the model uses them as a menu, reading each one to find the best fit. The clearer your descriptions, the cleaner the routing — and the easier your code becomes, because you can trust that the tool the model picked is the one the user actually needed.
Controlling Selection with tool_choice
By default the model decides for itself whether and which tool to call. Sometimes you want to override that — force a tool, require some tool, or block tools entirely. That’s the job of the tool_choice parameter. It takes one of four shapes:
tool_choice value | Behavior |
|---|---|
{"type": "auto"} | The model decides whether to use a tool (this is the default). |
{"type": "any"} | The model must use one of the available tools. |
{"type": "tool", "name": "..."} | The model must use the named tool. |
{"type": "none"} | The model cannot use any tool. |
Let’s see the forcing options in action. We’ll ask a currency question but override what the model is allowed to do:
money_q = [{"role": "user", "content": "How much is 200 euros in US dollars?"}]
for choice in [{"type": "auto"},
{"type": "any"},
{"type": "tool", "name": "get_weather"}]:
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=400, tools=multi_tools,
tool_choice=choice, messages=money_q,
)
picked = next((b.name for b in resp.content if b.type == "tool_use"), None)
print(f"{choice} -> picked: {picked}"){'type': 'auto'} -> picked: convert_currency
{'type': 'any'} -> picked: convert_currency
{'type': 'tool', 'name': 'get_weather'} -> picked: get_weatherWith auto and any the model sensibly picks convert_currency — the right tool for a money question. But look at the third line: {"type": "tool", "name": "get_weather"} forced the model to call get_weather even though it makes no sense here. That’s the point — forcing a specific tool overrides the model’s judgment entirely. Use it when you already know which function must run.
Finally, {"type": "none"} takes tools off the table. The model answers from its own knowledge instead of reaching for a function:
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=200, tools=multi_tools,
tool_choice={"type": "none"},
messages=[{"role": "user", "content":
"Name one good way to dress for cold weather. One sentence."}],
)
print("stop_reason:", resp.stop_reason)
print(next(b.text for b in resp.content if b.type == "text"))stop_reason: end_turn
Wear multiple layers, starting with moisture-wicking base layers, adding an
insulating middle layer, and finishing with a windproof and waterproof outer jacket.The stop_reason is end_turn — the model never even considered the tools. tool_choice gives you a precise dial: from “decide for yourself” all the way to “no tools at all.”
When to force, when to let go
Leave tool_choice at its default auto for most apps — well-described tools route themselves. Reach for tool/any when a step in your workflow must produce structured output via a specific tool, and none when you want a plain conversational answer with no tool calls.
Practice Exercises
Exercise 1: Sharpen a vague description
Take the vague_tools from the first section and rewrite only the descriptions to be prescriptive — without changing the names or schemas. Ask "Where is my order A1043?" again and confirm the model now picks the order-status tool instead of calling nothing.
Hint
You only need to edit the two "description" strings. Say when each tool applies — e.g. “Use this when the user asks where their order is.” Then rerun the first code block and watch stop_reason change from end_turn to tool_use.
Exercise 2: Add an enum
Extend get_weather with a unit property restricted to "celsius" or "fahrenheit", and add it to required. Ask "What's the weather in London in fahrenheit?" and inspect block.input — confirm the model fills unit with one of the allowed values.
Hint
Add "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit."} to the tool’s properties. The enum guarantees the model can only return a listed value — it cannot invent "kelvin".
Exercise 3: Force a tool
Reuse multi_tools and ask a question that the model would normally answer with convert_currency, but pass tool_choice={"type": "tool", "name": "create_calendar_event"}. Confirm the model calls create_calendar_event anyway — then think about why forcing the wrong tool is rarely what you want.
Hint
The forced tool will be called even though the question doesn’t fit it, and the arguments the model invents may be nonsense. This is exactly why tool forcing is reserved for steps where you know the right tool in advance — usually you want auto.
Summary
A tool definition is the full contract between you and the model: a verb-like name, a prescriptive description that says when to call the tool, and a precise input_schema that describes its arguments. The description is the field that decides whether the model calls the tool at all — vague descriptions leave it unable to choose, while prescriptive ones route requests cleanly even across several tools. A good schema uses the right type, a description on every property, a required list, and features like enum and arrays to keep the model’s arguments valid. When you need to override the model’s judgment, tool_choice lets you force a tool, require some tool, or block tools entirely.
Key Concepts
name— a clear, verb-like identifier for the function (create_calendar_event).description— what the tool does and when to use it; the model reads this to decide whether to call.input_schema— a JSON Schema describing the arguments:type,properties(each with adescription), andrequired.enum— restricts a property to a fixed set of allowed values.tool_choice—auto(default),any(must use a tool),tool(force one), ornone(no tools).
Why This Matters
Every multi-tool application — an assistant that books meetings, queries databases, and checks order status all in one conversation — depends on the model picking the right tool with valid arguments. That reliability comes almost entirely from the quality of your definitions. Learning to write them well is the difference between a model that helps and one that guesses.
Next Steps
Continue to Lesson 3 - The Tool Use Loop
Turn a single tool call into a working loop: run the function, return the result, and let the model keep going until it's done.
Back to Module Overview
Return to the Tool Use & Function Calling module overview
Continue Building Your Skills
You can now write tool definitions the model uses reliably and steer it with tool_choice. Next you’ll wire these tools into a real loop — running the function, feeding the result back, and letting the model continue until it reaches a final answer.