{"course":{"slug":"context-engineering","title":"Context Engineering","url":"https://addaly.com/learn/context-engineering","level":"advanced","track":"build"},"lesson":{"slug":"tool-descriptions-as-routing","title":"The description is the prompt that picks the tool","url":"https://addaly.com/learn/context-engineering/tool-descriptions-as-routing","position":61,"of":87,"minutes":9,"answer":"A tool description is not documentation for a developer but the entire basis on which the model decides to call it, so it must state the boundaries.","keyIdea":"A tool description is not documentation for a developer but the entire basis on which the model decides to call it, so it must state the boundaries, the limits and the cases where the tool is the wrong choice."},"body":{"format":"markdown","text":"## It is read at decision time, by the only reader that matters\n\nDevelopers 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.\n\nThe 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.\n\nCompare:\n\n```\n\"description\": \"Search orders.\"\n```\n\n```\n\"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.\"\n```\n\nThe 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.\n\n## The failures a thin description causes\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n## Parameter descriptions carry as much weight\n\nThe parameters are where malformed calls come from, and the cause is almost always an unstated format.\n\n```json\n\"since\": {\"type\": \"string\"}\n```\n\nThis 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.\n\n```json\n\"since\": {\n  \"type\": \"string\",\n  \"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.\"\n}\n```\n\nAdd 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.\n\nThe 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.\n\n## Tell it when not to call\n\nThe 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.\n\nName 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.\"\n\n## Measure the routing, do not admire the prose\n\nWriting good descriptions feels like editing, and editing feels like progress. The only thing that tells you whether it worked is a routing test.\n\nBuild a file of representative user messages with the tool you believe each should trigger, including the ones where the answer is \"no tool\":\n\n```\n\"Where's my order from March?\"          -> get_archived_order\n\"Has anything shipped this week?\"        -> search_orders\n\"What's your returns policy?\"            -> none\n\"Refund order ORD-88213\"                 -> issue_refund\n```\n\nThirty 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.\n\nThen (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.\n\nThe 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."},"check":{"question":"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?","options":[{"text":"The tool raises an out-of-range error, which the model reports to the user.","correct":false,"tempting":"Assumes the tool validates a range it was never told to treat as meaningful; an in-range query returning nothing is a normal result, not an error."},{"text":"The model refuses to call the tool, because the date falls outside a window it can infer from the parameter type.","correct":false,"tempting":"Types carry no semantics about coverage; nothing in the schema hints the tool is time-bounded."},{"text":"The tool returns an empty list and the model tells the user no such order exists.","correct":true},{"text":"The call fails schema validation, because the date is older than the tool's supported range and constrained decoding blocks it.","correct":false,"tempting":"Constrained decoding enforces the shape of a value, never its business meaning; a valid ISO date is valid whatever it refers to."}]},"prev":{"slug":"tool-definitions-are-context","title":"Every tool costs tokens before anyone calls it","url":"https://addaly.com/learn/context-engineering/tool-definitions-are-context"},"next":{"slug":"tool-results-are-unbounded","title":"The biggest thing in the window is a tool result","url":"https://addaly.com/learn/context-engineering/tool-results-are-unbounded"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The description is the prompt that picks the tool', in Context Engineering. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/context-engineering/tool-descriptions-as-routing (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The description is the prompt that picks the tool","courseTitle":"Context Engineering","module":{"index":7,"slug":"tools-schemas-and-the-agent-window","title":"Tools, schemas and the agent’s window"},"answer":null,"keyIdea":"A tool description is not documentation for a developer but the entire basis on which the model decides to call it, so it must state the boundaries, the limits and the cases where the tool is the wrong choice.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"it-is-read-at-decision-time-by-the-only-reader-that-matters","runs":[{"text":"It is read at decision time, by the only reader that matters"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"Compare:"}]},{"t":"code","lang":"","label":"Text","text":"\"description\": \"Search orders.\""},{"t":"code","lang":"","label":"Text","text":"\"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.\""},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"the-failures-a-thin-description-causes","runs":[{"text":"The failures a thin description causes"}]},{"t":"p","runs":[{"text":"Silent range truncation.","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"Result-limit blindness.","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"Empty versus absent.","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"Overlap.","b":true},{"text":" 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 "},{"text":"search_orders","code":true},{"text":" for browsing by date or status; use "},{"text":"get_order","code":true},{"text":" when you already have an exact order id.” A description that mentions its sibling resolves the ambiguity at the point where the ambiguity exists."}]},{"t":"h2","id":"parameter-descriptions-carry-as-much-weight","runs":[{"text":"Parameter descriptions carry as much weight"}]},{"t":"p","runs":[{"text":"The parameters are where malformed calls come from, and the cause is almost always an unstated format."}]},{"t":"code","lang":"json","label":"JSON","text":"\"since\": {\"type\": \"string\"}"},{"t":"p","runs":[{"text":"This will receive "},{"text":"\"last Tuesday\"","code":true},{"text":", "},{"text":"\"01/07/2026\"","code":true},{"text":", "},{"text":"\"July\"","code":true},{"text":" and "},{"text":"\"2026-07-01\"","code":true},{"text":", depending on how the user phrased the question. Three of those four will fail or, worse, parse into the wrong date: "},{"text":"01/07/2026","code":true},{"text":" is January in one convention and July in another, and your parser has an opinion it has never told you."}]},{"t":"code","lang":"json","label":"JSON","text":"\"since\": {\n  \"type\": \"string\",\n  \"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.\"\n}"},{"t":"p","runs":[{"text":"Add a "},{"text":"pattern","code":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"tell-it-when-not-to-call","runs":[{"text":"Tell it when not to call"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"Name the non-cases explicitly. “Do not call this to check whether a customer exists; use "},{"text":"get_customer","code":true},{"text":".” “Do not call this for questions about delivery times, which are in the policy documents already in context.”"}]},{"t":"h2","id":"measure-the-routing-do-not-admire-the-prose","runs":[{"text":"Measure the routing, do not admire the prose"}]},{"t":"p","runs":[{"text":"Writing good descriptions feels like editing, and editing feels like progress. The only thing that tells you whether it worked is a routing test."}]},{"t":"p","runs":[{"text":"Build a file of representative user messages with the tool you believe each should trigger, including the ones where the answer is “no tool”:"}]},{"t":"code","lang":"","label":"Text","text":"\"Where's my order from March?\"          -> get_archived_order\n\"Has anything shipped this week?\"        -> search_orders\n\"What's your returns policy?\"            -> none\n\"Refund order ORD-88213\"                 -> issue_refund"},{"t":"p","runs":[{"text":"Thirty 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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"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."}]}],"check":{"question":[{"text":"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?"}],"options":[{"text":[{"text":"The tool raises an out-of-range error, which the model reports to the user."}],"correct":false,"tempting":[{"text":"Assumes the tool validates a range it was never told to treat as meaningful; an in-range query returning nothing is a normal result, not an error."}]},{"text":[{"text":"The model refuses to call the tool, because the date falls outside a window it can infer from the parameter type."}],"correct":false,"tempting":[{"text":"Types carry no semantics about coverage; nothing in the schema hints the tool is time-bounded."}]},{"text":[{"text":"The tool returns an empty list and the model tells the user no such order exists."}],"correct":true,"tempting":null},{"text":[{"text":"The call fails schema validation, because the date is older than the tool’s supported range and constrained decoding blocks it."}],"correct":false,"tempting":[{"text":"Constrained decoding enforces the shape of a value, never its business meaning; a valid ISO date is valid whatever it refers to."}]}],"order":[0,3,1,2],"why":null},"prev":{"slug":"tool-definitions-are-context","title":"Every tool costs tokens before anyone calls it","minutes":9},"next":{"slug":"tool-results-are-unbounded","title":"The biggest thing in the window is a tool result","minutes":9}}}