Designing tools well
Most tool problems aren't bugs. The workflow runs, the API returns, the schema is valid - and the agent still calls the wrong tool, or calls the right one with the wrong argument, or doesn't call it at all.
That's because of one thing, and everything on this page follows from it.
What the agent actually sees
The agent never sees your tool. It sees a label.
Three fields, and nothing else:
| It sees | It never sees |
|---|---|
| The name | Your workflow, its nodes, or its logic |
| The description | The API behind it |
| The input schema, with each field's description | What you know the tool is for |
So when an agent uses a tool wrongly, the question is never "why did it get this wrong?" It's "what did the label tell it?"
Every rule below is a consequence of that. A vague description is a blank label. Ten inputs is ten things to extract from a conversation. A missing output schema is a reply it can't read.
Naming
The name is the shortest description you have, and it's what you'll type in prompts.
- Verb-first, and scoped.
getOrderStatus,updateShippingAddress,escalateToBilling. Nottool1,helper,doStuff. - One name, one purpose. Don't run
lookupfor three different domains.lookupOrder,lookupCustomer,lookupTicket. - Name the goal, not the mechanism.
@AppointmentDateValidatortells the agent what the call is for at the point of use.@wf1makes it guess.
Names show up in traces and in every prompt that references the tool, so they're worth a minute up front.
The description is your only trigger
The agent reads the description and decides whether to call the tool. Nothing else influences that decision. Treat it as a docstring written for a new colleague who can't see the code.
A good one answers four questions:
- What does it do? "Look up an order's current shipment status…"
- What does it need? "…by order ID."
- When should it be called? "Use when the customer asks about an order they've already placed."
- When should it not be? "Do not use for new orders, or for pricing questions."
The fourth is the one people skip, and it's what stops a tool being called in situations it can't help with.
Two failure modes worth naming:
A description that restates the name. bookAppointment described as "Books an appointment" has taught the agent nothing it didn't already have.
Two descriptions that overlap. If two tools could plausibly answer the same request, the agent will pick between them inconsistently, and the behaviour looks random. Sharpen each until their triggers are genuinely distinct - or delete one.
Inputs
Every input is a question you're asking the agent. It has to find the answer in a conversation and get the format right. So the goal isn't a complete schema - it's the smallest one that does the job.
When you do need a field:
- Describe every property.
orderIdis half a message. "The customer's order ID - 10 alphanumeric characters, printed at the top of their confirmation email" is the whole one. - Mark required fields required. Otherwise the tool can be called with them missing.
- Use an enum for a fixed set. It's the difference between choosing a value and inventing one.
- Type it honestly. A number typed as a string is a bug waiting for a comparison.
- Name it for someone who only has the name.
idanddateare ambiguous the moment a conversation involves two of either. The field-naming rule below applies to inputs identically.
When not to have an input at all
The most reliable input is the one you didn't ask for. Three cases where a field should come out:
The platform already has the value. Asking the agent to extract an email address that's already in session state turns a known fact into a guess.
The tool can derive it. Passing customerTier to a tool that already looks the customer up creates two sources of truth that can disagree - and the agent's version is the less reliable one.
It's a mode switch. manageOrder(orderId, action: "status" | "cancel" | "updateAddress") isn't one tool with an input. It's three tools sharing a name, and the agent will mis-pick the action. Split it:
getOrderStatus(orderId)
cancelOrder(orderId, reason)
updateShippingAddress(orderId, newAddress)
One tool, one job. A tool that does five things is harder to use correctly than five tools that each do one.
Outputs, and where they go
Define an output schema that matches what the tool actually returns. Declaring {orderState, trackingNumber} when the workflow returns {state, tracking} means the agent reads nothing useful.
Then two decisions: what to call each field, and whether the agent should see them at all.
Name every field for someone who only has the name
This is the most common mistake in tool design, and it's easy to miss because the schema looks fine. A field called status is valid, typed, documented - and still tells the agent almost nothing. Status of what? The claim, the request, or the call that just ran?
The agent reads a field name exactly the way it reads your description: as the only available explanation of what it's holding.
| Instead of | Write | Because |
|---|---|---|
status | claimStatus | "status" could be the claim, the request, or the tool call |
data, result, payload | policyDetails | names the container, not the contents |
name | policyHolderName | whose name? |
id | orderId | |
flag, check | isEligible | a boolean should read as the assertion it makes |
date | collectionDate | which of the four dates in this conversation? |
Three traps this avoids:
Two tools returning the same generic key. If @Verify Policy and @Check Claim both return status, and both results are in context on the same turn, nothing tells the agent which is which. It will pick one, and it will sound certain.
One name meaning two things. The tool rule - one name, one purpose - applies to fields too. If status means shipment state in one tool and claim stage in another, you've given one word two meanings and made every reference ambiguous.
Wrapper keys that carry no information. { data: { result: { claimStatus: … } } } costs two hops and explains nothing. Return the fields you mean at the top level.
The same applies to the variable you store a result in, below. Store output in → data is unreadable two nodes later; Store output in → policyDetails reads itself.
Store output in
By default, a tool's result goes back to the agent, which reads it and decides what to say. That's right when the result is small and needs reasoning about - an eligibility flag, a balance, a yes or no.
It's wrong when the result is a large payload. The agent then holds a wall of JSON: it costs more every turn, it distracts from the job, and it invites the agent to read raw field names out to the customer.
Store output in writes the result to a variable instead. The agent gets a short confirmation that the call succeeded, and the value stays available to the rest of the flow.
| The result is | Do this |
|---|---|
| A flag, a status, a single value the agent must reason about | Let it come back to the agent |
| A customer record, an order row, a list of results | Store output in a variable |
The related habit on the tool side: return less. If the agent only needs a state, a tracking number and an ETA, don't return the whole order row - every field you return is a field something has to carry.
When a tool fails
A tool erroring does not route to on_error. The agent is told the call failed and carries on inside the step. So if you want the flow to react to a failure, you write that path yourself.
Which means separating two things that look identical in a prompt that doesn't distinguish them:
- A negative result - no match, not eligible, none available. That's an outcome, and it deserves a proper reply.
- A failure to answer - the service is down, the call timed out. That's not the customer's fault, and treating it as a negative result tells someone with valid details that their details are wrong.
Two settings help here:
- Acknowledgement - what the customer hears while a slow tool runs. Either generated to fit the moment, or a static list you write. Essential on voice, where silence reads as a dropped call.
- Fallback settings - an AI-generated apology if the tool fails, and optionally a ticket raised.
What a description cannot do
Two jobs people expect the description to handle, and it doesn't.
It can't make the tool available. Creating a tool puts it in the library; it doesn't give it to any agent. A tool reaches an agent by being attached - to that node, or through Global tools to every applicable agent (up to 30, with optional attach conditions). A tool nobody attached is a tool nobody calls.
It can't convey order. A description says what a tool is for, never when it runs relative to another one. If two tools must run in sequence, say so in the Instructions:
Call @Validate Address first. Only once it returns a valid address,
call @Book Collection.
Making a critical tool fire
For escalations, payments and account changes, "the agent will usually pick it" isn't good enough. The lever is the agent's Instructions, naming the tool directly:
Call @getInvoice before answering any question about a charge.
Never answer a billing question from memory.
Attaching a tool is a grant; the Instructions are the trigger. They're two separate decisions, and a tool that's attached but never mentioned may fire at the wrong moment or never fire at all.
A routing rule chooses which agent takes a conversation. It cannot name a tool, and a rule written as "if they mention billing, call getInvoice" has no effect - it reads perfectly and does nothing. Put tool instructions in the agent.
How to phrase it
The agent takes your wording literally.
| ❌ "Execute @FetchOrderDetails to get the order." | "Execute" and "run" read as prose, and are easy to skip |
✅ "Call @FetchOrderDetails with the order ID." | "Call" is the action it performs |
The same applies to rich media, which is a tool call rather than a description of one:
| ❌ "Display the pending items using @cards-pending-orders." | It narrates the intent instead of firing the tool |
✅ "Call @cards-pending-orders to show the pending orders." |
Test it before you save
The Test tab exists so you find these problems now rather than in a conversation.
- Run it with a realistic input. Confirm the output is the shape you declared.
- Run it with a missing or malformed input. Confirm it fails in a way the agent can report.
- For workflow tools, use Mocks during development so you're not burning production data or paid API calls.
Then iterate against real conversations rather than guesses: run a batch in AI Trust Center → Testing Lab, re-run the failures in the Playground, and watch how the agent used the output. Three rounds of that beats six months of "it should work".
Keep the catalogue tidy
Every attached tool is an option the agent weighs on every turn. That costs a little accuracy and a little money each time.
- Delete tools you no longer use. Stale tools dilute every decision.
- Don't leave test tools in a live project. Build them somewhere else.
- Attach narrowly. If only two agents need it, don't make it global. The test: if you'd otherwise attach it to more than half your agents, make it global - otherwise attach it where it belongs.
Before you release
- Every tool's description says what it does, when to call it, and when not to
- No two tools could plausibly answer the same request
- Every input has a description, and
requiredis set correctly - Every input is one the agent can't derive or already have
- Output schemas match what the tool actually returns
- Every field name says what it holds - no bare
status,data,id,nameordate - No field name means two different things across two tools
- Large payloads use Store output in rather than returning to the agent
- A tool failure and a negative result take different paths
- Critical tools are named in the agent's Instructions, not left to inference
- Anything slow has an acknowledgement - especially on voice
- Every tool tested in isolation, including a failing input
- Stale and test tools removed
Read next: Workflow tool · Knowledge base tool · Attaching tools to a node