In the docs, tool use looks simple. In production it breaks for a dozen reasons at once. What follows is how to build schemas and loops so you never get that far.
What happens at the protocol level
You hand the model a list of functions: a name, a description and a JSON schema for the parameters. The model never runs the function itself. It returns a structured tool_use block with a name and arguments. You execute the code on your side, send the outcome back as a tool_result, and the model carries on with the conversation, now knowing the result.
One detail is easy to miss: in a single step the model can ask for several tools, if the task allows it. Your code has to be able to run them, in parallel if you like, and return the results in the right order. Otherwise the loop loses track of which result belongs to which call.
How to design the schema
To choose a tool, the model relies only on the function name and the parameter descriptions. A vague "works with data" leads to random calls where none are needed.
A good description answers three questions up front: what the function does, when to call it and when not to. Example:
{
"name": "search_orders",
"description": "Looks up a customer's orders by phone number or email. Call only when the user explicitly asks about their own order. Do not call for general questions about shipping or pricing.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Customer phone or email" },
"status": { "type": "string", "enum": ["any", "pending", "shipped", "delivered"] }
},
"required": ["query"]
}
}
Enums in place of free text cut the share of invalid calls more than any prompt edit. If a parameter takes three values, define an enum of three values rather than a paragraph of prose.
The multi-step loop
A real task is rarely closed by a single call. The model invokes a tool, looks at the result, decides whether it needs another step, and goes on like that until it writes a final text answer. A loop skeleton in Python:
messages = [{"role": "user", "content": user_query}]
while True:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
tool_calls = [b for b in response.content if b.type == "tool_use"]
if not tool_calls:
break
results = [run_tool(call.name, call.input) for call in tool_calls]
messages.append({
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": call.id, "content": result}
for call, result in zip(tool_calls, results)
],
})
Each turn of the loop sends a new request with a longer history. Count tokens at every step: the history swells fast, especially when tools return big chunks of text.
Common schema mistakes
Too many tools at once. Given twenty similar functions, the model more often mixes up which one to call. Fold related operations into one tool with a mode parameter instead of creating ten near-duplicates.
A description written for a person instead of the model. "See the API docs" is useless: the model does not open external links, so everything it needs has to sit in the description itself.
No error handling in tool_result. When a function fails, return the error as a result with a clear message instead of throwing an exception outward. If it sees the error as text, the model can react and try another way.
Guarding against an endless loop
The model can get stuck and repeat one call with slightly altered arguments, especially if a tool returned an empty or ambiguous result. Set a hard ceiling on the number of steps (five to ten is usually enough) and stop with a clear error once it is reached.
A log helps too: record every tool call together with its arguments. When the loop starts spinning in place, the log shows at once which step the model began going in circles.
How the two formats differ
Both formats are available in codexkey. Anthropic uses tool_use and tool_result blocks inside content. OpenAI uses a tool_calls field on the assistant message and a tool role in the next message. The logic is the same, but the JSON shape is different, so if you work straight against REST without an SDK, loop code for the two formats does not carry over one to one.
Summary
A schema should tell the model what a function does and when to call it, preferably with an enum instead of free text. The loop adds tokens with every step, so count them and cap the number of iterations. Return tool errors as text in tool_result, not as exceptions: the model copes with them better than your code expects.