Designing the tools your email agent calls

An email agent is a model plus a small set of tools, and when it behaves badly the tools are usually the reason. Too many of them, names that could mean several things, results that dump raw text into the context, and errors phrased as failures rather than answers.

4 min read

A small set of tools around one agent
Few tools, sharply named. Most bad behaviour starts here.

The email tools are given. What you design is everything the agent calls in your own systems, and those are where the quality is decided.

Fewer than you think

Every tool is another thing the model can pick wrongly, and the failure is not an error, it is a plausible wrong action. Four to six is a good working range for an email agent: the thread, the contact, one or two lookups into your systems, one action, and an escalation.

The test for adding one: can you name a message this week that the agent cannot handle without it. If not, leave it out and add it when the need is real, per the least-privilege reasoning in deciding what each agent is allowed to see.

Where you genuinely have many capabilities, keep the tool surface small and put the variety in parameters rather than in a tool per case. One lookup_order with a well-typed argument beats five near-identical tools the model has to choose between.

Name for the decision, not the implementation

The model reads names and descriptions as its instruction set, so they should describe the situation in which the tool is correct.

WeakBetter
query_dbget_order_status
processissue_refund_under_limit
handle_requestescalate_to_human
searchfind_help_article

The second column tells the model when to reach for something. The first requires it to infer, and inference is where wrong calls come from.

Descriptions should say when as well as what: "Call before writing a reply" produces the read-thread-then-contact order far more reliably than any amount of system prompt.

Return structure, not prose

A tool that returns a paragraph hands the model something to interpret. A tool that returns fields hands it something to use.

TypeScript
// Invites the model to paraphrase, and to believe whatever the paragraph says.
return `Order 4012 shipped on Tuesday via DHL and should arrive Thursday.`;

// Gives the model facts it can compose from, and you can assert on in a test.
return { orderId: '4012', status: 'in_transit', carrier: 'DHL', shippedAt: '2026-08-04', etaAt: '2026-08-06' };

Structured results also make grounding checkable. If your rule is that a factual claim must trace to a tool result, that only means something when the result has fields to trace to, per evaluating an email agent.

The same argument applies to anything the tool fetches from outside: a web page or a document is untrusted text, and passing it through verbatim is the injection path described in every tool you connect.

Make refusals answers

The most consequential design choice in the whole surface. When a tool cannot do something, return why in a form the model can act on rather than throwing a generic error.

TypeScript
return { refused: true, reason: 'above_refund_limit', limit: 200, escalate_to: 'billing' };

An agent given that escalates. An agent given Error: forbidden tries a different phrasing, then tries again, then invents something. The platform's own refusals are built this way deliberately, with stable reason strings and reset times, and your tools should match that shape so the agent experiences one consistent world.

A refusal carrying a reason and a next step, rather than a generic error
A reason the model can act on beats an error it can only retry.

Put the limits in the tool

Anything expensive or irreversible should be refused by the tool rather than discouraged by the prompt. A refund tool that will not exceed a limit *is* the limit. A prompt that asks the model not to exceed it is a request, and it competes with everything else in the context.

This is the same principle the platform applies to sending, and it is the one that survives contact with an agent having a bad day, per keeping an autonomous agent from becoming a spam problem.

Make them idempotent and testable

Two properties that pay for themselves the first week.

Idempotent. A tool called twice with the same arguments should do the thing once. Retries happen at every level, and email agents in particular re-run on webhook redelivery, per what happens when forty messages arrive at once.

Testable without a model. Every tool should be callable directly in a test with fixed arguments and asserted on. That is what lets a golden thread suite assert on actions rather than wording, which is the only stable thing to assert on.

A short checklist

  1. Four to six tools, each traceable to a real message you saw this week.
  2. Named for the decision, described with when to call them.
  3. Structured results, never prose, never raw external text.
  4. Refusals with reasons and next steps, not exceptions.
  5. Limits enforced in the tool, not requested in the prompt.
  6. Idempotent, and callable in a test without a model.

Questions

How many tools should an email agent have?
Four to six for most agents. Every additional tool is another chance for a plausible wrong call, and the test for adding one is a message this week that fails without it.
What makes a good tool name?
One that describes the decision it serves, like get_order_status or escalate_to_human, rather than the implementation, like query_db. The model reads names as instructions.
Should tools return text or structure?
Structure. Prose invites paraphrasing and cannot be traced for grounding, while fields can be composed from and asserted on in tests.
How should a tool refuse?
With a reason and a next step as data. A generic error makes the model retry or invent; a reason makes it escalate or wait.
Where should limits live?
In the tool. A refund tool that refuses above a limit is the limit; a prompt that asks the model to respect one is a request competing with everything else in the context.
Why must tools be idempotent?
Because retries happen at every layer, and email agents re-run on webhook redelivery. A tool called twice should act once.

Give your agent an address it can answer from.

Create an inbox