Tutorial
How to design tools an AI agent can actually use
Most agent failures are tool design failures. Naming, arguments, error messages, and why wrapping your REST API one-to-one produces a bad agent.
When you’re designing tools for LLM agents, the instinct is to expose your existing API — one tool per endpoint, arguments matching the request body, done. It works in a demo and degrades badly in practice, because a REST API is designed for a programmer who read the docs and an agent is a reader who gets one pass at a description.
Most behaviour people blame on the model is really tool design. Here’s what changes when the caller is a language model.
Name for the task, not the resource
create_calendar_event beats POST /events. find_free_time beats list_availability.
The model matches on semantics, so the name is your highest-leverage field. Use verb-first names describing user intent. Avoid internal jargon — if a tool is called sync_entity_graph, the model has no idea when to reach for it, and neither does a new engineer.
Be especially careful with near-synonyms. search_messages and find_messages sitting side by side will be confused, reliably, forever. If you can’t explain the difference in one clause, merge them.
Fewer, wider tools
The naive mapping gives you one tool per endpoint and 200 tools. Selection accuracy falls off a cliff well before that, and the failures are the confusing kind — plausible wrong tool, confident execution.
Collapse aggressively. Five variations on “list things with different filters” become one tool with a filter argument. Read-modify-write sequences that always occur together become one tool that does the whole thing, which also removes a class of partial-failure state.
The counter-pressure: don’t collapse across trust boundaries. manage_email that can read or delete is a bad tool, because you can no longer gate on what it does. Keep reads and destructive writes as separate tools even when they’d otherwise merge — the gate depends on that separation.
Beyond a few dozen tools, stop trying to fit them all in context and retrieve a relevant subset per turn.
Make arguments hard to get wrong
Every argument is a chance for the model to guess. Reduce the guessing.
Prefer enums to free strings. status: "unread" | "starred" | "archived" cannot be misspelled. status: string will be, in every casing you didn’t think of.
Accept natural forms. The model has “next Tuesday”, not an ISO timestamp. Either accept a relative-date string and resolve it server-side, or supply the current datetime in context. Making the model do date arithmetic is asking for an off-by-one that books a meeting on the wrong day.
Avoid opaque ids as inputs where you can. A tool requiring thread_id forces a lookup call first, and the model may invent one. Accept a natural key and resolve internally, or return ids from a companion search tool and say so in the description.
Default anything defaultable. Every optional argument with a sensible default is one less decision.
Descriptions are the interface
The description is the only documentation the model gets. Write it for someone competent who has never seen your system.
State what the tool does, when to use it, when not to, and what it returns. That third part is the one everyone skips and the one that most reduces wrong calls: “Use send_email only when the user explicitly asked to send. To prepare a message for review, use draft_email.”
Put constraints in the description, not only in validation. “Maximum 50 recipients” in the text prevents the call; a 400 back merely wastes a round trip.
Errors are prompts
This is the highest-return change and the least done.
An agent reads your error and decides what to do next. 400 Bad Request gives it nothing, so it retries the same call. Write errors that contain the fix:
Bad: {"error": "invalid_argument"}
Good: {"error": "No calendar named 'Work'. Available: 'Personal',
'Team'. Retry with one of these."}
The good version resolves itself on the next call. Include what was wrong, what valid values exist, and whether retrying is worthwhile — a “this will never succeed” signal stops a retry loop that would otherwise burn your budget.
Return less than you think
Dumping a full API response into context is the most common cause of a run that gets slower and dumber as it proceeds.
Return the fields needed for the next decision. A message list needs sender, subject, date and id — not full bodies, headers and MIME parts. If the agent needs the body, that’s a second tool call for one message, which is cheaper than fifty bodies it never reads.
Cap results and say so explicitly: "showing 20 of 340 matches" lets the model narrow its query. A silently truncated list makes it conclude there are 20.
Mark the effect
Every tool should declare what it does to the world: read-only, reversible write, irreversible or outbound. This is metadata your runtime enforces, not advice to the model — the gate has to hold whether or not the model cooperates.
Get this right and the security posture follows: reads run free, reversible writes run and log, irreversible actions stop for a human.
The checklist
- Verb-first names describing intent.
- Merge near-synonyms; never across trust boundaries.
- Enums over free strings; defaults everywhere possible.
- Accept natural inputs, resolve ids internally.
- Descriptions say when not to use the tool.
- Errors name the fix and whether to retry.
- Return the minimum, and label truncation.
- Declare the effect class; enforce it in the runtime.
If you only do two, do 6 and 7 — they compound over every call in every run.
See also: what to keep in context as a run grows and testing that a tool is never called.