Tool calling
Tool calling is the contract between the LLM and the world: the model returns structured JSON (name + arguments), the runtime validates it against a schema and executes. It is what turns text into action.
What it is
Tool calling (also called ) is the interface through which an LLM returns, instead of (or in addition to) text, a structured function call: tool name + arguments in JSON. The runtime receives it, validates the arguments against a schema, executes and returns the result to the context.
The model never executes anything on its own. It only proposes: 'I would call get_weather with { city: 'São Paulo' }'. Execution, authorization, retry and auditing stay with the runtime. Forgetting who executes is the most expensive mistake that shows up in production.
The JSON schema: the contract
Each tool is declared to the model by a JSON schema with name, description and typed parameters. The LLM uses this schema as a grammar: it knows the name, knows the format of the arguments and uses the description to decide when it makes sense to call.
The description is the most neglected part. The LLM does not read code: it reads the string. 'Gets the weather of a city' is better than 'get_weather', because the model decides when to call based on prose. Schemas with bad descriptions produce right or wrong calls at random.
{
"name": "get_weather",
"description": "Gets the current temperature and the forecast for the next 3 days in a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, in Portuguese or English (e.g. 'São Paulo', 'Lisbon')."
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["city"]
}
}Argument validation: your responsibility
When the model returns a function call, the runtime receives a JSON that may be wrong: wrong type, missing field, value outside the enum, string where a number should be. Validating before executing is non-negotiable: it is the boundary between the model, which proposes, and your code, which executes with real permissions.
In Python, Pydantic is the standard path: declare a model, call model_validate(model_dump), and Pydantic raises ValidationError with the exact list of fields and the reason. In TypeScript, the natural pair is Zod, with safeParse returning { success, data, error }.
from pydantic import BaseModel, Field, ValidationError
class GetWeatherArgs(BaseModel):
city: str = Field(..., min_length=1)
unit: str = Field("celsius", pattern=r"^(celsius|fahrenheit)$")
def call_tool(name: str, raw_args: dict):
if name != "get_weather":
raise ValueError(f"unknown tool: {name}")
try:
args = GetWeatherArgs.model_validate(raw_args)
except ValidationError as e:
return {"error": "invalid_args", "details": e.errors()}
return fetch_weather(args.city, args.unit)The complete flow, end to end
A tool call goes through five steps, each with a possible response and a distinct error path. Without seeing all five, it looks like magic, and the bugs are not where you expect.
1. declaration: the list of schemas is handed to the LLM along with the prompt.
2. proposal: the LLM returns tool_calls with name + JSON arguments.
3. validation: the runtime parses and validates against the schema; a failure here means 'the model hallucinated'; treat it as an error and return the reason to the LLM.
4. execution: the runtime calls the real function (HTTP, DB, command) with permissions and a timeout.
5. observation: the result becomes a tool message in the context; the LLM uses it for the next decision.
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';
const GetWeather = z.object({
city: z.string().min(1),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});
async function run(anthropic: Anthropic, userMessage: string) {
const tools = [{
name: 'get_weather',
description: 'Gets the current weather of a city.',
input_schema: /* JSON shape of the schema */,
}];
let msg = await anthropic.messages.create({
model: 'claude-opus-4', max_tokens: 1024, tools,
messages: [{ role: 'user', content: userMessage }],
});
if (msg.stop_reason === 'tool_use') {
const call = msg.content.find((b) => b.type === 'tool_use');
const parsed = GetWeather.safeParse(call.input);
if (!parsed.success) return { error: 'invalid_args', issues: parsed.error.issues };
const result = await fetchWeather(parsed.data.city, parsed.data.unit);
msg = await anthropic.messages.create({
model: 'claude-opus-4', max_tokens: 1024, tools,
messages: [
{ role: 'user', content: userMessage },
{ role: 'assistant',content: msg.content },
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: call.id, content: JSON.stringify(result) }] },
],
});
}
return msg;
}Why you cannot skip validation
- the model can invent fields: it has seen 1000 different schemas and does not guarantee consistency
- types are guaranteed, values are not: it knows 'unit' is a string, but it may send 'celcius' (typo) from time to time
- enums are the most common error boundary: the model rarely respects the enum without an explicit prompt
- validation is cheap: it costs microseconds; unvalidated execution can allow a critical execution order
Strengths and limitations
- strength: the schema forces structure; the model does not need to 'try' to produce valid JSON; it is producing tokens within a grammar
- strength: validation errors return a rich message to the model, which almost always corrects itself on the next turn
- weakness: a bad description leads to right or wrong calls at random; writing a good schema is design work
- weakness: the call is opaque to the user; if they do not read the trace, they do not know which tool ran
- weakness: parallelism depends on the provider; some allow several tool_calls in one response, others only one
connections on the map
- Agent frameworksenablesTool calling
- Tool callingspeaks the protocolMCP - model context protocol
updated on 14/10/2026 · sources to go deeper · report an error
in other domains
- Context engineering for coding agentsvibecodingtools and permissions decide what the agent can do
- SASTappseccatches injection in code; here the target is the tool argument
- Security automation and playbooks as codesecenga model-triggered action needs the same brakes as a playbook
Study before
Unlocks
test what you learned
Loading questions…