Designing Systems for Use by Agents and Humans
APIs and internal tools now have two categories of caller with opposite needs. Serving both well is a design discipline, not an afterthought — here's what that looks like in practice.
CONTEXT
Written from the practical experience of building internal tools and integrations that get driven by both a human clicking around and an agent calling an API — and noticing how often those two callers want contradictory things from the same system.
For most of software history, "who uses this system" had one answer: a person, sitting at a screen, tolerant of ambiguity and capable of noticing when something looks wrong. Every design convention we have — modals, toasts, "are you sure?" dialogs, breadcrumbs, hover states — assumes that caller. Now a growing share of calls to admin panels, internal tools, and public APIs come from an agent: a model that reads a page once, extracts what it needs, and moves on without noticing whether the layout looks right. It doesn't get frustrated by a confusing form. It also doesn't recover from one, because it has no intuition to fall back on.
That's not a niche edge case anymore. Support bots trigger refunds through the same admin panel a support rep uses. Coding agents call internal APIs to provision resources. Research agents crawl documentation sites looking for the one page that answers a question. If you're building anything with an interface — API, dashboard, internal tool — you now have two audiences with materially different requirements, and pretending you only have one is where most of the friction comes from.
Why the two audiences actually diverge
A human filling out a form can tolerate a field that's technically optional but practically required, infer meaning from placement and visual grouping, and shrug off an error message that just says "something went wrong" because they'll retry, or call someone. None of that works for an agent. An agent needs:
- Deterministic structure. The same request shape produces the same response shape, every time, regardless of which code path handled it.
- Machine-parseable errors. Not a string for a human to read, but a structured object with an error code, a field reference, and enough detail to decide whether to retry, fix the request, or give up.
- Explicit state, not implied state. A human infers that a button being greyed out means "not ready yet." An agent needs a status field that says so.
- No hidden side channels. A human notices a toast notification that says "saved, but your image didn't upload." An agent that only checks the HTTP status code misses that entirely, unless the response body has a place for it.
A human, in exchange, needs a lot of things an agent has no use for: visual hierarchy so they know what matters first, affordances that suggest what's clickable, forgiving validation that doesn't punish a stray space, and enough context to recover from a mistake without reading documentation. Optimize a UI purely for agent consumption — flat unstyled tables, no labels, no grouping — and it becomes unusable for the human who still has to operate it during an incident at 2am when the agent is the thing that's broken.
The tempting shortcut is to treat the agent as "just another user" and let it drive the UI like a human would, via browser automation or screen scraping. This works until it doesn't: the UI changes a CSS class, the agent's selector breaks, and now a routine task fails in a way nobody can debug from the error alone. Scraping a UI built for humans is inherently brittle because that UI was never a contract — it's a rendering, and rendering details change constantly without anyone treating it as a breaking change.
Patterns that serve both
Expose a real API next to the UI, not underneath it as an afterthought. If a human can do something in the dashboard, an agent should be able to do the same thing through a documented endpoint that isn't just an internal implementation detail you're reluctantly stabilizing. The UI becomes a client of the API, not the API's only interface. This also tends to produce a better UI, because it forces you to define the actual state transitions instead of letting them live implicitly in component logic.
Return structured errors, always. A useful shape looks like:
{
"error": {
"code": "invoice_already_paid",
"message": "Invoice inv_8f2a is already marked paid.",
"field": null,
"retryable": false
}
}The code is for the agent (or any programmatic caller) to branch on. The message is for the human reading logs. retryable answers the single most important question an agent asks after a failure: should I try this again, or is retrying pointless or actively harmful? Leaving that ambiguous is how agents end up in retry loops that make outages worse.
Use idempotency keys on anything with a side effect. An agent that times out mid-request has no reliable way to know whether the action happened. A human just checks the screen. Accepting a client-generated idempotency key on POST requests — and returning the original result on a repeat with the same key — turns "did that actually go through?" from a guess into a solved problem. This is standard practice in payments APIs for exactly this reason, and it applies just as much to any internal tool an agent might call twice.
Model state as an explicit, named machine. "Pending → approved → fulfilled → closed," with each transition validated server-side and the current state always present in the response — not inferred from a combination of three boolean flags and a null check. Humans can survive ambiguous state because they ask someone. Agents act on what the response tells them, so if the state isn't explicit, they'll act on a guess.
Write documentation an agent can actually consume. This is where llms.txt and OpenAPI specs earn their keep. An llms.txt file at your domain root gives an agent a clean, markdown-native map of what documentation exists, without needing to render and strip a JavaScript-heavy docs site to find it — treat it as the agent-facing equivalent of a sitemap. An OpenAPI spec goes further: it's not just documentation, it's a contract an agent (or the tooling around it, including MCP servers that wrap your API as callable tools) can generate a client against directly, rather than inferring request shapes from prose examples that drift out of date. Neither replaces human-facing docs — they're additive, not a redesign of your existing docs site.
Keep error messages honest in both directions. A vague human-facing "please try again" and a machine-facing generic 500 are the same failure of nerve: neither tells the caller what actually happened. Specificity costs you nothing and helps every consumer.
Where single-audience design breaks
Design only for humans and you get systems agents can't reliably operate without screen-scraping — which means every UI change becomes a silent breaking change for automation nobody remembers exists, until it fails at the worst time.
Design only for agents and you get systems humans can't operate under pressure — flat JSON dumps with no visual priority, error codes with no plain-language explanation, and workflows that assume the operator already knows the full state machine by heart. This shows up hardest during incidents, which is precisely when a human needs the interface to be forgiving and the agent needs it to be precise, at the same time.
The systems that hold up are the ones where the API is the real interface and the UI is a well-designed client of it — not the other way around. That ordering is what lets both audiences get what they actually need instead of one being tolerated as a workaround for the other.