Tool Schemas Agents Follow: Descriptions, Enums, and Killing the Omnibus Tool
Agents invent arguments when schemas are vague. Write JSON Schema like agent UX—enums, required fields, property descriptions—and kill the do_anything tool.
Agents invent arguments when your tool schema is vague. Treat JSON Schema as agent-facing UX: every property gets a description, finite sets become enums, required fields are honest, and the omnibus do_anything tool dies before it becomes privilege escalation. Schema quality is not documentation polish — it is how you stop silent production bugs.
This spoke belongs to the Agentic Systems Operating Manual. It pairs with tool-use sandboxes (where tools execute) and evaluators before agents (how you prove argument accuracy). Schemas tell the model what shape to emit; sandboxes and gates decide whether that shape may run.
The short answer
- Descriptions are the UX. Empty property descriptions force the model to guess units, formats, and when to omit fields.
- Enums beat free text for status codes, channels, and action verbs the handler actually supports.
requiredmust match reality. Optional-in-handler but required-in-schema (or the reverse) creates retry storms.- Strict mode grammar-constrains valid JSON — and rejects schemas outside the provider’s supported subset.
- Split omnibus tools. One mega-tool with a free-form
actionstring is a confused deputy with an API. - Measure argument accuracy on a golden set the same way you measure task pass rate.
What makes a tool description agent-facing UX
The top-level description answers three questions the model asks every turn:
- When should I call this (triggers)?
- When must I not call it (negative triggers)?
- What side effect happens if I do?
Weak:
Updates a CRM record.
Stronger:
Update an existing CRM contact by contact_id. Call when the user confirmed a field change
(email, phone, lifecycle stage). Do not call to create contacts — use create_contact.
Do not call when the change is still a draft awaiting human approval.
Prescriptive when beats poetic what. Recent models that reach for tools conservatively especially need trigger language (Anthropic’s own tool-definition guidance stresses this).
Why empty property descriptions cause production bugs
The model sees property names. Names lie.
| Schema smell | What the model invents | Production bug |
|---|---|---|
amount with no description | Dollars vs cents | Off-by-100 charges |
date as string, no format | tomorrow, 02/03/26, unix | Handler parse fail → retry loop |
status free string | closed, Complete, done | Downstream enum reject |
user vs user_id | Email stuffed into id field | 404 / wrong tenant |
Nested options: {} empty | Hallucinated keys | Strict mode 400 or silent ignore |
Worked failure: a “schedule_meeting” tool exposed duration with no description. The model sent 30 (minutes) on Monday and "30m" on Tuesday after a prompt tweak. Half the calendar API calls 400’d; the agent retried until budget died. One sentence — "Duration in minutes as an integer, e.g. 30" — would have prevented the week of noise.
JSON Schema shape that agents can follow
Minimal production pattern (Anthropic input_schema / OpenAI parameters / MCP inputSchema — same core object):
{
"type": "object",
"additionalProperties": false,
"properties": {
"contact_id": {
"type": "string",
"description": "CRM contact UUID from search_contacts. Never invent an id."
},
"lifecycle_stage": {
"type": "string",
"enum": ["subscriber", "lead", "opportunity", "customer", "churned"],
"description": "Exact lifecycle stage value accepted by the CRM. Use only these enums."
},
"note": {
"type": ["string", "null"],
"description": "Optional internal note, max ~500 chars. Null if no note."
}
},
"required": ["contact_id", "lifecycle_stage", "note"]
}
Rules that hold across providers in 2026:
| Rule | Why |
|---|---|
additionalProperties: false on every object | Required for strict / structured paths; stops mystery keys |
| Describe every property | Biggest accuracy win per edit |
Prefer enum for finite sets | Collapses inventable strings |
Express optionals as nullable + required listing (strict mode style) | Providers differ; portable pattern is “all keys listed, null allowed” |
| Keep nesting shallow | Deep trees confuse models and hit provider limits |
Do not rely on minimum / maximum / pattern for portability: OpenAI may accept and not enforce; Anthropic strict mode may reject unsupported keywords. Validate bounds in the handler and return a clear tool error the model can fix.
Strict mode — when it helps and when it hurts
Helps: write tools where invalid JSON arguments must never reach the handler; high-volume agents where “almost valid” burns retries.
Hurts / fails closed:
- Schema uses unsupported keywords → API 400 before the model runs
- You needed a loosely typed escape hatch for rare admin ops
- You generated schemas from rich Pydantic/Zod models without a “strict-safe” transform
Procedure when enabling strict:
- Freeze the schema subset your provider documents
- Turn on
strict: true(OpenAI tools / Anthropic tool definitions) - Run the golden set; collect schema compiler errors separately from model errors
- Keep handler validation anyway — strict is not authorization
Strict mode is grammar. It is not a policy gate.
Killing the omnibus tool
An omnibus tool looks like this:
{
"name": "crm",
"description": "Do anything in the CRM",
"input_schema": {
"type": "object",
"properties": {
"action": { "type": "string" },
"payload": { "type": "object" }
},
"required": ["action", "payload"]
}
}
Why it fails in production:
- No enum on
action→ invents verbs the handler does not implement payloadis a bag → no property descriptions, no required fields- Privilege escalation → one allowlisted tool name unlocks every CRM write
- Eval blindness → you cannot score “correct tool choice” when there is only one tool
Split by side-effect class and audience:
| Split tool | Side effect | Who may call |
|---|---|---|
search_contacts | Read | Agent |
update_contact_stage | Write (reversible) | Agent + policy |
merge_contacts | Write (hard) | Human approval only |
export_contacts_csv | Read / bulk | Deny in prod agent |
Fewer, sharper tools beat one god-tool every time. If you need composition, put it in your code — not in a free-form action string.
MCP tool schemas vs provider-native function schemas
Same JSON Schema idea; different wrappers and field names:
| Surface | Parameters key | Wrapper |
|---|---|---|
| OpenAI function tools | parameters | { "type": "function", "name", "description", "parameters", "strict?" } |
| Anthropic tools | input_schema | Top-level { name, description, input_schema, strict? } |
| MCP tools | inputSchema | Server-advertised tool; host translates for the model |
Consequences:
- You cannot paste an MCP tool record into OpenAI’s
toolsarray without translation - MCP hosts discover tools at runtime — giant catalogs blow context (token tax)
- Execution location differs: native function calling usually runs in your harness; MCP often runs in a server process with its own credentials
Practical pattern: keep one canonical schema (Zod/Pydantic/JSON), generate provider adapters, and generate MCP inputSchema from the same source. Do not hand-maintain three drifting copies.
Should schemas be generated from Pydantic / Zod?
Yes — with a strict-safe export path.
Checklist:
- Generator emits
additionalProperties: false - Every field has a description string (enforce in CI)
- Enums for closed sets, not open strings
- Target flag matches the provider (
openAi/ Anthropic-safe) - Golden-set fixtures assert on generated schema hashes so silent regen diffs fail CI
Generated schemas without descriptions are how teams ship empty UX at scale.
Measuring tool-call argument accuracy
Do not wait for “task failed” to learn the schema is wrong.
Golden-set columns that matter:
| Column | Example |
|---|---|
expected_tool | update_contact_stage |
expected_args | { "contact_id": "…", "lifecycle_stage": "customer" } |
forbidden_tools | merge_contacts |
notes | Ambiguous user text on purpose |
Score separately:
- Tool choice accuracy — right tool name
- Argument exact-match / schema-valid — right shape
- Semantic arg match — same meaning after normalization (dates, phones)
A schema change that lifts argument exact-match 20 points with flat task pass rate still shipped value — fewer retries, lower cost, fewer weird writes.
First schema review checklist
- Tool name is a verb_noun the handler implements (
update_contact_stage, notcrm) - Top-level description has triggers and negative triggers
- Every property has a non-empty description
- Finite sets are
enum -
requiredmatches handler reality -
additionalProperties: falseon objects - No omnibus
payload: objectwithout inner schema - Side-effect class documented for the policy gate
- Example args in docs or
input_examplesif your provider supports them - Golden cases cover the three failure modes you fear most
Failure mode: schema drift after the CRM upgrade
What broke: CRM added lifecycle_stage values; the tool schema enum stayed frozen; the model correctly wanted evangelist; strict mode / handler rejected; agent looped.
Fix path:
- Schema version in the tool registry
- Contract test against a live or stubbed CRM enum endpoint
- Golden case for each new stage before deploy
- Alert on repeated
invalid_enumtool errors online
Schemas are APIs. Version them.
Pilot minimum
In a Spurlock $1,500 · 5-day pilot: inventory tools and kill omnibus entries; rewrite descriptions + enums on the write path; add a 20–40 case argument-accuracy slice; return handler validation errors the model can fix (not opaque 500s). Zero empty property descriptions on tools that send email or move money.
FAQ
When should I split one “do_anything” tool into many?
As soon as one tool name can perform more than one side-effect class, or when action/payload is free-form. Split by verb and risk: reads vs reversible writes vs irreversible writes. Omnibus tools hide privilege and make evals meaningless.
Do input_examples help more than longer descriptions?
They help different jobs. Descriptions win for when/when-not triggers and units. Examples win for formats the model keeps wrong. For write tools, use both — but put closed sets in enum before you write a novel.
Strict mode — when does it hurt?
When your schema uses keywords the provider’s strict compiler rejects, or when you still need a rare loosely typed admin escape hatch. It also does not replace authorization. Use strict on high-volume write tools after the schema is inside the supported subset; keep handler validation.
How do MCP tool schemas differ from provider function schemas?
MCP advertises inputSchema; OpenAI expects parameters inside a function tool object; Anthropic expects input_schema. The JSON Schema core can be shared, but the wrappers differ — translate from one canonical source. MCP also shifts discovery/execution to servers, which changes credential boundaries and context cost.
Should schemas be generated from Pydantic/Zod?
Yes, if CI enforces descriptions, enums, additionalProperties: false, and a provider-safe export. Generated schemas without those constraints just industrialize empty UX. Hash schemas in golden-set CI so silent regen cannot drift production.
What’s a good first schema review checklist?
Name, triggers, negative triggers, every property described, enums for finite sets, honest required, no omnibus payload bags, side-effect class for the gate, and golden cases for the scary paths. If a write tool fails that list, it does not ship.
CTA
Want schemas that stop inventing arguments on a real job in five days? Start at /agentic or /contact?intent=agentic-pilot.