On this page
What makes a tool usable by an agent
A tool is usable by an agent when its description alone lets a competent stranger call it correctly on the first attempt, with no access to your codebase, no way to ask a question, and no memory of yesterday. That is the entire design brief. Nearly every complaint that a model called the wrong tool, invented an argument or looped on an error turns out to be a description problem rather than a model problem, and it is fixable in an afternoon.
This gets neglected because tool descriptions look like documentation, so they get written last and reviewed by nobody. They are not documentation. They are the part of the prompt that decides what the agent does, and the only instruction it has at the moment it chooses an action.
- Every turnHow often your full tool schema set is resent and billed, in every run.
- 0Identifiers an agent should ever construct itself. Every ID comes from an earlier tool result.
- 2Phases an irreversible write needs: a prepare that previews, and a commit that acts.
- 1:1The wrapping ratio to avoid. One tool per API endpoint is the commonest tool design mistake.
The Five P Tool Contract
Every tool description should carry the same five parts in the same order. Consistency matters as much as content: twelve tools written to one template compare cleanly, and twelve written in twelve styles force the model to guess what the differences mean.
The Five P Tool Contract
Write every tool to these five headings. If a heading is empty, that is a finding about the tool, not a reason to drop the heading.
One sentence naming the user-visible outcome, then one sentence naming what the tool does not do. The negative sentence is the part people skip and it is the part that prevents the overlap bug, where two similarly named tools both sound correct and the model picks by coin toss.
For every argument, state where the value must come from. Say plainly that order_id comes from search_orders and must never be constructed. This single habit removes the largest class of agent bug, because a model under pressure to produce an identifier will produce one that matches your format perfectly and refers to nothing.
What must be true before the call is valid, expressed as a check the agent can perform with another tool. Not a warning, an instruction: call get_order and read status first. Preconditions written as prose advice get ignored; written as a prior tool call they get followed.
What changes in the world and what the return value actually proves. The distinction between queued and completed belongs here, because an agent that reads a queued response as done will report success for money that has not moved.
For each named error class, the next action: retry once with a corrected argument, call a different tool, or escalate. Generic advice to handle errors gracefully produces the apology loop. Naming the error and the response produces recovery.
Read it back with one question in mind: could someone who has never seen your system call this correctly first time. If they would need to ask you anything, the model will guess instead, and the guess will be confident.
One tool per outcome, not one per endpoint
Wrapping your REST API one to one is the fastest way to build a tool set and the most reliable way to make an agent slow and error prone. Endpoints are shaped for programmers who read documentation and hold state across calls. Tools should be shaped for the outcome someone wants.
The pattern that carries most of the benefit is search then act. Provide one lookup tool that takes a human-shaped query and returns the records with the fields the next action needs, including their identifiers. Then provide action tools that require those identifiers. The agent never has to invent an ID because there is always a tool that hands it one, and your validator can reject any identifier that did not appear in an earlier result. That check is covered further in controlling what an agent is allowed to do.
Error messages are prompts
Whatever a tool returns lands in the model's context and shapes the next decision. That makes error text prompt engineering with a different job title, and it is the most valuable writing in the whole system, because the agent reads it at the exact moment it is stuck.
| What the tool returns | What the agent does next | What to return instead |
|---|---|---|
| 500 Internal Server Error | Retries the identical call until the turn cap fires | Billing service unavailable, transient. Wait 2s, retry once, then escalate. |
| 400 Bad Request | Changes an argument at random and tries again | date_from must be an ISO 8601 date. Received 1st of last month. Today is 2026-08-26. |
| An empty array | Concludes the customer does not exist and stops | No orders matched. Searched by email only. Retry with order_number or postcode. |
| null | Reads it as success and reports the task done | An explicit state field with a reason. Never a bare null from a write. |
| Permission denied | Tries a neighbouring tool that also fails | Your role cannot refund over 500. Use request_refund_approval instead. |
| 200 OK | Assumes the money has moved | Refund RF-123 created, state pending, settles in about 3 working days. Verify with get_refund. |
A stack trace tells the model nothing actionable and can carry internal hostnames, query fragments and occasionally customer data straight into a context window that gets logged, replayed and sometimes sent to a provider. Map upstream failures to a small set of named error codes, each with a prescribed recovery, and keep the raw detail in your own trace where a human can find it.
Schema rules that remove whole bug classes
The schema is a contract the model cannot argue with, so put as much of the constraint there as possible and leave the description for what a schema cannot express. Each rule below corresponds to a bug that stops occurring once the rule is applied.
- Enumerate anything enumerable. An enum of five reason codes is checkable. A free-text reason field produces sixty variants in a month and no reporting.
- Stay one level deep. Models fill flat objects far more reliably than nested ones. If you need nesting, you probably need a second tool.
- Make almost nothing optional. Every optional field is an invitation to invent a value that seems helpful. Required and validated beats optional and hopeful.
- Ban negative booleans. A field called exclude_inactive gets set wrong roughly half the time. Use status with values active, inactive and all.
- Absolute dates only, and say what day it is. Put the current date in the system prompt and reject relative strings at the boundary with an error naming the expected format.
- Put units in the field name. Fields called amount_minor_units and timeout_seconds prevent an argument nobody notices until a refund is a hundred times too large.
- Pattern-match identifier fields. A regex on order_id catches an invented ID at the schema layer, before it reaches your database.
- Cap and paginate returns. A four thousand token result is charged again on every remaining turn of the run. Return the fields the next action needs plus a cursor, not the whole record.
The last rule is the one with a running cost attached. Return size is not a one-off charge, it is a subscription paid for the rest of the run, which is why trimming tool output is usually a bigger saving than changing model. The mechanism is set out in what an AI agent actually is.
# tools/refund_order_line.yaml
# Authored in YAML, serialised to the provider's JSON tool schema at build time.
# Every section below maps to one part of the Five P contract.
name: refund_order_line
description: |
PURPOSE
Issues a refund for ONE line on ONE order and returns the refund record.
It does not cancel the order, does not refund shipping, and does not email
the customer. Use cancel_order and send_customer_email for those.
PROVENANCE
order_id must come from a search_orders or get_order result in
this run. Never construct or infer it.
line_id must come from the lines array of that same order.
amount_minor_units integer, order currency, minor units. Must be less than
or equal to line.refundable_minor_units from get_order.
reason_code one of the enum values. Free text belongs in note.
idempotency_key one stable string per intended refund. Reusing it returns
the original refund instead of creating a second one.
PRECONDITION
Order status must be delivered or shipped. Call get_order and read status
before calling this tool.
POSTCONDITION
Creates a refund in state pending. Money moves later. The return value proves
the refund was created, not that it settled. Confirm with get_refund.
ON ERROR
amount_exceeds_refundable read refundable_minor_units from get_order and
retry once with a valid amount.
order_not_refundable stop and escalate. Do not try another tool.
upstream_unavailable wait 2 seconds, retry once, then escalate.
input_schema:
type: object
additionalProperties: false
required: [order_id, line_id, amount_minor_units, reason_code, idempotency_key]
properties:
order_id: { type: string, pattern: "^ORD-[0-9]{8}$" }
line_id: { type: string, pattern: "^LN-[0-9]{4}$" }
amount_minor_units: { type: integer, minimum: 1 }
reason_code:
type: string
enum: [damaged, not_as_described, late_delivery, goodwill, duplicate_charge]
note: { type: string, maxLength: 280 }
idempotency_key: { type: string, minLength: 8 }
returns: |
{ "refund_id": "RF-00012345", "state": "pending",
"amount_minor_units": 1299, "currency": "GBP",
"expected_settlement": "3 working days",
"verify_with": "get_refund(refund_id)" }Writes need idempotency and two phases
Read tools can be retried freely. Write tools cannot, and an agent will retry, because retrying is what a model does when a response looks ambiguous. Design every write assuming it will be called twice with the same intent, once because of a timeout and once because the model was not sure the first one worked.
- Make the idempotency key an argument
The key belongs in the schema so the model generates one per intent rather than per call. If your code generates it, a retry produces a fresh key and a second refund.
- Store the key and replay the original result
A repeated key returns the first result, including the same identifier and the same state. The agent then sees a consistent world instead of two conflicting truths.
- Split irreversible actions into prepare and commit
Prepare returns a preview with a short-lived token: the amount, the record, what will change. Commit takes only that token. A human can approve between the two without anybody building an approval system.
- Return the resulting state, never OK
The response should say what the record now is, and name the tool that verifies it. An agent given a verification path uses it. An agent given OK reports success.
- Tag every write with the run identifier
When a run fails between two writes, you need to find the first one to compensate for it. Without a run tag, that search is manual and slow at exactly the wrong moment.
If a task writes to a CRM and then to billing, there is no transaction spanning both, whatever the framework implies. You get two options and you must pick one before launch: a compensating action that reverses the first write, or an approval gate placed before either write happens. The retry mechanics that sit underneath this are covered in rate limits, retries and backoff.
The tool schema tax
Every tool description is sent to the model on every turn, whether or not the agent uses that tool. A large tool set is therefore a fixed tax on every run, and it also lowers accuracy, because more similar-sounding options make the choice harder. Both costs point the same way.
Estimate schema tokens by counting characters in your rendered tool definitions and dividing by about four. Put your own input price in rather than a rate quoted anywhere, because those change.
Run it with your real tool count before you argue about model pricing. The number that usually surprises people is the share output: on an endpoint-shaped tool set, a large fraction of every prompt is spent describing tools the agent will never call on this task. Grouping by outcome cuts the tax and the error rate together.
Testing a tool description
Tool descriptions are prose that controls behaviour, so they need tests like any other behaviour. The test is cheap: give a model nothing but your tool list and a phrased request, and check which tool it picks and which arguments it fills. No agent loop, no database, no waiting.
- Tool schema
- The machine-readable declaration of a tool's name, arguments, types and constraints, sent to the model alongside its description on every turn so it can produce a valid call.
- Provenance rule
- An instruction stating where an argument's value must come from, usually that an identifier must appear in an earlier tool result in the same run and may never be constructed by the model.
- Idempotency key
- A stable string supplied with a write so that repeating the same call returns the original result instead of performing the action twice. Generated once per intent, not once per attempt.
- Two-phase write
- A write split into a prepare call that returns a preview and a short-lived token, and a commit call that takes only that token, so an irreversible action can be reviewed before it happens.
- Tool surface
- The complete set of tools exposed to an agent in one run. It defines both the token cost of every turn and the outer limit of the damage a bad run can do.
That last item is the one people resist. A tool nobody calls is not free: it is a permanent tax on every prompt and one more option to choose wrongly. The tool set is usually the part of a build that gets postponed, and it is the first thing we rewrite in AI agent development.
Questions readers ask next
How many tools should one agent have?
Should tool descriptions be long or short?
Why does the agent keep inventing identifiers?
Do I need MCP or a specific framework to define tools well?
How do I stop an agent calling a write tool it should not?
What is the single highest-value change to an existing tool set?
ChatGPTalker. "How to Design Tools an AI Agent Can Actually Use." chatgptalker.com, 2026-08-26. https://chatgptalker.com/guides/designing-tools-for-agents/