Writing Tool Descriptions the Model Will Actually Use Correctly
Models pick tools from names, descriptions, and parameter docs, not from your internal wiki.
Vague or overlapping text produces wrong tools, skipped tools, or invented arguments. This page is a recipe for wording that steers selection without turning every schema into a novel.
Summary
Write each tool as a mini-spec: what it does, when to use it, when not to use it, and what each parameter means with units and examples. Keep descriptions non-overlapping across the tool list, and verify selection with golden prompts.
Recipe
- Name the tool as a verb_noun action (
get_order,create_refund), not a vague noun (data,helper). - Open the description with one sentence: capability + primary trigger.
- Add a second sentence for non-goals ("Not for historical analytics"; "Not for password reset").
- Document every parameter with type, meaning, format, and a short example value.
- Mark required vs optional deliberately; optional fields need defaults the model can omit safely.
- Disambiguate siblings: if two tools are similar, state the decision rule in both descriptions.
- Cap description length; move long policy to the system prompt only when it applies to all tools.
- Run a small eval set: prompts that should call A, call B, call none, or call A then B.
- Fix failures by tightening wording before adding more tools.
- Re-audit when you add, rename, or deprecate tools.
Working Example
TOOLS = [
{
"type": "function",
"function": {
"name": "get_order_status",
"description": (
"Fetch live fulfillment status for one order by numeric id. "
"Use when the user asks where an order is, ETA, or shipped/delivered state. "
"Not for placing orders, cancellations, or multi-order reports."
),
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "integer",
"description": "Numeric order id, e.g. 1042. Digits only; no # prefix.",
}
},
"required": ["order_id"],
"additionalProperties": False,
},
},
},
{
"type": "function",
"function": {
"name": "search_orders_by_email",
"description": (
"List recent orders for a customer email when the order id is unknown. "
"Use to discover order ids. For status of a known id, call get_order_status instead."
),
"parameters": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "Customer email, e.g. ada@example.com",
},
"limit": {
"type": "integer",
"description": "Max rows to return (1-20). Default 5.",
"minimum": 1,
"maximum": 20,
},
},
"required": ["email"],
"additionalProperties": False,
},
},
},
]The pair above encodes a clear handoff: search when id is missing, status when id is known.
Deep Dive
What the model optimizes for
On each turn the model scores candidate actions against the user goal and the tool list in context. Descriptions that share adjectives ("gets data", "manages orders") collapse into noise. Specific triggers ("when the user lacks an order id") create separable decision boundaries.
Name quality
| Pattern | Example | Effect |
|---|---|---|
| verb_noun | cancel_order | Clear side effect |
| vendor jargon | oms_v2_mut | Model may avoid or misuse |
| overloaded | handle | Attracts every task |
| symmetric pair | get_* / list_* | Helps read vs search |
Prefer stable names. Renaming mid-flight breaks prompts, evals, and MCP clients that cached the old name.
Description layers
- Capability - what the side effect or read returns.
- Trigger - user intents that should select it.
- Exclusion - intents that should use something else or refuse.
- Postcondition - what success looks like in the observation (optional, short).
You rarely need more than 2-4 sentences. If policy is long, put shared rules in the system prompt and keep tool text local.
Parameter descriptions that fill correctly
- State units (
cents,ISO-8601 UTC,IANA timezone). - State formats (
E.164 phone,ISO country code). - Give one example inline for tricky fields.
- Use
enumwhen the set is small and closed. - Avoid dual meaning fields (
valuethat might be id or name).
Overlap is the main failure mode
Symptoms:
- Model alternates between two tools on retries
- Model calls both when one would do
- Model invents a hybrid argument set
Fixes:
- Merge tools that always run together
- Split tools that do opposite side effects
- Write explicit "use X instead when..." cross-links in both descriptions
System prompt vs tool description
| Put in system prompt | Put in tool description |
|---|---|
| Global safety and tone | When to pick this tool |
| Max refund policy overview | Parameter constraints for this call |
| "Prefer tools over memory for live data" | What this tool is not for |
Duplicating a long policy in every tool wastes tokens and drifts.
Eval the wording
CASES = [
{"prompt": "Where is order 1042?", "expect_tool": "get_order_status"},
{"prompt": "What orders does ada@example.com have?", "expect_tool": "search_orders_by_email"},
{"prompt": "Write a haiku about shipping", "expect_tool": None},
]
# Run each through your agent with stubbed tools; assert first tool name.Treat description edits like code: small change, re-run the suite.
Gotchas
- Poetry instead of triggers. "Helpful utility for customers" does not select anything.
- Hidden required context. If the tool needs
tenant_id, require it or inject it host-side; do not hope the model invents it. - Copy-pasted descriptions. Two tools with the same blurb guarantee confusion.
- Enums in prose only. If values are closed, put them in JSON Schema
enum, not only in English. - Side-effect understatement. Write tools that charge money or delete data must say so plainly.
- Description rot. Product changes while schemas stay old; schedule re-audits.
- Too many tools in one agent. Even perfect prose fails when twenty near-duplicates compete.
Alternatives
| Approach | When it wins | When it loses |
|---|---|---|
| Careful native descriptions (this recipe) | Small-medium tool sets | Extremely large catalogs |
| Tool search / deferred loading | Huge libraries | Extra hop latency |
| Router agent picking a specialist | Distinct domains | Overhead for simple apps |
Single mega-tool with a action enum | Tiny prototypes | Becomes an untyped RPC mess |
| Free-form text tools | Legacy models without function calling | Brittle parsing |
FAQs
How long should a tool description be?
Usually 1-4 sentences. If you need a page, the tool is probably too broad or policy belongs in the system prompt.
Should parameter descriptions repeat the type?
Prefer meaning, format, and examples. The schema already carries type; English should add what the type cannot.
Do examples in descriptions overfit the model?
Short examples help format. Avoid implying the only valid business case is the example case.
Is English quality model-dependent?
Stronger models tolerate mess better, but clear boundaries help every model and reduce cost from repair turns.
Should I mention error codes in the description?
Only high-level ("returns not_found when id is unknown"). Detailed error shaping belongs in tool results; see the errors page in this section.
Can I use markdown inside descriptions?
Plain sentences travel best across providers. Fancy markdown is not guaranteed to help selection.
What if marketing names differ from API names?
Map user language in the description ("invoice number is order_id") so the model can translate.
How often should we re-eval tool wording?
On every tool add/rename and when production shows rising wrong-tool rates.
Related
- How Function Calling Actually Works Under the Hood - protocol context
- Tool Use & Function Calling Basics - first schemas
- JSON Schema for Tool Parameters: A Reference Guide - schema knobs
- Tool Search: Loading Tool Definitions On Demand Instead of Upfront - large catalogs
- Tool Use & Function Calling Best Practices - broader habits
- What Makes an Internal API a Good Agent Tool - API fitness
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.