Designing APIs for Agents
Good design for agents is not the same as good design for humans.
Designing APIs for agents is different from designing them for humans. Most consumers of APIs today do so through agent-written code. This was not true two years ago, and it changes how we should think about designing them.
I no longer believe in most systems built for humans. I'm skeptical of most packages, I'm anti-utility, and I'm pro extremely long names now — all opinions I've done complete 180s on in the past 24 months.
Good APIs for humans
When I design a good API for humans, my first step is sketching the minimal usage patterns, the onboarding, and the top ten use cases. From those, I imagine the code I'd ideally write to get there. Then I try using it, find the edges where I got stuck, and add good defaults and configuration where necessary.
I want people to have something functional in fifty lines. From there, the API should be approachable enough that reaching for more functionality is obvious — that's where they'd look. Ideally, someone who needs only one of twenty fields should only become aware of the other nineteen through the autocomplete around the one they want. A good SDK should be self-explanatory enough that you can use it with only minimal skimming of the docs, if that.
Some great examples:
client.messages.create(
body="Join Earth's mightiest heroes. Like Kevin Bacon.",
from_="+15017122661",
to="+15558675310",
)const paymentIntent = await stripe.paymentIntents.create({
amount: 1099,
currency: "usd",
automatic_payment_methods: {
enabled: true,
},
});These embody the values above. I implemented both as a fourteen-year-old with no issues, and without having to learn about Stripe's tax options, what ACH is, or any of the fifty other things Stripe does — and without learning about Twilio's scheduling, bots, or anything else. Later on I did. But these SDKs let me make a ton of progress without knowing much about what was actually going on.
This is the opposite of how you should design for agents
AI agents can read our entire docs in one sitting. The average Claude Code prompt uses 10k+ tokens; on their first prompt, agents can read your entire API and every relevant document. They can also produce thousands of lines of code to reach their goals in seconds. This changes everything.
Good APIs for agents
Agents need clarity above everything else — APIs where reading the code tells you exactly what it does. There is a lot more code in the AI-agent era, which makes debugging much harder, and the only way to solve that is through precise definitions of what the code is expected to do.
That means:
Defaults are bad. Agents can be expected to read the documentation, register what good starting values are, and fill them all in, in place. Explicitness is cheap now, and bringing specificity to expected behavior reduces bugs. The examples above each had thirty fields I didn't fill in because I didn't comprehend them. An agent should fill in every single one.
This isn't to say the less important fields shouldn't have a comment between them and the more important ones to separate them — just that the tax on filling out large amounts of seemingly unimportant fields has disappeared, while the tax of understanding what code does has gone way up.
Errors are not bad. Many great APIs I've used have smoothed over my stupidity: accepting uppercase for lowercase-only fields and coalescing it, or accepting multiple keys for the same thing the way Postgres booleans accept
true,yes,on,1. This is actively bad for agent codebases. It leads to different functions using different values for the same thing, which causes headaches for later reviewers.Modern harnesses can be expected to read the docs when debugging — bonus points if you tell them to in the error message. Half-correct coalescing should not happen; let the agent explicitly coalesce the values however it wants on its side. For humans, errors in onboarding are bad and you should minimize them. For agents, they are opportunities to clarify what something actually means.
This doesn't mean all errors are good. Blank internal errors with no direction make agents spiral, but that's an argument against bad errors, not errors. A good error is an amazing tool: it means the AI had a misconception about your API and solved it. And it will have misconceptions. Even with everything in context, backwards compatibility breaks and confusion happens. Errors that lead to clarity solve this.
Errors are one of the best surfaces an agent has for finding the happy path, but only if they're precise. Unlike humans, agents follow instructions literally, so a vague or wrong error makes them spiral instead of recover. In our data, 27% of the friction agents hit comes from errors, so great error design is as important as great docs.
Read more from MikaIn distribution, not in hallucination. When an API is unclear, agents tend to hallucinate and use it like similar APIs they've seen. For this reason I prefer specific field names to general ones. Take the field
name. Some APIs use it for a display name, others for a full ID, others for a scoped ID. If you ask an agent to use an API withnamein it across ten different use cases, it will use it in five different ways — and following the point above, only one can be right. Cannamehave spaces, slashes, dashes, underscores, emoji?This problem compounds as the codebase grows. Reviewers who read this code in the future will interpret
namein those same five different ways again. Prefer explicitness: in place ofname, I preferdisplayName,slug,externalId, or anything you choose that won't carry the same assumptions. These are words an agent can truly understand — not get stumped by, and not misinterpret. Doc comments help cement the correct interpretation too.Facts, not feelings. The value of an API in the era of agents is that it provides a fact the agent or its human team cannot replicate internally. That can be the fact of a bill paid, a message sent, or a virtual machine provisioned (shameless plug for Freestyle VMs). Utilities beyond that are largely irrelevant, and can be replaced with documentation and guides explaining how to get somewhere. In that vein, I'm fairly skeptical of most SDKs for APIs that do more than expose API norms in language-specific patterns — transforming API errors into strongly typed TypeScript errors, for example.
Putting this into practice
Freestyle builds the most powerful virtual machines in the sandbox space as measured by virtualization quality (we support everything others don't, like Docker-in-Docker, nested virtualization, and advanced networking), scale (we allow 8x larger memory footprints in our public tiers, and much more for enterprise), configuration (we support the full operating system, rootFS, and networking, including private VPCs and VPN connections), stability, and full memory snapshots that actually work ;) — all provisioned in 400ms.
This puts us in a weird spot. We work with users solving fundamentally complex problems that are open-ended, hard to debug, and the most difficult in the industry — people come to us for what other sandboxes cannot do. To that end, we try our very best to not do much: just provide the facts I gave above, as consistently as possible.
We used to try to hide as much of the complexity as we could. We built a declarative functional build system combined with SDK utilities that let us author packages that came with dependencies. The idea was: "If a user wants Bun, give them the Bun package and let them not worry about the internals, because we made Bun work well." But we didn't. It was never configurable enough, there were always more questions than answers, agents didn't get it and misused it in every possible way, and in the end the complexity of trying to use these abstract layers became the hardest part of onboarding to us. See below how beautiful the code snippet looks — and how little you get about what's going on in it.
import { freestyle, VmSpec } from "freestyle";
import { VmBun } from "@freestyle-sh/with-bun";
const { vm } = await freestyle.vms.create({
spec: new VmSpec({ with: { bun: new VmBun() } }),
});
await vm.bun.install({ deps: ["zod"], global: true });
await vm.bun.runCode(`import { z } from "zod"; console.log(z.string().parse("hi"))`);This was optimized around my former belief that humans needed to get something working fast and would dive into the details later. But we got rid of all of that months ago. We removed every complex SDK package and indirection in favor of a guide. Now when you want anything complex like that, you tell your agent to read the guide, it takes the code relevant to it, adapts it to your project's norms, customizes the behavior however it wants, and has a much cleaner onboarding experience combined with much clearer code at the end.
import { freestyle } from "freestyle";
const { vm } = await freestyle.vms.create();
// Just exec — install a package, write a file, run it. No bespoke APIs.
await vm.exec("cd /tmp && /opt/bun/bin/bun add zod");
await vm.exec(`echo 'import { z } from "zod"; console.log(z.string().parse("hi"))' > /tmp/main.ts`);
const { stdout } = await vm.exec("cd /tmp && /opt/bun/bin/bun run main.ts");In practice in other APIs and SDKs
🤖 Agent frameworks
🏆 Excellent — Flue Framework. It provides minimal semantics for the core things agents need. Everything is a pluggable function, primarily defined through its interactions with other parts of the agent. Flue is completely clear about what it does — and it actually doesn't do much: it provides shared semantics for building harnesses.
import { defineAgent } from "@flue/runtime"; // The whole agent is one function returning config — every part is a value. export default defineAgent(() => ({ model: "anthropic/claude-sonnet-4-6", instructions: "Triage the incoming GitHub issue.", }));👍 Good — Vercel AI SDK. Shared SDK semantics around connecting to agents, and very useful.
generateTextandstreamTextare great, clear functions that let agents treat complex interactions with many different inference APIs as facts.import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; // One call in, one fact out. const { text } = await generateText({ model: openai("gpt-5"), prompt: "Summarize this transcript.", });😬 Medium — Mastra. Mastra has a lot of good scheduling, and workflows as well; most of their agent is built from pluggable types you can implement yourself. That said, I think it's heavy-handed and unclear in many places. For example, its Workspace, Filesystem, and Sandbox semantics are extremely data-destructive toward their internal implementers: a Docker sandbox that doesn't support FUSE is not the same thing as a SQLite filesystem combined with a bare-metal GPU. This is solvable in Mastra, but an agent-centric design would remove a lot of the contract and replace it with guides on how to implement different patterns.
💀 Bad — Eve. It's like they took Next.js and made it an agent framework: great for 2016. As I understand it, their skills system is completely monolithic and non-programmable, so you have to leave it behind for their dynamic system if you want to change loading.
--- description: How this team defines revenue. Load before any revenue question. --- Revenue is net of refunds, over the subscription term. Weeks are Monday-anchored, UTC.The whole skill is this file. Loading is decided by that
descriptionstring at build time — there is no hook to program when or how it loads.
📦 Sandbox APIs
🏆 Excellent — Freestyle (bias much 🙄). Freestyle's APIs don't do much; they do what they say. We have APIs that make everything you could want to do with a VM possible, but we don't bloat them with the same utilities other providers do. Where other providers say you should use their
box.git.cloneutil, we say just have your AI write agitClonefunction if it wants one — choose depth, auth strategy, anything; it's all just a wrapper aroundexec.import { freestyle } from "freestyle"; const { vm } = await freestyle.vms.create(); // Write the wrapper you want. It's all just exec. const gitClone = (url: string, dir: string, depth = 1) => vm.exec(`git clone --depth ${depth} ${url} ${dir}`); await gitClone("https://github.com/acme/app", "/app");👍 Good — E2B. E2B shows some restraint — not much. A great example of the issue with their utilities is their
sbx.runCodefunction: the first argument takes a string of code, and the second, in the options, is an optional configuration field (a textbook example of human-centric design that's bad for agents). Pushing on it, you can set the language to whatever you want (js, py, R, bash), but not all E2B sandboxes have all those languages. Good restraint elsewhere, though — they keep their SDK fairly consistent and debuggable.import { Sandbox } from "@e2b/code-interpreter"; const sbx = await Sandbox.create(); // The language is buried in an options bag — not every sandbox even has it. await sbx.runCode("summary(cars)", { language: "r" }); // ...which is really just: await sbx.commands.run("Rscript -e 'summary(cars)'");😬 Mid — Daytona. Daytona has VNC, Git, Docker, Web Terminal, LSP, and more built into their SDKs — all non-configurable, and some silently unsupported on some Daytona sandboxes. Freestyle supports all of these too, but doesn't bake them behind non-pluggable SDK methods, so you can customize them (see the guides). They're generally usable in most cases — I just wouldn't, nor should your agent. Daytona believes these are important enough to bake into the SDK, so they ship SDKs in half a dozen languages. That seems targeted at humans; ideally an agent understands the APIs well enough that this isn't relevant.
import { Daytona } from "@daytonaio/sdk"; const sandbox = await new Daytona().create({ language: "typescript" }); // things daytona thinks you're too dumb to do yourself await sandbox.git.clone("https://github.com/acme/app", "/app"); await sandbox.fs.replaceInFiles(["/app/config.ts"], "dev", "prod"); await sandbox.git.add("/app", ["."]); await sandbox.git.commit("/app", "ship it", "bot", "bot@acme.dev"); await sandbox.git.push("/app"); // or... await sandbox.process.executeCommand( "cd /app && sed -i 's/dev/prod/' config.ts && git commit -am 'ship it' && git push", );
Closing
Building for agents is finally diverging from building for human engineers. Many core principles will carry over: good documentation matters, following usage patterns and making clear errors still matter. But a lot of what used to matter doesn't anymore. Lines of code, errors in onboarding, required reading to become productive, even tokens required for onboarding are all fairly irrelevant to how good an API is for agents. Everything in here wouldn't have been correct with GPT-4.5, so it might not be relevant for GPT-6, but I'm looking forward to seeing where this goes.

