How to design tool schemas that AI agents use correctly
When a model decides to call a tool, it has no source code to read and no teammate to ask. It has a name, a description and a JSON schema. Most wrong tool calls trace back to those few lines. This post is a practical guide to writing them: how to name tools, how to treat descriptions as prompts, how to use enums and required fields, what to return, how to handle retries and what an error message should say. It ends with a before and after schema you can adapt.
Key takeaways
- The model sees only your tool's name, description and schema, so those three things are the whole interface.
- Use specific verb-noun names, constrain inputs with enums and required fields, and say when not to use a tool.
- Return compact, useful outputs and errors that tell the model what to do next.
- Make write tools safe to retry with idempotency keys or natural keys.
The schema is the whole interface#
With function or tool calling, you give the model a list of tool definitions. Each has a name, a description and a parameters schema, usually JSON Schema. The model reads that text and, when it wants an action, returns a structured request. Your harness validates and runs it.
The model does not know what your function does internally, what the argument defaults are, or which of two similar tools is the preferred one. Whatever it needs to know must be on the page. That reframes the job: you are not documenting an API for a human who will skim and experiment. You are writing instructions for a reader that gets one attempt per call and cannot ask a question.
A good test is to hand the tool definitions, and nothing else, to a colleague and ask them to perform a task. Wherever they hesitate, the model will guess.
Name tools for the decision, not the implementation#
Names are the first thing the model scans. Make them specific verb-noun pairs that describe the outcome: create_issue, list_open_pull_requests, send_email_draft.
- Avoid generic names like
query,runorexecute. They hide what the tool does and invite misuse. - Avoid near-duplicates. If
get_userandfetch_user_infoboth exist, the model will flip a coin. Merge them or make the difference explicit. - Keep a consistent scheme. If reads start with
list_orget_and writes start withcreate_,update_ordelete_, the model, and your permission layer, can reason about risk from the name. - Separate read from write. A tool that sometimes reads and sometimes writes depending on a flag is hard to approve safely.
Write descriptions as prompts#
The description is where you spend your effort. A strong one covers four things in a few sentences:
- What the tool does, in plain words.
- When to use it.
- When not to use it, and what to use instead.
- Anything non-obvious about the arguments or the result, such as formats and limits.
The same applies to each parameter. A field called query says nothing. A description such as "Search text. Matches title and body. Use quotes for exact phrases" tells the model how to fill it. Include an example value where the format is picky, such as dates, IDs or repository names.
Keep it tight. Descriptions are sent on every call, so each extra sentence is a recurring cost, and a rambling one buries the instruction that matters.
Constrain inputs with types, enums and required fields#
Every degree of freedom you leave open is a chance for a wrong value. Close it in the schema.
- Use enums for any field with a fixed set of values:
"state": {"enum": ["open", "closed", "all"]}. The model will pick from the list instead of inventing "opened". - Mark required fields honestly. If the action cannot work without a value, make it required. If you make everything optional, the model will omit things and you will get partial calls.
- Prefer simple, flat arguments. Deeply nested objects and unions are harder to fill correctly.
- Use real types. Integers for counts, booleans for flags, strings with a stated format for dates.
- Set sensible defaults in your code, say so in the description, and keep the number of optional knobs small.
Then validate on the harness side anyway. The schema guides the model, but it is not a guarantee, so check arguments before you execute.
A before and after example#
Here is a tool that looks fine to its author and fails in practice.
{
"name": "issues",
"description": "Manage issues",
"parameters": {
"type": "object",
"properties": {
"action": { "type": "string" },
"data": { "type": "object" },
"repo": { "type": "string" }
}
}
}
The model has to guess the valid actions, the shape of data and the repo format. Nothing is required. Reads and writes share one tool. Now the revised version, split by intent:
{
"name": "create_issue",
"description": "Create a new issue in a GitHub repository. Use for new bugs or tasks. Do not use to comment on an existing issue; use add_issue_comment. Returns the issue number and URL.",
"parameters": {
"type": "object",
"properties": {
"repo": { "type": "string", "description": "Repository as owner/name, for example acme/web." },
"title": { "type": "string", "description": "Short summary, under 100 characters." },
"body": { "type": "string", "description": "Markdown details: steps to reproduce, expected result." },
"labels": { "type": "array", "items": { "type": "string" }, "description": "Optional existing label names." }
},
"required": ["repo", "title"]
}
}
It is longer, but each added line removes a guess.
Return outputs the model can use#
Tool results go back into the context, so they are part of the interface too. Return what the model needs for the next decision, not everything the API gave you.
- Be compact. Pick the useful fields, drop internal IDs and metadata noise, and cap list sizes.
- Be readable. Names beat opaque IDs. If you must return an ID, return a human label with it.
- Say what was cut. "Showing 20 of 134 results. Pass page 2 for more" lets the model decide to continue.
- Confirm writes. Return the created object's number and link so the model can cite it and the user can check.
Keep the format consistent across tools. If one tool returns dates as ISO strings and another as epoch numbers, the model will eventually compare them wrong.
Idempotency and errors that say what to do next#
Agents retry. The network times out, the harness restarts, the model decides to try again. If "create invoice" runs twice, you have two invoices. For write tools, accept an idempotency key, or design around a natural key such as a unique title or external ID, so a repeated call returns the first result instead of repeating the action.
Errors need the same care. A bare "400 Bad Request" gives the model nothing to work with. Compare:
{
"error": "repo_not_found",
"message": "No repository 'acme/webb' is connected. Connected repositories: acme/web, acme/api. Retry with one of these."
}
A good error states what went wrong, why, and the next step: valid values, which tool lists them, or whether to stop and ask the user. Distinguish errors the model can fix by itself from those that need a human, such as a permission refusal or a pending approval.
How Setu approaches this#
Setu exposes app actions through MCP, so the same tool-design rules apply to the connectors we ship for apps such as GitHub, Slack, Notion, Linear, Jira and Google Sheets. Actions are classified by risk as read, write or delete, which is what lets you set a per-app rule of go ahead, ask or never. By default, new accounts ask before writes and deletes.
Separating reads from writes in the schema is what makes that policy simple to apply: a rule can say reads go ahead while writes ask first, with no guessing about what a tool does. More detail is in the concepts guide.
Frequently asked questions#
How do I write a good tool description for an LLM?
State what the tool does, when to use it, when not to and what to use instead, and any format or limit that is not obvious. Keep it short, since descriptions are sent with every model call.
How many parameters should a tool have?
As few as the task allows. Keep arguments flat, mark truly required fields as required, use enums for fixed choices and limit optional parameters, because each extra option is another chance for the model to choose wrongly.
Why does my agent keep calling the wrong tool?
The usual cause is overlapping names or vague descriptions. Merge near-duplicate tools, add a line saying when not to use each one, and check that read and write actions are clearly separated.
What should a tool return when it fails?
Return a structured error with a code and a message that says what went wrong and what to try next, such as valid values or a tool that lists them. Say clearly when the user must step in.
Use tools that already follow these rules
Setu connects GitHub, Slack, Notion, Linear and more through one private MCP endpoint, with approvals before risky actions. Start with the quickstart.
Keep reading.
-
Agent engineering
12 ways AI agents misuse tools, and how to fix each
Twelve tool-calling failure modes, from hallucinated arguments to retry storms and prompt injection, each with its symptom and a harness-level fix.
-
Agent engineering
Context engineering for agents: what to keep in the window
Context engineering for AI agents: select what goes in the window, compact history, trim tool results, use sub-agents and order prompts so caching works.
-
Agent engineering
MCP explained: how models call tools, and where it breaks
MCP (Model Context Protocol) explained for builders: how clients and servers exchange tools, transports, auth, session URL risks, context bloat and versioning.