Agent infrastructure
Platform integration
Tool schema design for AI agents decides whether the model picks the right tool and fills it correctly. Input, output, validation, and where schemas break.

Tool schema design decides whether an agent works. Everything else — the model, the orchestrator, the prompt — sits downstream of it. If the schema is ambiguous, over-broad, or misses the parameters the model needs to disambiguate, no amount of prompting recovers the call.
This piece is the deep-dive on that layer. Not "what is a JSON schema" — you already know. What we're doing here: naming the design choices that decide whether the model picks the right tool, fills the right parameters, and reads the response back into something it can act on next turn. Pontil's view in one sentence: tool schemas are a compression problem — you're encoding a slice of a product surface into a form the model can reason about, and every field you add or leave out changes what the model can do.
We'll cover five things: what a tool schema actually is at the wire, how to shape input schemas so the model fills them correctly, how to shape output schemas so the next turn works, the trade-offs between strict and permissive validation, and where schemas fit in the wider tool interface design problem.
A tool schema is the contract you hand a foundation model at inference time. In the OpenAI Responses API and Chat Completions, it's a tools array of function definitions. In Anthropic's API, it's the tools parameter with input_schema. In the Model Context Protocol (MCP), it's a tools/list response containing name, description, and inputSchema. The shapes differ; the substance is the same.
Every current implementation uses JSON Schema as the type system, but the flavour varies. Anthropic requires Draft 2020-12 for input_schema; MCP now defaults to 2020-12 as well. OpenAI and Google Gemini use custom subsets with their own constraints — OpenAI's strict mode, for instance, requires additionalProperties: false and every declared field marked required, which isn't standard Draft 2020-12 behaviour. The cross-vendor story is more fragmented than it looks. The schema is serialised into the model's context window on every turn. That has three immediate consequences most teams underestimate.
First, schemas cost tokens. A verbose schema with fifteen enum values, deeply nested objects, and long descriptions eats context the model needs for reasoning and history. If you're calling with fifty tools loaded, schema bloat is often the single biggest use of your context budget — bigger than the conversation itself.
Second, the model reads the schema as documentation. Field names, descriptions, enum values, required arrays, and default values are all signals it uses to decide whether to call the tool and what to pass. A field called id with no description is worse than a field called customer_id with a one-line description saying "the customer's Stripe customer ID, format cus_...". The model isn't parsing JSON Schema semantics — it's reading English.
Third, structured output enforcement is not the same as good schema design. OpenAI's strict: true mode and Anthropic's tool-use guarantees will constrain the model to produce valid JSON matching your schema. They will not stop the model from filling the wrong values into a valid shape. Validity is table stakes. Correctness is a design problem.
Input schema design is where most agent failures live. The model produced a syntactically valid call; it just called the wrong tool, or filled a required parameter with a hallucinated value, or passed null where a real ID belonged. Four heuristics catch most of it.
get_customer beats customer. create_invoice_draft beats invoice. The model routes on the tool name before it reads anything else — a verb-object phrase tells it what action the tool performs, which is what it's trying to match against the user's request. This is the same pattern we walked through in how to write tool descriptions for LLM agents, and it holds up under evals.
A parameter marked required that the model has no way to fill from context is the single most common cause of agent loops. The model calls the tool, gets a validation error, tries again with a guess, gets another error, gives up. If a required parameter is an opaque ID (customer_id, invoice_id, record_id), the schema needs to tell the model how to obtain it — usually by naming the tool that returns it.
{
"name": "customer_id",
"type": "string",
"description": "The customer's ID (format: cus_...). Obtain by calling search_customers with the customer's name or email."
}
That one sentence prevents a whole class of failure modes.
If a field only accepts five values, make it an enum. "status": {"type": "string", "enum": ["draft", "open", "paid", "void", "uncollectible"]} is dramatically more reliable than "status": {"type": "string", "description": "the invoice status"}. Enums show up in the serialised schema; the model sees them; it picks from the list. Free-text with a description in prose is a guessing game.
The trade-off: enums are brittle to product changes. Add a sixth status and every schema referencing the old enum is now wrong. This is where automated maintenance stops being a nice-to-have.
Deeply nested input objects ({customer: {billing: {address: {line1: ...}}}}) make the model work harder to construct the call, and they make partial calls impossible — you can't fill three of four required leaves and let the tool fetch the rest. Flat parameter lists (customer_billing_address_line1) are uglier to read but more reliable to call. Nest when the shape genuinely reflects an atomic domain object (a coordinate, a monetary amount with currency); flatten otherwise.
This connects to the broader agent tool granularity question. A tool with twelve flat parameters is often a signal you've combined two tools that should be separate — the schema is telling you the granularity is wrong.
Output schema design is the half most teams skip. The model called the tool, the tool ran, now what does the tool return? Whatever comes back is going into the model's context on the next turn — which means it's now input to a reasoning step, not just data for a UI.
A tool that returns the full record for a search result gives you agents that burn 40,000 tokens on a list_customers call and then hit context limits three turns later. Every tool response needs a bounded shape. For lists, that means pagination with a small default page size and explicit next_cursor — the pagination pattern we've written up separately. For single-record lookups, it means returning only the fields the next turn actually needs.
The pattern that works: return the minimum fields the model needs to reason about the result, plus IDs it can pass to the next tool. If the model needs the full object, it can call get_customer(customer_id) in a follow-up turn. This keeps the search response small and forces the model to be explicit about which record it's acting on.
{
"results": [
{"id": "cus_abc123", "name": "Acme Corp", "created": "2024-11-03"},
{"id": "cus_def456", "name": "Acme Industries", "created": "2023-06-12"}
],
"next_cursor": null,
"total_matched": 2
}
The model can now say "the customer is cus_abc123" and pass that ID to the next tool. Compare that to returning two full 4KB customer objects with every field.
A tool that returns HTTP 500 with a stack trace teaches the model nothing. A tool that returns a structured error the model can read ({"error": "customer_not_found", "suggestion": "try search_customers with a broader query"}) lets the model recover. The response schema needs to cover the failure paths, not just the happy path. We went deeper on this in the error handling piece.
Both OpenAI and Anthropic now support strict schema enforcement — the model is constrained at decoding time to produce output matching your schema exactly. This is a real improvement over the earlier world where you'd catch a malformed call after the fact and retry. But it comes with a cost.
Strict is the right default for production tools where a malformed call causes real damage (writes, financial operations, anything mutating). Permissive with server-side validation is the right default for read-only tools where a rejected call is cheap and schema iteration is frequent. Most agent projects need both, chosen per tool — not a global setting.
The deeper point: neither mode fixes the design problem. Strict guarantees the model produces {"customer_id": "cus_abc123"} in the right shape. It doesn't guarantee cus_abc123 is the right customer.
Tool schema design is one layer of a larger problem: the interface between agents and the product surface they're acting on. The schema is where you encode what the tool does; the runtime is where you enforce who's allowed to call it and as which user; the maintenance loop is where you keep the schema honest as the underlying product changes.
Most teams treat these as separate. Schemas live in code, in a tools/ directory somewhere, hand-written and hand-maintained. Runtime lives behind an API gateway that doesn't know anything about tool semantics. Maintenance happens when something breaks in production.
That model works for ten tools. It falls apart at a hundred, which is where any serious enterprise agent project ends up — one product with fifty capabilities exposed as fifty tools, times three products, times ongoing product change. Schema drift becomes the dominant cost. The schemas say the tool takes customer_id as a string; the underlying API now requires a UUID; nobody notices until the agent starts failing at 3am.
We wrote about this failure mode in OpenAPI spec drift — the same problem applies to tool schemas, one layer up. The fix is the same: schemas need to be generated from the source of truth (the code that actually implements the capability), maintained automatically as that source changes, and validated in CI before drift reaches production.
Pontil is Tools-as-a-Service — we generate and maintain the tool layer that sits between agents and the SaaS products they need to act on. Tool schemas are the surface of that layer, and getting them right is what we do.
We generate tool schemas from the customer's existing codebase, not from a hand-written OpenAPI spec, so the schemas reflect what the product can actually do. Then we run the Tool Runtime that executes those tools as the authenticated user, with the schema enforced at the boundary and drift detected as part of the CI cycle. When the product changes, the schema changes; when the schema changes, the agent's context updates on the next turn.
The design principles in this piece — verb-object naming, required-parameter discoverability, bounded response shapes, structured errors — are what we build into every schema we generate. Because at portfolio scale, hand-writing them stops working.
The honest answer: dynamic schemas. Every schema pattern in this article assumes static tool definitions loaded at inference time. That model is already straining. Products with per-tenant customisation, feature flags, and role-based capabilities need per-user tool schemas — the same underlying tool exposes different fields depending on who's asking. Current APIs (Responses, Anthropic tools, MCP) all support runtime schema fetching, but the ecosystem around evals, testing, and versioning assumes static.
The teams that solve this first will be the ones building agents on their own products at portfolio scale. The teams that don't will keep shipping ten tools, hitting the same reliability ceiling, and blaming the model.
Schema design is boring, downstream, and unglamorous. It's also the layer that decides whether your agent project ships.
Stay up to date on the ever changing agentic landscape.