It is read at decision time, by the only reader that matters
Developers write tool descriptions the way they write docstrings: a sentence saying what the function does, on the assumption that someone reading the code will work out the rest from the signature and the implementation.
The model has no access to the implementation. It has the name, the description, and the parameter descriptions. That is the complete evidence on which it decides whether this tool answers the user’s question. The description is not documentation. It is a prompt, it is the only prompt, and it is being evaluated against every other tool’s prompt simultaneously.
Compare:
"description": "Search orders.""description": "Search a customer's orders. Covers only the last 90 days — for anything older use get_archived_order with an exact order id. Returns at most 20 results, newest first, so a broad date range may silently omit older matches. Returns an empty list when the customer has no orders in range; an empty list does not mean the customer does not exist."The second is four times the tokens. It also prevents four specific failures, and each of them is one you would otherwise spend an afternoon reproducing.
The failures a thin description causes
Silent range truncation. With “Search orders”, the model asks about an order from last year, gets an empty list, and tells the customer there is no such order. The tool worked perfectly. The answer is false, the model is not at fault, and no error was raised anywhere in the stack. This is the single most common tool bug in production systems, and it lives entirely in the description.
Result-limit blindness. A tool that returns the top 20 of 300 matches, without saying so, produces confident aggregate claims (“you have 20 orders this year”) from a truncated list. State the cap and state what it means.
Empty versus absent. An empty result can mean no matches, no permission, or no such entity. A model reading an empty array will guess, and it tends to guess the most conversationally natural option, which is usually “it does not exist”. If your tool can return empty for more than one reason, either say which in the description or, better, return distinguishable results.
Overlap. Two tools whose descriptions could both plausibly serve a request produce inconsistent routing: the same question picks differently across runs. The fix is not a longer description of each, it is one sentence in each that names the other. “Use search_orders for browsing by date or status; use get_order when you already have an exact order id.” A description that mentions its sibling resolves the ambiguity at the point where the ambiguity exists.
Parameter descriptions carry as much weight
The parameters are where malformed calls come from, and the cause is almost always an unstated format.
"since": {"type": "string"}This will receive "last Tuesday", "01/07/2026", "July" and "2026-07-01", depending on how the user phrased the question. Three of those four will fail or, worse, parse into the wrong date: 01/07/2026 is January in one convention and July in another, and your parser has an opinion it has never told you.
"since": {
"type": "string",
"description": "ISO 8601 date only, YYYY-MM-DD, e.g. 2026-07-01. Do not pass relative expressions; resolve them against today's date first."
}Add a pattern if the provider supports it in the schema, so constrained decoding enforces the shape rather than relying on the sentence. Belt and braces: the description teaches, the pattern guarantees.
The same applies to units and identifiers. An amount field should say whether it is currency-major or minor units, because a model that guesses minor units will refund a hundredth of what it should and nothing in the type system will notice.
Tell it when not to call
The most valuable sentence in most descriptions is the negative one. Models are biased towards action: a tool exists, the tool is roughly on topic, so the tool gets called. That produces the pattern where a question the model could answer becomes a round trip that returns nothing useful and then gets answered anyway, at double the latency and double the cost.
Name the non-cases explicitly. “Do not call this to check whether a customer exists; use get_customer.” “Do not call this for questions about delivery times, which are in the policy documents already in context.”
Measure the routing, do not admire the prose
Writing good descriptions feels like editing, and editing feels like progress. The only thing that tells you whether it worked is a routing test.
Build a file of representative user messages with the tool you believe each should trigger, including the ones where the answer is “no tool”:
"Where's my order from March?" -> get_archived_order
"Has anything shipped this week?" -> search_orders
"What's your returns policy?" -> none
"Refund order ORD-88213" -> issue_refundThirty lines is enough to be useful. Run them, compare the tool actually called against the expected one, and print the mismatches. It is a dozen lines of script and it runs for a few cents.
Then (and this is the part that makes it an engineering practice rather than a one-off) run it again on every change to the toolset. Adding a tool changes the routing of tools you did not touch, because you changed the set of alternatives. That effect is invisible in code review and obvious in the test.
The limitation to hold onto: this measures routing against your own expectations, and your expectations can be wrong. When the model picks a tool you did not expect, check whether it was actually a better choice before you edit the description to stop it. Roughly one time in five, it was.
Every lesson here is open. Your progress is recorded once you pass the Data, SQL and Getting to the Answer exam.
Check your understanding
A tool described only as “Search orders” silently covers just the last 90 days. A user asks about an order from last year. What is the most likely observable outcome?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Sayali, your AI tutor
