Skip to main content

Designing Good Tool Schemas

Introduction

Last lesson, you learned that the model requests a tool. This lesson is about the thing that decides whether it requests the right tool with the right arguments — or flails, hallucinates IDs, and burns tokens: how you design the tool itself.

Here's the reframe that changes everything: a tool definition is the highest-leverage prompt you will write. The model never sees your function's code — it sees only the name, description, and schema. Those few lines are the entire interface between a probabilistic model and your real systems. Anthropic found that small refinements to tool descriptions were enough to push Claude to state-of-the-art on the SWE-bench coding benchmark — same model, better tools.

In this lesson you'll learn the principles the best agent builders use:

  • Why the description is a prompt, not a comment — and how to write it
  • Naming and semantic IDs that stop the model from hallucinating
  • The biggest design mistake: wrapping your API 1:1 instead of designing for the task
  • Returning high-signal results, and writing errors that teach
  • Namespacing, how many tools is too many, and iterating with evals

The Description Is a Prompt, Not a Comment

Treat every tool description the way you'd brief a new teammate on their first day. They're smart, but they don't know your domain's implicit context — your query formats, your jargon, how your resources relate. Spell it out.

The canonical rule: be descriptive and business-focused, not technical. Compare:

  • "Execute order query." — tells the model nothing about when or how to use it.
  • "Search for a customer's orders by date range, status, or total. Returns order details including items, shipping, and payment info."

The second version tells the model exactly when to reach for the tool and what it gets back — so it chooses correctly and knows how to use the result. A good description usually covers: what the tool does, when to use it (and when not to), and the shape of what it returns.

Because the model reads this text to make its decision, a few words of clarity here outperform paragraphs of system-prompt instructions trying to corral it after the fact.

An infographic titled 'Designing Tools the Model Uses Well' comparing a bad tool and a good tool. The left red card 'Wraps your API' (1:1 with an endpoint) has name run_query, a vague description 'Executes a query.', a parameter u typed as a raw UUID, a returns list dumping id, uuid, mime_type, 256px_url and raw rows, and an opaque error 'Error 500'; its verdict: the model picks the wrong tool, hallucinates the UUID, and drowns in tokens. The right green card 'Designed for the task' has name search_orders, a rich description, parameters customer_id plus a status enum (open, shipped, cancelled) plus a response_format enum, returns only high-signal fields, and a teaching error 'No orders for customer_id — pass the numeric id, not the email'; its verdict: the model calls confidently with valid args and gets a clean, cheap result. Below are five rules: description is a prompt, semantic IDs beat UUIDs, enums plus strict constrain inputs, a task-shaped tool beats many endpoint wrappers, and return high-signal results with errors that teach. A bottom banner: the tool definition is the prompt the model reads to act — design for the task, then refine with evals.

Name Things So the Model Can't Misread Them

Names and types are not cosmetic — they're how you constrain the model at the schema level, before it ever generates an argument. Recall the structured-output lessons: the schema is your guarantee on shape. Use it.

Unambiguous parameter names. A parameter called user invites guesswork — a name? an email? an object? Call it user_id and the ambiguity vanishes.

Prefer semantic IDs over cryptic ones. This is a subtle, high-impact finding: models handle human-readable names and identifiers far better than opaque UUIDs. Resolving an arbitrary a3f8c2e1-… to a meaningful name (or even a simple 0-indexed #3) significantly reduces hallucination in retrieval tasks. If your tool must take a UUID, consider accepting the human name and resolving it internally.

Constrain with enums + descriptions + strict. Don't let the model invent status="in progress" when your system only knows open|shipped|cancelled. Put those in an enum, describe each field, and turn on strict so the arguments are guaranteed to fit the schema.

# The schema itself prevents whole classes of bad calls
{
    "name": "update_order_status",
    "description": "Set an order's fulfillment status. Use after a shipment or cancellation is confirmed.",
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id":  {"type": "string", "description": "Numeric order id, e.g. '10293' (not the order URL)"},
            "new_status": {
                "type": "string",
                "enum": ["open", "shipped", "cancelled"],   # ← model can't invent a value
                "description": "open = not yet shipped; shipped = handed to carrier; cancelled = refunded",
            },
        },
        "required": ["order_id", "new_status"],
        "additionalProperties": False,
    },
}

Design Tools for the Task, Not Your API

This is the single biggest design mistake — and the one that separates beginner agents from reliable ones: don't expose one tool per API endpoint.

It feels natural (you already have GET /users, GET /events, POST /events), but it pushes all the orchestration onto the model: list users, find the right one, list their events, check availability, then create. Every hop is a chance to pick the wrong tool or stall. Too many overlapping tools actively distract the agent from an efficient path.

Instead, build tools shaped like the task — each one can do several API calls under the hood:

  • list_users + list_events + create_event → ✅ schedule_event (finds availability and books it)
  • get_customer_by_id + list_transactions + list_notes → ✅ get_customer_context (compiles the recent picture in one call)
  • read_logs (returns everything) → ✅ search_logs (returns just the relevant lines + surrounding context)

