Your Tool Description Is a Prompt: Rewriting Schemas the Model Can Actually Follow

A three-word tool description sent an inbox agent after the wrong thread. Tool specs live inside the prompt, and five specific rewrites to yours will fix more wrong calls than any system-prompt rule.

An inbox assistant archived the wrong thread. The trace shows a clean call to archive_thread(id="t_9931") — well-formed, valid ID, no error. It simply picked a thread the user never asked about.

The instinct is to reach for the system prompt and add a rule. The actual defect was three words long. The tool's description read: "Archives a thread."

Tool specs are not documentation that sits beside the prompt. They are inside the prompt — the name, the description, every parameter name, every enum value, every default. That text is doing as much work as your system prompt, and it usually gets a fraction of the attention.

What the model is working from

At call time the model sees roughly this, and nothing else:

archive_thread
  Archives a thread.
  id: string

From that it must infer: which threads are eligible, whether archiving is reversible, whether it needs confirmation first, what id refers to (a thread? a message? the sender?), and what happens after. None of that is stated, so it gets guessed. A guess that lands wrong looks exactly like a reasoning failure in the trace, which is why these get misdiagnosed.

Five rewrites that change behavior

1. Describe when to call it, and when not to

The single highest-leverage change. A description that only says what the function does leaves selection entirely to inference.

Archives one email thread, removing it from the inbox. Reversible — an archived thread can be restored with restore_thread. Use only for threads the user has explicitly named or that match a filter the user described in this conversation. Do not archive threads that are unread, that mention an unresolved question, or that arrived in the last hour unless the user says so.

That paragraph eliminates a whole class of wrong calls without touching the system prompt.

2. Parameter names are documentation

id forces a guess. thread_id does not. Renaming a parameter is the cheapest precision you will ever buy — the name appears in every call the model composes, while a description has to be recalled.

3. State what comes back

The model plans the next step from the return value. If it can't predict the shape, it either calls the tool speculatively to find out, or invents a follow-up that doesn't fit.

Returns {archived: true, thread_id, restored_from_inbox: false}. On a thread that is already archived, returns {archived: true, no_op: true} rather than an error.

4. Put constraints in the schema, not in prose

Anything expressible as a type or an enum belongs there. Prose constraints are advisory; schema constraints are enforced before the call ever runs.

Before → after:

before:
  filter: string      # "Options: unread, starred, older_than_30d"

after:
  filter:
    type: string
    enum: [unread, starred, older_than_30d]
  older_than_days:
    type: integer
    minimum: 1
    maximum: 365

The before version invites filter: "unread emails from last week", which parses as a string and fails somewhere downstream. The after version cannot express that at all.

5. Disambiguate siblings explicitly

When two tools overlap, say so in both descriptions. search_threads and list_recent_threads will be confused forever until one of them reads: "For a specific query or sender, use search_threads. This tool only returns the most recent N threads in arrival order and ignores content."

🔍 Telling whether descriptions are your problem

A quick diagnostic. Replace each tool's implementation with a stub that logs the arguments and returns a plausible fake result, then run your eval set against the stubs.

  • Wrong arguments to the right tool → the parameter names and schema constraints need work.
  • Right arguments to the wrong tool → the descriptions don't separate the tools' territories.
  • Right tool, right arguments, wrong order → the missing piece is what each tool returns, so the model can't sequence them.

This separates tool-surface defects from model or prompt defects in a few minutes, and it runs without touching any real data.

What not to do

  • ❌ Paste your API reference into the description. Long descriptions crowd the context and bury the decision criteria. Aim for a short paragraph that answers when, what it returns, and what not to use it for.
  • ❌ Ship forty narrow tools because each one is well described. Selection accuracy degrades as the list grows; consolidate variations into one tool with an enum instead.
  • ❌ Encode tool rules in the system prompt. A rule about archive_thread lives with archive_thread — otherwise it applies even in conversations where the tool is never offered, and it drifts out of sync when the tool changes.

The takeaway

Before adding a guardrail, a validation layer, or a stronger model, read your tool specs as the model receives them: name, one paragraph, parameter names, types. If a competent new colleague couldn't pick the right tool and fill in the arguments from that alone, the model can't either — and the fix is a rewrite that takes ten minutes, not an architecture change.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.