Tool Schema Design
Tool-calling reliability lives in three places: the description the model reads to pick the tool, the JSON Schema it fills to call it, and the error response it uses to recover. Get those three right and a well-tuned model picks the right function, supplies valid arguments, and recovers gracefully.
The description writes the router
Descriptions — of tools and of parameters — are the primary lever for selection accuracy. The model disambiguates between similar tools by reading descriptions, not names.
- Tool description: cover what it does, when to use it, and when NOT to use it (name the sibling tool the model should pick instead).
- Parameter description: include expected format, valid range, and a concrete example.
"The ISO 8601 start date, e.g. '2026-03-15'" beats "The start date".
- Write in a neutral, third-person voice. No marketing.
Example:
{
"name": "search_knowledge_base",
"description": "Search the internal knowledge base for factual answers. Use for product documentation, policies, and FAQs. Do NOT use for real-time data like prices, stock levels, or order status — use get_product or get_order_status instead.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A natural-language search query, 3–15 words. Example: 'return policy for opened electronics'."
}
},
"required": ["query"]
}
}
Schema design rules
Every JSON Schema constraint is information the model uses to generate a valid call. Add signal, not noise.
- Types carry semantics:
integer on quantity tells the model fractional quantities are invalid; number allows them. Use the narrower type.
enum closes the space: prefer "enum": ["day", "week", "month"] over "description": "one of day, week, or month". The model cannot invent a fourth value.
minimum / maximum / pattern / format narrow generation. page_number with minimum: 1 implicitly signals one-indexed pages. format: "date", format: "uri", format: "email" are standardized.
- Consistent naming across the toolkit: pick one of
user_id / userId / uid and use it everywhere. Mixing shapes lowers extraction accuracy.
- Keep
required minimal. Every required field is a point of failure — if the model cannot extract or infer the value, the call fails. Give sensible defaults to non-critical fields and document the default in the description.
- One responsibility per tool.
create_or_update_user is two contracts colliding; split them.
- Nested objects sparingly. Deep nesting increases call errors; flatten when the operational meaning survives.
Structured outputs, not free text
When the response has to be machine-consumable, do not parse free text. Use provider strict-schema modes:
- OpenAI:
response_format: { type: "json_schema", json_schema: { strict: true, schema: {...} } } — the model output is grammar-constrained to the schema.
- Anthropic: pass a tool schema and set
tool_choice: { type: "tool", name: "..." } to force the output shape.
- Google Gemini:
response_mime_type: "application/json" with response_schema.
Rules of thumb:
- Prefer a strict schema over "please respond in JSON" in the prompt.
- Include the schema in the system prompt only if the model does not support strict mode; do not duplicate it if it does.
- Version the schema alongside the prompt.
tool_choice — the selection dial
Providers expose it under slightly different names. The four states are the same:
| State |
When |
auto (default) |
Model decides whether to call any tool. |
required / any |
Model must call some tool this turn. Use to force actioning. |
<specific tool> |
Model must call that tool. Use for structured extraction and eval harnesses. |
none |
Model may not call tools. Use for summarization / final response steps. |
Pin tool_choice at the agent step where behavior matters; leave auto where the model should route.
Error responses that steer recovery
An error is another turn of the conversation. Return content the model can act on.
- Return a clear error string; do not throw from a tool's
execute. A thrown exception ends the loop; a returned error becomes an observation the model can react to.
- Name the failure class:
not_found, invalid_argument, rate_limited, permission_denied, upstream_error.
- Include the offending argument value and the correction hint:
"invalid_argument: 'category'='electronic' is not in enum. Valid: ['electronics','clothing','food']."
- For retryable failures (rate limit, transient upstream), say so and give a hint:
"rate_limited: retry after 5s". Combine with framework-level retry policies (see loop-control-and-pivots) — do not loop internally.
- Never leak stack traces, secrets, or PII into the error string; the model puts it in the response.
Parallel and streaming considerations
- Parallel tool calls default on for OpenAI/Anthropic in 2026. Design tools to be idempotent where possible; add an idempotency key parameter when a repeat call would double-charge or double-write.
- Long-running tools should return a small handle (
operation_id) plus a get_status companion tool, not block the loop. For MCP, use the Tasks extension when negotiated (see mcp-creator).
- Streaming outputs from a tool are rarely useful to a model that reasons on complete observations; buffer and return the final result unless the tool is streaming user-visible content the agent forwards verbatim.
Anti-patterns
description: "A tool for users" — useless; the model routes on descriptions, not names.
- Bare
"type": "string" on an id, date, or enum — you gave the model no scaffolding, expect hallucinated values.
- Overlapping tools with the same trigger words — model routes randomly. Fix by explicit when-NOT-to-use on each description, or merge the tools.
get_data(filters: object) with no schema for filters — the model invents keys. Enumerate the fields.
- Everything
required — the model gets stuck when a field can't be inferred; a call fails instead of asking. Make optional what has a sensible default.
- Silent success on empty result —
"[]" reads as "worked, nothing found." If the argument was probably wrong, say so.
- Boolean flags that mean two different things —
force=true conflates "override safety" and "skip cache". Split.
Verification
Before shipping a tool schema:
- Read the description as if you were the model: does it say when to use and when NOT to use? Does each parameter description carry format, range, and an example?
- Sample 5–10 realistic prompts and confirm the model picks the right tool and fills valid arguments (record the traces).
- Force a malformed call and confirm the returned error is diagnostic — the model should self-correct on the next turn.
- If two tools have overlapping triggers, add a differentiating clause and re-test.
Pair with evidence-before-claims before declaring "the tool works" — a single successful sample is not evidence; a small held-out set is.
Companion skills
mcp-creator — for MCP-specific tool/resource contracts (execution classes, result types, transport).
agents-claude-creator / opencode-agent-creator / pi-extension-creator — when the tools live inside an agent's tools: array.
opencode-plugin-creator — for the tool() API with zod schemas and ToolContext.
evidence-before-claims — before reporting a tool schema as "correct".
loop-control-and-pivots — for the retry / non-retryable classification the error string signals to.
1---2name: tool-schema-design3description: Design LLM-callable tool signatures and JSON Schemas so the model picks the right tool and supplies valid arguments. Use when writing or reviewing function/tool definitions for OpenAI, Anthropic, Google, Bedrock, MCP servers, or any agent framework (LangChain, LangGraph, Semantic Kernel, Claude Code, OpenCode, Pi). Covers tool descriptions (what + when + when-NOT-to-use), parameter descriptions with format/range/example, JSON Schema constraints (enum, minimum, format), required-minimalism, naming consistency across a toolkit, structured outputs with strict schemas, tool_choice, parallel calls, and error responses that steer the model to recover. For portable Agent Skills use skill-creator; for MCP server architecture use mcp-creator.4license: MIT5---67# Tool Schema Design89Tool-calling reliability lives in three places: the **description** the model reads to pick the tool, the **JSON Schema** it fills to call it, and the **error response** it uses to recover. Get those three right and a well-tuned model picks the right function, supplies valid arguments, and recovers gracefully.1011## The description writes the router1213Descriptions — of tools and of parameters — are the primary lever for selection accuracy. The model disambiguates between similar tools by reading descriptions, not names.1415- **Tool description**: cover *what it does*, *when to use it*, and *when NOT to use it* (name the sibling tool the model should pick instead).16- **Parameter description**: include expected format, valid range, and a concrete example. `"The ISO 8601 start date, e.g. '2026-03-15'"` beats `"The start date"`.17- Write in a neutral, third-person voice. No marketing.1819Example:2021```json22{23 "name": "search_knowledge_base",24 "description": "Search the internal knowledge base for factual answers. Use for product documentation, policies, and FAQs. Do NOT use for real-time data like prices, stock levels, or order status — use get_product or get_order_status instead.",25 "parameters": {26 "type": "object",27 "properties": {28 "query": {29 "type": "string",30 "description": "A natural-language search query, 3–15 words. Example: 'return policy for opened electronics'."31 }32 },33 "required": ["query"]34 }35}36```3738## Schema design rules3940Every JSON Schema constraint is information the model uses to generate a valid call. Add signal, not noise.4142- **Types carry semantics**: `integer` on `quantity` tells the model fractional quantities are invalid; `number` allows them. Use the narrower type.43- **`enum` closes the space**: prefer `"enum": ["day", "week", "month"]` over `"description": "one of day, week, or month"`. The model cannot invent a fourth value.44- **`minimum` / `maximum` / `pattern` / `format`** narrow generation. `page_number` with `minimum: 1` implicitly signals one-indexed pages. `format: "date"`, `format: "uri"`, `format: "email"` are standardized.45- **Consistent naming across the toolkit**: pick one of `user_id` / `userId` / `uid` and use it everywhere. Mixing shapes lowers extraction accuracy.46- **Keep `required` minimal**. Every required field is a point of failure — if the model cannot extract or infer the value, the call fails. Give sensible defaults to non-critical fields and document the default in the description.47- **One responsibility per tool**. `create_or_update_user` is two contracts colliding; split them.48- **Nested objects sparingly**. Deep nesting increases call errors; flatten when the operational meaning survives.4950## Structured outputs, not free text5152When the response has to be machine-consumable, do not parse free text. Use provider strict-schema modes:5354- **OpenAI**: `response_format: { type: "json_schema", json_schema: { strict: true, schema: {...} } }` — the model output is grammar-constrained to the schema.55- **Anthropic**: pass a tool schema and set `tool_choice: { type: "tool", name: "..." }` to force the output shape.56- **Google Gemini**: `response_mime_type: "application/json"` with `response_schema`.5758Rules of thumb:5960- Prefer a strict schema over "please respond in JSON" in the prompt.61- Include the schema in the system prompt only if the model does not support strict mode; do not duplicate it if it does.62- Version the schema alongside the prompt.6364## `tool_choice` — the selection dial6566Providers expose it under slightly different names. The four states are the same:6768| State | When |69|---|---|70| `auto` (default) | Model decides whether to call any tool. |71| `required` / `any` | Model must call **some** tool this turn. Use to force actioning. |72| `<specific tool>` | Model must call **that** tool. Use for structured extraction and eval harnesses. |73| `none` | Model may not call tools. Use for summarization / final response steps. |7475Pin `tool_choice` at the agent step where behavior matters; leave `auto` where the model should route.7677## Error responses that steer recovery7879An error is another turn of the conversation. Return content the model can act on.8081- **Return a clear error string; do not throw** from a tool's `execute`. A thrown exception ends the loop; a returned error becomes an observation the model can react to.82- Name the failure class: `not_found`, `invalid_argument`, `rate_limited`, `permission_denied`, `upstream_error`.83- Include the offending argument value and the correction hint: `"invalid_argument: 'category'='electronic' is not in enum. Valid: ['electronics','clothing','food']."`84- For retryable failures (rate limit, transient upstream), say so and give a hint: `"rate_limited: retry after 5s"`. Combine with framework-level retry policies (see `loop-control-and-pivots`) — do not loop internally.85- Never leak stack traces, secrets, or PII into the error string; the model puts it in the response.8687## Parallel and streaming considerations8889- **Parallel tool calls** default on for OpenAI/Anthropic in 2026. Design tools to be idempotent where possible; add an idempotency key parameter when a repeat call would double-charge or double-write.90- **Long-running tools** should return a small handle (`operation_id`) plus a `get_status` companion tool, not block the loop. For MCP, use the Tasks extension when negotiated (see `mcp-creator`).91- **Streaming outputs** from a tool are rarely useful to a model that reasons on complete observations; buffer and return the final result unless the tool is streaming user-visible content the agent forwards verbatim.9293## Anti-patterns9495- **`description: "A tool for users"`** — useless; the model routes on descriptions, not names.96- **Bare `"type": "string"`** on an id, date, or enum — you gave the model no scaffolding, expect hallucinated values.97- **Overlapping tools with the same trigger words** — model routes randomly. Fix by explicit *when-NOT-to-use* on each description, or merge the tools.98- **`get_data(filters: object)` with no schema for `filters`** — the model invents keys. Enumerate the fields.99- **Everything `required`** — the model gets stuck when a field can't be inferred; a call fails instead of asking. Make optional what has a sensible default.100- **Silent success on empty result** — `"[]"` reads as "worked, nothing found." If the argument was probably wrong, say so.101- **Boolean flags that mean two different things** — `force=true` conflates "override safety" and "skip cache". Split.102103## Verification104105Before shipping a tool schema:106107- Read the description as if you were the model: does it say *when to use* and *when NOT to use*? Does each parameter description carry format, range, and an example?108- Sample 5–10 realistic prompts and confirm the model picks the right tool and fills valid arguments (record the traces).109- Force a malformed call and confirm the returned error is diagnostic — the model should self-correct on the next turn.110- If two tools have overlapping triggers, add a differentiating clause and re-test.111112Pair with `evidence-before-claims` before declaring "the tool works" — a single successful sample is not evidence; a small held-out set is.113114## Companion skills115116- `mcp-creator` — for MCP-specific tool/resource contracts (execution classes, result types, transport).117- `agents-claude-creator` / `opencode-agent-creator` / `pi-extension-creator` — when the tools live inside an agent's `tools:` array.118- `opencode-plugin-creator` — for the `tool()` API with zod schemas and `ToolContext`.119- `evidence-before-claims` — before reporting a tool schema as "correct".120- `loop-control-and-pivots` — for the retry / non-retryable classification the error string signals to.