When an agent "calls the tool wrong" — sends "USA" where you wanted "US", forgets a required field, invents a parameter you never defined — the instinct is to rewrite the prompt. That's usually the slow fix. The bad call happened because the schema permitted it. Tighten the schema so the wrong call can't be expressed, and the error stops happening instead of happening less often.
Here's the whole idea in one line: encode every rule you can as a JSON Schema constraint, then turn on strict mode so the model's decoder is forced to obey it. What's left over — the rules a schema can't state — is the only thing your prompt and your runtime validation should worry about.
Make illegal arguments unrepresentable#
A free-form string is an invitation to guess. Give a field a closed set and only the real values can be produced.
{
"name": "set_order_status",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"]
},
"region": { "type": "string", "enum": ["us-east-1", "eu-west-1"] }
},
"required": ["order_id", "status", "region"],
"additionalProperties": false
}
}
enum is the single highest-leverage keyword in a tool schema. It converts "please use one of these values" — a prompt-level plea the model can drop under load — into a set the model can only pick from. Same story for type: declare integer, not string, and you stop parsing "3" on the receiving end.
required + additionalProperties: false#
These two lines kill the two most common structural errors together. required closes the "missing field" gap; additionalProperties: false closes the "phantom field" gap, where a helpful model tacks on a notes or priority you never asked for and your parser chokes. Together they mean the object has exactly the shape you declared — no more, no less.
Optional fields don't get an exemption under strict mode; they get expressed as a union with null:
"tracking_number": { "type": ["string", "null"] }
The field is always present, its absence just spelled null. That's the small tax strict mode charges for its guarantee.
Turn on strict mode#
Everything above is still best-effort until you flip the switch. Strict mode promotes the schema from documentation to grammar.
# OpenAI — Structured Outputs for tools
tools = [{
"type": "function",
"function": {
"name": "set_order_status",
"strict": True, # <- the switch
"parameters": { "...": "schema above" }
}
}]
# Anthropic — strict tool use
tools = [{
"name": "set_order_status",
"strict": True, # <- top-level, beside name/description
"input_schema": { "...": "schema above" }
}]
A tool schema isn't documentation for the model to read — under strict mode it's a grammar the model is forced to speak.
The mechanism is worth knowing because it's why the guarantee is real. Anthropic constrains sampling to a grammar built from your schema; OpenAI does constrained decoding for the same effect. Only tokens that keep the argument object schema-valid can be emitted, so type mismatches, missing required fields, and invalid enum values become impossible rather than improbable — the same constrained-decoding machinery behind JSON mode and structured output, pointed at your tool's arguments. The one cost: OpenAI requires additionalProperties:false and every property in required, and rejects a schema that breaks the rules — loudly, with the reason, which beats a silent wrong call.
What strict mode still won't do#
Strict mode enforces structure, not values. These stay your job:
- Numeric bounds and patterns.
minimum,maximum,pattern, and stringformataren't in the enforced subset. Ifquantitymust be 1–100, the schema won't stop999— validate it in the tool. - Cross-field rules. "
end_datemust be afterstart_date" is not expressible in the schema at all. - Live facts. Whether
order_idactually exists is a database question, not a schema question.
For all three, keep a validation step inside the tool and, when it fails, return a clear tool error the model can retry against rather than throwing. The schema shrinks the error surface; good error handling covers the remainder.
The one-line decision#
Spend your schema budget first, your prompt budget last:
- Every closed set →
enum. Every value the tool actually needs →required. Every object →additionalProperties:false. - Turn on
strict:trueso the decoder enforces all of it, not the prompt. - Validate ranges, patterns, and business rules in the tool, and return descriptive errors for what the schema can't state.
- **Reserve the tool description for when to call and *why*** — not for pleading about argument formats the schema should have nailed down. (And tighter descriptions cost fewer tokens too.)
If you generate schemas from types — Zod, Pydantic, or another Standard Schema library — this is nearly free: add the enum, mark the field required, set the strict flag once, and every call your agent makes inherits the guarantee. It's the cheapest reliability you'll buy this quarter, and unlike a prompt tweak, it doesn't regress the next time the model updates. If you also need to force a call rather than just shape it, that's the tool_choice knob, a separate lever from the schema.



