What is function calling?
Explain what function calling is and is not, define a tool, and write the full request, tool_use, tool_result loop by hand and with the SDK tool runner.
Words in this lessonTool calling · Tool definition · Tool result · Stop reason · JSON Schema · Hallucination
- Tool calling
- When a model asks your code to run a named function with specific inputs, then uses the result. Like a waiter who cannot cook but can send a ticket to the kitchen and bring back what comes out.
- Tool definition
- The description of a tool you give a model: its name, what it does, and what inputs it takes. Like a line on a restaurant menu: the dish name, a short description, and the choices you can make.
- Tool result
- The output of a tool your code ran, sent back to the model so it can continue. Like the dish passed back through the kitchen hatch with the order number on it.
- Stop reason
- A field in an API response that says why the model stopped writing. Like the note a delivery rider leaves: delivered, nobody home, or address not found.
- JSON Schema
- A standard, machine-readable description of what valid JSON looks like. Like a blank order form that shows exactly which boxes must be filled and how.
- Hallucination
- When a model gives a fluent, confident answer that is false or made up. Like a nervous new employee who would rather invent an answer than say "I don't know".
On this page
- Arjun's two obvious fixes
- The waiter and the order slip
- Opening the hood: what you send, what comes back
- Following CR-1042 through the loop
- The real code, built step by step
- Step 1: the kitchen
- Step 2: the menu
- Step 3: one call, and look at the slip
- Step 4: the loop
- A shortcut: the SDK's tool runner
- Where it breaks
- Try it yourself
- Common mistakes
- Explaining it to Meera
- Recap
- Go deeper
The Baner manager’s test question had been simple: “Is order CR-1043 out for delivery?” The assistant had answered warmly and instantly: “Yes! It’s on its way and should reach you within the hour.”
It sounded perfect. It was a guess.
Arjun went looking and found worse. Two days earlier, a real customer had asked the website chat about order CR-1042, a birthday cake. The assistant had told her it was “out for delivery, arriving in about 30 minutes”. In fact the order had been cancelled that morning because her payment failed. She waited at home for two hours, then drove to the Baner store in the rain to find out. Her email to Meera was polite, and devastating: “Your website lied to me on my daughter’s birthday.”
Meera forwarded it to Arjun with one line: “How can it say something so confident when it doesn’t know?”
“Because nothing in its world can know,” Priya said, reading over his shoulder. “The model has never seen your order database. It’s like asking someone locked in a room with no windows whether it’s raining. They’ll give you a plausible answer. Today we cut a door in the room.”
Important
The idea in one sentence: Function calling lets a model ask your code to run a function you have described (look up an order, check stock) and then use the real result in its answer; the model only ever writes the request, and your code decides whether and how to run it.
Arjun’s two obvious fixes
Arjun’s first fix was the prompt. He added a line to the system prompt: “Never guess order details.” He asked about CR-1042 again.
“I’m sorry, I don’t have access to order information. Please contact our team on WhatsApp.”
Honest, and useless. Most chat questions at Crumb are about orders. A bot that refuses all of them saves no one any time.
His second fix was to give the model the data. Every morning, he thought, he could paste the day’s orders into the system prompt. He checked: 2,300 open orders, about 80 tokens each. That is about 184,000 tokens in every single request. At Claude Opus 5.5’s 0.74, or ₹65, per customer question, before the model writes a word. And the data would be stale within minutes: CR-1042 was cancelled at 10:14 a.m.; a snapshot from 9 a.m. would still say “baking”.
“Neither of those scales,” Priya said. “One makes the bot useless, the other makes it expensive and still wrong. What you actually want is for the model to fetch just the one order it needs, at the moment it needs it.”
“But the model can’t run code.”
“No. So how does a waiter get food from a kitchen he isn’t allowed into?”
The waiter and the order slip
“Walk me through the restaurant at Koregaon Park,” Priya said. “A customer asks the waiter for the paneer tikka. Does the waiter cook it?”
“No. He writes a slip and passes it through the hatch to the kitchen.”
“What’s on the slip?”
“The dish, table number, any changes. ‘No onion’, that kind of thing.”
“Who decides whether to cook it?”
“The kitchen. If they’re out of paneer, they say so.”
“And then?”
“The food comes out through the hatch, and the waiter brings it to the table and says something nice.”
“That’s function calling,” Priya said. “The model is the waiter. It can’t enter the kitchen, which is your code and your database. But you give it a menu of slips it’s allowed to write, like ‘look up order status, order id goes here’. When a customer asks something that needs a slip, the model writes one and stops talking. Your code is the kitchen: it reads the slip, decides whether to run it, runs it, and passes the result back through the hatch. Then the model turns the result into a friendly answer.”
Arjun frowned. “So the model never actually calls the function?”
“Never. That’s the most important sentence in this whole module. The name ‘function calling’ is misleading. The model requests a call. You make it.”
Where the analogy breaks: a waiter remembers the slip he wrote. The model does not remember anything between API calls (the statelessness from Module 01), so your code must send the whole exchange back, slip and result included, every time. You will see exactly what that looks like in a moment.
Opening the hood: what you send, what comes back
Function calling (Anthropic’s docs call it tool use, and the functions tools; the words are interchangeable) adds one new ingredient to the API call: a list of tool definitions. Each definition has three parts:
name: what the model writes on the slip, likeget_order_status.description: plain English explaining what the tool does and when to use it. This is the menu text, and the model reads it as carefully as any prompt.input_schema: a JSON Schema (a standard JSON description of allowed keys and types, which you met in Module 05) saying what arguments the tool takes.
The model never sees your Python function. It sees only those three fields. That is all it knows about your kitchen.
When the model decides a tool would help, its response contains a tool_use block: the tool name, the arguments as JSON, and a unique id. The response’s stop_reason is "tool_use" instead of the usual "end_turn", which is the model saying “I’ve written a slip; I’m waiting for the kitchen.”
Your code then runs the real function and sends the output back in a tool_result block, tagged with the same id so the model knows which slip it answers. The model reads the result and writes its final reply.
sequenceDiagram
participant U as Customer
participant A as Your app
participant M as Claude
participant D as Order database
U->>A: Where is my order CR-1042?
A->>M: messages + tool definitions
M-->>A: tool_use get_order_status order_id CR-1042, stop_reason tool_use
A->>D: get_order_status("CR-1042")
D-->>A: status cancelled, reason payment failed
A->>M: messages + assistant tool_use + user tool_result
M-->>A: Final text, stop_reason end_turn
A-->>U: Your order was cancelled because the payment failed...
Two round trips to the model instead of one. That is the price of grounding answers in real data, and you will see the exact cost below.
Following CR-1042 through the loop
Here is the birthday-cake question, traced end to end with the real values from Arjun’s terminal.
Request 1. Arjun’s app sends the system prompt, the customer’s question, and one tool definition:
{
"name": "get_order_status",
"description": "Look up the live status of a Crumb & Co. order. Use this whenever a customer asks where their order is, when it will arrive, or what happened to it. Order ids look like CR-1042.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "The order id, e.g. CR-1042"}
},
"required": ["order_id"]
}
}
Including the tool definition and the few hundred tokens of tool-use instructions the API adds behind the scenes, this request is about 525 input tokens.
Response 1. Claude does not answer the customer. Its content holds a thinking block (on Claude Opus 5.5 the model thinks before acting; the thinking text is empty by default) and then this:
ToolUseBlock(type='tool_use', id='toolu_01A9xK...', name='get_order_status', input={'order_id': 'CR-1042'})
stop_reason: tool_use
About 80 output tokens. The model has filled in the slip: the right tool, and the order id copied exactly from the question.
Your code runs the function. get_order_status("CR-1042") queries the database and returns:
{"order_id": "CR-1042", "status": "cancelled", "reason": "payment failed", "cancelled_at": "10:14"}
Request 2. Arjun’s app sends the whole conversation again, now three messages long: the customer’s question; the assistant’s response 1 exactly as received, including the tool_use block; and a new user message containing the tool_result:
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_01A9xK...", "content": '{"order_id": "CR-1042", "status": "cancelled", ...}'}
]}
About 650 input tokens.
Response 2. Now stop_reason is end_turn and the content includes text:
“I’m really sorry, but order CR-1042 was cancelled this morning at 10:14 because the payment didn’t go through, so it isn’t on its way. If you’d still like the cake today, I can help you place a new order, or our Baner team can check same-day options for you.”
About 90 output tokens.
The bill. Request 1: 525 × 0.00002 = 0.000004 + 90 × 0.0044. Total about $0.0081, or about ₹0.71. Compare that with ₹65 for stuffing the whole database into the prompt, and with the cost of a customer driving to the store in the rain.
Note
Pause and think. Look back at request 2. Why must the app re-send response 1 (the assistant message with the tool_use block) before the tool_result? What do you think happens if it doesn’t?
Show the answer
The API is stateless: it has no memory of response 1. A tool_result says “here is the answer to slip toolu_01A9xK...”, and that only makes sense if the slip itself is in the conversation, in the assistant message just before. If you leave it out, the API rejects the request with a 400 error saying the tool_result refers to a tool_use it cannot find. The rule: always append the assistant’s content exactly as received, then your results.
The real code, built step by step
Step 1: the kitchen
Start with a plain Python function. Nothing AI about it:
# order_bot.py
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
# A pretend order database. In real life, a query against your orders table.
ORDERS = {
"CR-1042": {"status": "cancelled", "reason": "payment failed", "cancelled_at": "10:14"},
"CR-1043": {"status": "out_for_delivery", "rider": "Sunil", "eta_minutes": 25},
}
def get_order_status(order_id: str) -> dict:
order = ORDERS.get(order_id.strip().upper())
if order is None:
return {"error": f"No order found with id {order_id}"}
return {"order_id": order_id.upper(), **order}
Notice that “not found” returns a helpful dictionary instead of raising. The model will read whatever you return, so an error it can understand lets it tell the customer something useful (“I couldn’t find that order; could you check the number?”).
Step 2: the menu
TOOLS = [{
"name": "get_order_status",
"description": (
"Look up the live status of a Crumb & Co. order. Use this whenever a customer asks "
"where their order is, when it will arrive, or what happened to it. "
"Order ids look like CR-1042."
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "The order id, e.g. CR-1042"},
},
"required": ["order_id"],
},
}]
def run_tool(name: str, tool_input: dict) -> dict:
"""The kitchen hatch: map a tool name to real code."""
if name == "get_order_status":
return get_order_status(**tool_input)
return {"error": f"Unknown tool {name}"}
run_tool is a dispatcher: it reads the slip’s tool name and calls the matching Python function with the slip’s arguments. **tool_input unpacks {"order_id": "CR-1042"} into order_id="CR-1042".
Step 3: one call, and look at the slip
Before writing a loop, look at what comes back:
SYSTEM = "You are Crumb & Co.'s support assistant. Never guess order details; look them up."
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=2000,
system=SYSTEM,
tools=TOOLS,
messages=[{"role": "user", "content": "Where is my order CR-1042?"}],
)
print(response.stop_reason)
for block in response.content:
print(block.type, getattr(block, "name", ""), getattr(block, "input", ""))
Output:
tool_use
thinking
tool_use get_order_status {'order_id': 'CR-1042'}
Ask “Do you sell sourdough?” instead and you get end_turn and a plain text answer: the model only writes a slip when the question needs one. That decision, made from your description, is the whole intelligence of function calling.
Step 4: the loop
A conversation may need several slips in a row (look up the order, then something else), so wrap it in a loop: keep going while the model asks for tools, stop when it answers.
def ask(question: str) -> str:
messages = [{"role": "user", "content": question}]
for _ in range(5): # safety cap: never loop forever
response = client.messages.create(
model="claude-opus-5-5", max_tokens=2000,
system=SYSTEM, tools=TOOLS, messages=messages,
)
if response.stop_reason != "tool_use":
return next((b.text for b in response.content if b.type == "text"), "")
# 1. Keep the assistant's slip(s) in the history, exactly as received
messages.append({"role": "assistant", "content": response.content})
# 2. Run every requested tool and collect the results
results = []
for block in response.content:
if block.type == "tool_use":
print(f" -> model asked for {block.name}({block.input})")
output = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id, # which slip this answers
"content": json.dumps(output),
})
# 3. Send all results back in ONE user message
messages.append({"role": "user", "content": results})
return "Sorry, I couldn't finish that. A teammate will follow up on WhatsApp."
print(ask("Where is my order CR-1042?"))
print(ask("Is cr 1043 nearly here?"))
Output:
-> model asked for get_order_status({'order_id': 'CR-1042'})
I'm really sorry, but order CR-1042 was cancelled this morning at 10:14 because the payment didn't go through...
-> model asked for get_order_status({'order_id': 'CR-1043'})
Good news: CR-1043 is out for delivery with Sunil and should reach you in about 25 minutes.
Look at the second question: the customer typed “cr 1043”, and the model normalised it to CR-1043 because the description said ids look like CR-1042. Your function’s .strip().upper() is a second line of defence. Descriptions guide; code enforces.
Note
Pause and think. A teammate suggests making the tool description just "Gets order" to save tokens. What could go wrong?
Show the answer
The model chooses tools from their names and descriptions alone. With “Gets order” it does not know the id format, so it may pass “1042” or the customer’s name. It does not know when to use the tool, so it may skip it for “when will my cake arrive?” (no word “order”) and go back to guessing. It may even call it for “do you take orders for weddings?”. A description of two or three sentences costs perhaps 40 tokens, about a fifth of a paisa per request. It is the cheapest reliability you can buy.
A shortcut: the SDK’s tool runner
Writing the loop yourself is the best way to understand it, and it gives you full control. Once you understand it, the Anthropic SDK can run it for you. The tool runner turns a decorated Python function into a tool definition (from its type hints and docstring) and drives the loop:
from anthropic import beta_tool
@beta_tool
def get_order_status_tool(order_id: str) -> str:
"""Look up the live status of a Crumb & Co. order. Use this whenever a customer asks where
their order is, when it will arrive, or what happened to it.
Args:
order_id: The order id, e.g. CR-1042.
"""
return json.dumps(get_order_status(order_id))
runner = client.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=2000,
system=SYSTEM,
tools=[get_order_status_tool],
messages=[{"role": "user", "content": "Where is my order CR-1042?"}],
)
for message in runner: # one message per model turn; tools run automatically in between
final = message
print(next(b.text for b in final.content if b.type == "text"))
Same result, about half the code. The docstring’s first paragraph becomes the description, and the Args: section becomes the argument descriptions, so write them as carefully as you would write the JSON by hand. The runner is marked beta, which means its details may still change. You will use it again in this module’s external-APIs lesson.
Where it breaks
Function calling fixed CR-1042. It also opened new ways to fail, and Arjun hit most of them in the first week.
The model can ask for the wrong thing. A customer wrote “my order from Tuesday”, with no id. The model, eager to help, sometimes called the tool with "Tuesday". Your function must treat every argument as untrusted input: validate it, and return a clear error the model can relay (“I need the order id, which starts with CR-”).
Caution
The model will happily look up any order id a customer types, including someone else’s. A tool’s arguments come from the conversation, and the conversation comes from a stranger. Authorization belongs in your code: check that the order belongs to the logged-in customer (or that their phone number matches) before returning details. Never rely on a prompt like “only show the customer their own orders”.
Loops need a cap. If a tool keeps returning errors, a model may keep retrying. The range(5) cap in ask() turns a runaway loop into a polite handover.
Every tool round trip adds latency. Two model calls instead of one means roughly double the wait, about 4 to 6 seconds for CR-1042. For a chat, stream the final answer (Module 05) and show “checking your order…” while the tool runs.
Errors deserve their own flag. When a tool fails, you can add "is_error": True to the tool_result. The model then knows the result is a failure, not data, and explains accordingly. The next two lessons use this.
And a misconception to drop now: “Function calling means the AI is running my code, so it is dangerous.” The danger is real but it lives in a different place. The model cannot run anything. The danger is in what your code does with the model’s requests: which tools you offer, what they can change, and what you check before acting. Treat the model like a polite, fast, occasionally confused new employee: give it read access first, and ask for confirmation before anything irreversible.
Try it yourself
Add a second tool, check_allergens, that takes a product name and returns the allergens from a small dictionary:
ALLERGENS = {
"chocolate truffle cake": ["milk", "wheat", "soy"],
"kaju katli": ["cashew (tree nut)", "milk"],
"sourdough loaf": ["wheat"],
}
Write the tool definition, add it to TOOLS, and extend run_tool. Then ask: “Does kaju katli have nuts? My nephew is allergic.”
Expected: the model calls check_allergens with product set to "kaju katli" (or similar), then answers that it contains cashew, a tree nut, and suggests checking with the store for cross-contamination.
Show the answer
def check_allergens(product: str) -> dict:
found = ALLERGENS.get(product.strip().lower())
if found is None:
return {"error": f"No allergen data for '{product}'. Known products: {', '.join(ALLERGENS)}"}
return {"product": product, "allergens": found}
TOOLS.append({
"name": "check_allergens",
"description": "Get the list of allergens in a Crumb & Co. product. Use this for any question "
"about ingredients, allergies, nuts, eggs, gluten or dairy.",
"input_schema": {
"type": "object",
"properties": {"product": {"type": "string", "description": "Product name, e.g. kaju katli"}},
"required": ["product"],
},
})
# in run_tool:
# if name == "check_allergens":
# return check_allergens(**tool_input)
Listing the known products in the error message is a neat trick: if the model guesses a slightly wrong name, the error tells it the right options, and it usually retries correctly on its own.
Common mistakes
- Believing the model runs your function. It only writes a request. Your code decides, runs and checks.
- Forgetting to append the assistant’s content. Every
tool_resultmust follow the assistant message containing itstool_use. Appendresponse.contentexactly as received. - Vague tool descriptions. The model chooses tools by description. Say what the tool does, when to use it, and what the arguments look like.
- Trusting tool arguments. They come from a stranger’s message. Validate them and check permissions in code.
- No loop cap. Always bound the number of tool rounds.
- Returning huge raw data. Every byte you return becomes input tokens on the next call. Return only what the model needs.
Explaining it to Meera
Arjun wrote back to Meera that evening, and copied the line into the reply to the birthday-cake customer too.
“Until today, the assistant could only use what it already knew, and it knew nothing about our orders, so it filled the gap with a confident guess. Now, when someone asks about an order, the assistant writes a request, ‘please look up CR-1042’, and our own system does the lookup and hands it the real answer. The assistant never touches the database itself. Our code decides what it’s allowed to see.”
Meera replied: “So it’s like the waiter writing a slip for the kitchen, instead of making up what’s in the oven.”
“Exactly that.”
“And it can’t look up other people’s orders?”
“Not anymore. The check is in our code, not in the AI’s instructions.”
Recap
- Models cannot see your data or run your code; function calling lets them request that your code run a described function.
- A tool definition is a name, a description and a JSON Schema for the inputs; the model sees only these.
- The loop: the model returns a
tool_useblock withstop_reason == "tool_use", your code runs the tool, you send atool_resultwith the matching id, and the model writes the answer. - Always append the assistant’s content before your results, cap the number of rounds, and return small, clear results, including clear errors.
- Validate arguments and check permissions in code; the SDK’s tool runner can drive the loop once you understand it.
The next morning, a customer typed: “Is the eggless truffle cake in stock at Koregaon Park and Baner, and what’s the delivery fee to Aundh?” Three questions, three lookups, one message. Arjun’s loop answered correctly, but it took eleven seconds, and the logs showed something odd about the order in which things happened. Priya leaned in. “Now we talk about many tools, many calls, and the day you’ll want to force a call.”
Go deeper
- Anthropic docs (tool use): https://docs.anthropic.com
- OpenAI docs (function calling): https://platform.openai.com/docs
- Building effective agents: https://www.anthropic.com/research/building-effective-agents