Tool Schemas
The name, description and JSON schema of a tool are the only documentation the model ever reads — write them like an API contract, not a comment.
The schema is the documentation
When you register a tool, the model receives three things: name, description, and a JSON schema under parameters. It does not see your source code, your README, your type hints, or your unit tests. If a constraint is not expressed in those three fields, the model does not know it exists.
This reframes schema writing. It is not boilerplate for the parser; it is a prompt. A vague description costs you wrong tool selections and malformed arguments on every call. Teams routinely fix "the agent keeps picking the wrong tool" by rewriting descriptions alone, with no model or prompt change.
Names matter too. search says nothing; search_support_tickets tells the model both the domain and that it is read-only. Use verb_object naming and keep verbs consistent across tools (get_, list_, create_, delete_).
Parameter schema mechanics
The parameters object is standard JSON Schema (draft 2020-12 in most providers, with a supported subset). The features that pay for themselves are: type on every property, enum for closed sets, required for what must be present, description on each property, and minimum/maximum or pattern where the provider honors them.
Enums are the highest-leverage feature. A status parameter typed as string invites "Open", "open", "OPEN" and "opened". Typed as enum: ["open","pending","closed"], the model produces exactly one of three values. Every closed set — currencies, regions, priorities, sort orders — should be an enum.
Mark a field required only if the tool cannot run without it. Optional fields with sensible defaults reduce the chance the model invents a value to satisfy the schema. Avoid deeply nested objects and anyOf: many providers flatten or reject them, and models fill nested shapes less reliably.
1const createTicket = {2 name: 'create_support_ticket',3 description:4 'Create a new support ticket for the current customer. ' +5 'Use when the user reports a problem that cannot be solved in chat. ' +6 'Do NOT use for feature requests (use submit_feedback). Returns the ticket id.',7 parameters: {8 type: 'object',9 properties: {10 title: { type: 'string', description: 'One-line summary, max 80 chars, no PII.' },11 body: { type: 'string', description: 'Full description in the user\'s words.' },12 priority: {13 type: 'string',14 enum: ['low', 'normal', 'high', 'urgent'],15 description: 'urgent only for outages affecting multiple users.',16 },17 product: { type: 'string', enum: ['billing', 'api', 'dashboard'] },18 },19 required: ['title', 'body', 'product'],20 additionalProperties: false,21 },22} as constGood vs bad descriptions
A good description answers four questions in two or three sentences: what the tool does, when to use it, when not to use it (naming the alternative), and what it returns. Bad descriptions restate the name or describe the implementation.
- Bad:
"Searches."— no domain, no scope, no hint about when it applies. - Bad:
"Calls the /v2/orders endpoint with a GET request."— implementation detail the model cannot use. - Good:
"Look up an order by id. Use when the user provides an order number like ORD-12345. For orders by date range use list_orders. Returns status, items and shipping ETA." - Good property description:
"ISO 8601 date, e.g. 2025-03-14. Must not be in the future."— format plus a constraint plus an example. - Disambiguate sibling tools explicitly: if
get_userandsearch_usersboth exist, each description should name the other and say which case it is for.
Versioning and evolution
Tool schemas drift. A field gets renamed, a new enum value appears, a required parameter becomes optional. The model that was tuned by your evals on the old schema now sees a different contract. Treat a schema change like an API change: version it, test it, and roll it out with an eval run (Regression Gates and Online Evaluation).
Practical rules: never reuse a tool name with different semantics; add new optional fields rather than changing types; when a breaking change is unavoidable, register create_ticket_v2 alongside the old one for a transition window and log which one the model picks. Keep the schema in code next to the implementation so the two cannot diverge silently — generating the JSON schema from a Pydantic model or zod schema does this for free.
Finally, watch the count. Past roughly 20–30 tools, selection accuracy and prompt cost degrade. If the list keeps growing, route to tool subsets (Router Architecture) rather than exposing everything on every call.
Key points
- Name + description + parameter schema are the model's complete knowledge of a tool.
- Use
enumfor every closed set; it removes an entire class of argument errors. - Descriptions should say what, when, when not (naming the alternative), and what is returned.
- Mark fields
requiredonly when the tool genuinely cannot run without them. - Generate schemas from typed models so code and contract never diverge.
- Version schema changes and re-run evals; too many tools degrades selection accuracy.
When to use — and when not to
- Any tool exposed to a model — there is no "simple enough to skip the description" tool.
- When selection errors appear in traces: rewrite descriptions before touching the prompt.
- When two tools are frequently confused: add explicit "use X instead" cross-references.
- Encoding business rules only in the description and hoping the model obeys — enforce them in Argument Validation.
- Deep nested schemas or
anyOfunions when a flat shape with an enum discriminator would do. - Exposing 40 tools with one-line descriptions; route to subsets instead.
Failure modes
- Overlapping descriptions cause nondeterministic tool choice between similar tools.
- Free-text field where an enum was needed; the executor receives twelve spellings of one value.
- Schema renamed a field; the model keeps sending the old name it learned from few-shot examples.
- Description promises behavior the implementation does not have (e.g. "returns all orders" but the API pages).
- Provider silently drops unsupported schema keywords like
minimum, so the constraint never reaches the model.