← All articles Engineering

An error is your agent’s next prompt

11 August 2026 · 6 min read

Your API was designed for a caller who could go and look things up. A growing share of your callers cannot.

Here is a trace from this morning. An agent called a platform tool with the tenant slug wwww, one keystroke off from www. Every API I have worked with answers that with some version of 404 Not Found, and those two words are the entire budget a model has to work with.

It cannot open the docs or check the dashboard. Everything available to it on the next step is already in the transcript, and the transcript now says a thing it believed was true is false, with no indication of which part. So it does one of two things. It guesses again, burning a round trip and possibly wandering somewhere worse. Or it stops and reports failure to a person who has to work out what happened from a stack of tool calls.

Both outcomes are avoidable, and the fix lives entirely on your side of the wire.

The consumer changed and the errors did not

Error design has always assumed a lookup step outside the response. A human reads unknown domain, opens a tab, finds the right slug, gets on with it. That lookup is real work, but it happens somewhere you never see, so it never showed up as a cost of your API.

A model has no somewhere else. Whatever the error does not say, it has to invent. That is the whole change, and it moves error design from a diagnostic concern to a correctness one.

Four things are worth putting in the payload. All four examples below are real responses from the OBTO control plane, captured while writing this.

1. Echo what you actually looked for

{ "error": "not_found", "message": "Artifact 'this-page-does-not-exist' not found in collection 'pltf_page' for app='ob_www', domain='www'.", "searched": { "collection": "pltf_page", "name": "this-page-does-not-exist", "app": "ob_www", "domain": "www" } }

The agent sent one composite identifier. The response breaks it into the four fields the server resolved it to. That difference is the useful part: it separates “the name is wrong” from “the name is right and I am pointed at the wrong app.” Without the echo those two failures are the same string, and the agent has no way to tell which correction to attempt.

This costs nothing. You already have the resolved values in scope at the point you raise the error.

2. Return the near miss

{ "error": "unknown_domain", "message": "Domain 'wwww' is not an active tenant in the registry (pltf_domain). Nothing was read or written.", "didYouMean": [ "www" ], "hint": "Confirm the tenant slug with the human, or read obto_whoami's tenantDomains summary for the registry list." }

This is the field that pays for itself. An open-ended re-guess becomes a closed set of one, and the correction arrives on the next call rather than after a run of blind attempts.

It is also the field a layer above your API cannot produce. Computing didYouMean means running a similarity pass over the tenant registry at the moment of failure. A framework wrapping someone else’s service receives the 404 after that registry is out of reach, so the most it can do is rewrite the wording. Wording was never the missing part.

3. Say what did not happen

Look again at the clause in the middle of that message: Nothing was read or written.

That clause is a control-flow instruction. An agent holding an ambiguous failure on a write has to decide whether to retry, and deciding wrong is how a single logical operation lands twice. We wrote about that failure at length after it happened to us. Five words in the error settle the question, and they settle it for the caller least equipped to work it out from context.

State it explicitly on every refusal, including the ones where it seems obvious. Obvious to you is not in the transcript.

4. Name the legal moves

{ "error": "forbidden_operator", "message": "Forbidden operator '$where' at $.query.$where. This operator is blocked because it allows arbitrary JS execution or bypasses the per-app allowlist.", "hint": "Use only data operators ($eq, $ne, $in, $lt, $gt, $exists, etc.). Cross-collection joins and JS-eval operators are blocked by design." }

Three things arrive together: the exact JSON path that offended, the reason, and the shape of what would have been accepted. A bare refusal teaches the agent nothing, so it tries a neighbouring operator and gets refused again. Drawing the boundary converts one failure into a rule the agent applies for the rest of the session.

The same principle covers argument validation generally. If you build an MCP tool that rejects a field, the rejection should carry the accepted set, not the observation that the value was invalid.

The part that is easy to get wrong

Suggestions are a disclosure decision. didYouMean over a tenant registry tells the caller which tenants exist, and a helpful error handler is a fine way to build a directory-enumeration endpoint by accident.

The rule that keeps it safe is narrow: suggest only from the set the caller was already entitled to read. On OBTO that falls out of the same tenancy scoping that rewrites every query before it runs, so the suggestion list can never be wider than the caller’s own view. If you bolt near-miss hints onto an API whose reads are permission-checked somewhere else, the hint path is a second read that has to clear the same gate, and it is worth confirming rather than assuming.

Why this matters more than it used to

Under a stateless contract, every call carries its own context rather than inheriting it from a session. That removes a large class of drift bugs, which is why we moved to one. It also means every call is a fresh opportunity to get that context wrong, and the error response is the only channel through which a wrong guess becomes a right one.

None of these four fields require new infrastructure. The resolved identifiers, the registry, the write status and the allowlist are all in scope at the moment you construct the error. Adding them is an afternoon of work in the error handler, and they get read on every single failure for as long as the API exists.

Frequently asked questions

What makes an API error “recoverable” for an AI agent?

A recoverable error contains enough information for the caller to construct a corrected request without consulting anything outside the response. In practice that means four things: the parameters the server actually resolved, a suggestion drawn from valid values, an explicit statement of whether any state changed, and the set of inputs that would have been accepted.

Why can’t an agent framework just improve the error messages it receives?

It can rewrite wording, and wording is rarely the problem. The useful fields require data the underlying system holds: the tenant registry, the resolved identifiers, the allowlist, whether the write committed. A wrapper receives an error after that context is gone, so it can make the message clearer without making it more actionable.

Should error responses tell the agent whether the operation was applied?

Yes, on every refusal. Ambiguity about whether a write landed is what drives an agent to retry an operation that already succeeded. A short explicit clause such as “nothing was read or written” costs one line and removes the decision entirely.

Is returning suggested values a security risk?

It can be. A near-miss suggestion drawn from a global list leaks the existence of records the caller cannot otherwise see. Generate suggestions from the same scoped set the caller is permitted to read, and verify that the suggestion path passes through the same authorisation check as a normal read rather than querying the underlying store directly.

How is this different from writing good tool descriptions?

Tool descriptions are read once, before the call, and describe the general contract. Errors are read after a specific failure and can reference the specific values involved. A description can say that the domain must be an active tenant; only the error can say that the domain sent was one character away from a real one.

More from the OBTO blog