What function calling really is
"Function calling" (OpenAI) and "tool use" (Anthropic) are the same idea: you describe a set of functions to the model using JSON Schema, and instead of answering in prose, the model responds with a structured request to call one of them — including a JSON object of arguments that conforms to your schema.
Your code then runs the real function and (optionally) feeds the result back so the model can continue. The model never executes anything itself; it only decides *which* function to call and *with what arguments*.
This makes JSON Schema the contract between the model and your code. A good schema produces reliable calls; a vague one produces hallucinated or malformed arguments.
Anatomy of a tool definition
{
"name": "get_weather",
"description": "Get the current weather for a city. Use when the user asks about weather conditions.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Ahmedabad' or 'Tokyo'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default celsius."
}
},
"required": ["city"]
}
}Three parts do the heavy lifting:
- `name` — short, verb-based, machine-friendly.
- `description` — the most underrated field. Describe *what the function does and when to use it*. The model reads this to decide whether to call the tool.
- `input_schema` — the JSON Schema for arguments, with per-property
descriptions.
Writing schemas the model can satisfy
- Describe every property. A
descriptionon each field dramatically reduces wrong arguments. - Use enums for closed sets.
"status": {"enum": ["open", "closed"]}prevents the model from inventing values. - Mark only what you truly need as `required`. Over-requiring forces the model to guess.
- Constrain formats. Use
"format": "date","format": "email", orpatternso arguments arrive parseable. - Avoid free-form objects.
"type": "object"with no properties invites arbitrary, unpredictable shapes.
Example: a multi-tool agent
tools = [
{
"name": "search_orders",
"description": "Search a customer's orders by status or date range.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "shipped", "delivered", "cancelled"]},
"since": {"type": "string", "format": "date"}
},
"required": ["customer_id"]
}
},
{
"name": "issue_refund",
"description": "Issue a refund for a specific order. Only call after confirming with the user.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"amount": {"type": "number"},
"reason": {"type": "string"}
},
"required": ["order_id", "amount", "reason"]
}
}
]Notice how issue_refund uses its description to add a guardrail ("Only call after confirming"). Descriptions are policy, not just documentation.
The call loop
- Send the user message plus your tool definitions.
- The model returns a tool call with JSON arguments.
- Validate the arguments against the schema (never trust them blindly).
- Execute the real function.
- Send the result back to the model as a tool result.
- The model produces a final answer or another tool call.
Common mistakes
- No descriptions. The model guesses intent and picks the wrong tool.
- Loose types.
amountas a string leads to"50"vs50bugs. - Giant schemas. Twenty tools with thirty fields each overwhelm the model. Group and trim.
- Skipping validation. Always re-validate arguments server-side before acting, especially for destructive operations.