Fewer, higher-level tools mean fewer decisions for the model, fewer round-trips, and far less that can go wrong. Ask not "what does my API expose?" but "what does the agent actually need to accomplish?"

Return High-Signal Results (and Errors That Teach)

Everything a tool returns lands back in the model's context — where it costs tokens, latency, and attention. So return only what the agent needs to take the next step.

Drop low-level noise. The model rarely needs uuid, mime_type, or 256px_image_url. It needs name, image_url, file_type. Prioritise contextual relevance over completeness.

Give the agent a verbosity dial. A response_format enum (concise | detailed) lets the model ask for only what it needs — a concise response can use a third of the tokens of a detailed one. Add pagination, filtering, and truncation with sensible defaults so a tool can't dump 10,000 rows into context.

Errors are prompts too. When a call fails, an opaque Error 500 or a raw traceback teaches the model nothing — it'll retry the same broken call. Instead, write the error like an instruction:

# ❌ opaque — the model just retries blindly
Error: 400 Bad Request

# ✅ actionable — the model self-corrects on the next turn
No customer found for customer_id='alice@acme.com'. Pass the numeric
customer_id (e.g. '4471'), not the email. Use search_customers to look it up first.

A well-written error message steers the model toward the fix — the right format, a filter, a prerequisite tool — turning a dead end into a self-correction.

Namespacing, How Many Tools, and Iterating with Evals

Namespace related tools. Prefixes group them and cut confusion: asana_search, jira_search — or asana_projects_search, asana_users_search. (Prefix vs suffix naming measurably affects tool-use accuracy, and the winner varies by model — so test it.)

Keep the set small and sharp. A long tool list means more options for the model to weigh on every turn — more latency, more chances to pick wrong. If you have many tools, defer the rarely-used ones (load them only when relevant) rather than presenting all of them up front.

Then iterate with evals — this is the real workflow. You won't get tool design right on paper. Prototype the tool, run an eval of realistic tasks (each needing several tool calls), watch where the agent stumbles, refine the description/schema/returns, and re-run. Hold out a test set so you don't overfit. The gains compound — and you can even paste your eval transcripts back to a model and ask it to improve the tools for you.

🧪 Try It Yourself

Redesign a bad tool. Here's a tool a teammate wrapped straight from the API:

name: getData
desc: "Gets data."
params: { id: string (a UUID), type: string }
returns: full raw row incl. uuid, created_ts, _internal_flags, blob_url

Spot at least four problems and rewrite it. Check yourself against these:

  1. NamegetData says nothing about the task → name it for what it does (e.g. get_invoice).
  2. Description — "Gets data" gives the model no when/what → describe when to use it and what it returns.
  3. type: string — unconstrained → make it an enum of the real values.
  4. id: UUID — invites hallucination → accept a semantic id, or resolve a name internally.
  5. Returns — dumps uuid, _internal_flags, blob_url → return only high-signal fields (+ a response_format dial).

If you fixed those five, you've internalised the whole lesson.

Tool Schema Doctor — a teammate wrapped a tool straight from the API (getData, “Gets data.”, a raw-UUID id, an unconstrained type string, a dumped raw row, and an opaque “Error 400”). Apply the six fixes one at a time — name it for the task (get_invoice), write a real description, enum the type field, accept a semantic id (INV-4471) instead of a UUID, return only high-signal fields with a response_format dial, and write a teaching error — and watch the tool definition transform bad→good live while an agent-reliability meter climbs from flailing (picks the wrong tool, hallucinates the UUID, drowns in tokens) to calling confidently. Lands the lesson: the name, description, and schema are the entire prompt the model acts on, and tool quality is the cheapest, biggest lever on agent reliability.

Mental-Model Corrections

  • "The schema is just plumbing." No — the name + description + schema are the prompt the model uses to act. They're the highest-leverage text in your whole agent.
  • "More tools = more capable." Usually the opposite. Overlapping, fine-grained tools distract the model and add round-trips. Fewer, task-shaped tools win.
  • "UUIDs are fine, they're precise." Precise for you, hallucination-bait for the model. Prefer human-readable/semantic identifiers; resolve opaque ids internally.
  • "Return everything so the model has options." Every field costs tokens and attention. Return high-signal data; add filtering/pagination/verbosity controls.
  • "An error is just a failure." An error is a prompt. Make it tell the model exactly how to succeed next time.
  • "I can design tools by reasoning alone." You can't — tool design is empirical. Refine against an eval, every time.

Key Takeaways

  • The tool definition is a prompt. Name, description, and schema are all the model sees — write them with the same care as any prompt.
  • Description: brief it like a new teammate — what it does, when to use it, what it returns. Small refinements yield big gains.
  • Constrain at the schema: unambiguous names, enums, per-field descriptions, strict. Prefer semantic IDs over UUIDs to cut hallucination.
  • Design for the task, not your API: one schedule_event beats list_users + list_events + create_event. Fewer, higher-level tools = fewer ways to fail.
  • Returns & errors are context: return only high-signal fields (+ verbosity/pagination), and write errors that teach the fix.
  • Namespace, keep the set small, and iterate with evals — held-out tasks, refine, re-run. Tool quality is the cheapest, biggest lever on agent reliability.