Validating Tool Arguments Before Execution
The model sent 2026-02-30 and the schema passed it. Agent tool argument validation is the layer that checks arguments against reality, not just against a shape.
"delivery_date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$" # still passes 2026-02-30
}An agent at a logistics company called a tool named schedule_delivery with a date of 2026-02-30. The tool's schema said the field was a string in date format, and 2026-02-30 is a perfectly well-formed date string. It just doesn't exist — February has no 30th. The schema passed it. The downstream system accepted it, silently rolled it to March 2nd, and a shipment went out two days late to a customer who'd paid for next-day. Nobody wrote a bug. Everybody trusted the schema. This is the gap that agent tool argument validation fills: the space between "well-formed" and "actually valid," which a schema alone can't see.
The lesson that team took away, and the one this case study is built around, is that a schema constrains what the model produces but guarantees nothing about what's correct. The model can hand you arguments that are schema-valid and semantically wrong all day long. The execution boundary is where you catch that — or don't.
The Problem This Team Faced
Their agent scheduled deliveries, updated inventory, and issued refunds. Each tool had a clean JSON schema, and they'd assumed — reasonably — that a schema-valid call was a safe call. The February 30th incident broke that assumption, and once they went looking, they found more like it.
A refund tool accepted any order_id string; the model occasionally passed an ID for an order that didn't exist, and the tool cheerfully tried to refund it. An inventory tool took a quantity integer with a minimum of 0; the model sometimes passed a quantity larger than what was in stock, and the tool let stock go negative. Every one of these calls was schema-valid. Every one was wrong.
⚠️ Common mistake: Treating schema validation as argument validation. The schema checks shape — types, formats, ranges the model sees. It cannot check meaning: whether a date exists, whether an ID refers to something real, whether a quantity is available. Confusing the two is how well-formed nonsense sails straight into your production systems.
The Wrong Approach
Their first fix was to tighten the schemas. More patterns, more constraints, more enums. It helped at the margins and missed the core problem, because the failures weren't shape failures.
[object Object],: {
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object], ,[object Object],
}What this does: Enforces a YYYY-MM-DD shape with a regex. It rejects March 5th and 2026/02/30, which is genuinely useful — but 2026-02-30 matches the pattern perfectly and is still a date that doesn't exist. No regex can encode "February has 28 or 29 days depending on the year." Shape validation has a ceiling, and semantic correctness is above it.
The deeper issue was location. All their validation lived in the schema, which is enforced by nobody — the model reads it as a suggestion and mostly complies, but "mostly" is exactly the set of cases that hurt. They had no validation layer that ran, in their own code, before the action.
The Correct Approach
The fix was a dedicated validation step between the model's tool call and the tool's execution — a function that runs in the trusted harness, parses and canonicalizes the arguments, and checks them against reality, not just against a shape.
[object Object], datetime ,[object Object], date
,[object Object], ,[object Object],(,[object Object],):
,[object Object],
,[object Object],:
y, m, d = ,[object Object],(,[object Object],, args[,[object Object],].split(,[object Object],))
parsed = date(y, m, d) ,[object Object],
,[object Object], ValueError:
,[object Object], ArgError(,[object Object],)
,[object Object],
,[object Object], parsed <= date.today():
,[object Object], ArgError(,[object Object],)
,[object Object], parsed.weekday() == ,[object Object], ,[object Object], ,[object Object], ctx[,[object Object],]:
,[object Object], ArgError(,[object Object],)
,[object Object], {,[object Object],: parsed.isoformat()} ,[object Object],What this does: Parses the date into a real date object, which throws on February 30th because the type itself knows the calendar. Then it applies domain rules the schema can't express — no past dates, no Sunday delivery on routes that don't support it — and returns a canonicalized value. The impossible date is caught at step one; the business-invalid dates are caught right after. None of this fits in a schema, and all of it runs in code that actually executes.
The refund and inventory bugs got the same treatment: validation that checks referential integrity, not just format.
[object Object], ,[object Object],(,[object Object],):
order = ctx[,[object Object],].get_order(args[,[object Object],])
,[object Object], order ,[object Object], ,[object Object],:
,[object Object], ArgError(,[object Object],)
,[object Object], args[,[object Object],] > order.total:
,[object Object], ArgError(,[object Object],)
,[object Object], argsWhat this does: Confirms the order actually exists before refunding, and that the refund doesn't exceed what was paid. These are checks against state, not shape — the schema has no way to know whether an ID is real, because that answer lives in your database, not in the argument. Validation is where the argument meets reality.
⚡ Pro tip: Make validation return a canonicalized version of the arguments and have the tool use that, not the raw input. Parsing 2026-02-30 into a date and passing the ISO string downstream means every consumer gets the same clean, verified value — and the act of parsing is itself a validation. If it parsed, it's real; if it's real, everyone downstream can trust it.
Results and What Changed
The February 30th class of bug disappeared entirely, because the validation layer parses every date into a real date object and impossible dates can't survive that. More broadly, the team drew a hard line: the schema is what the model sees, and the validator is what actually runs, and the validator trusts nothing the schema "guaranteed."
Their error rate on tool calls dropped, but the more valuable change was where failures happened. Before, bad arguments failed deep in a downstream system, or worse, succeeded with wrong data. After, they failed loudly at the validation boundary with a clear message the model could read and correct — "order does not exist," "date is in the past" — turning silent data corruption into a recoverable retry.
⚡ Pro tip: Return validation errors to the model as tool results, not as exceptions that kill the run. "ArgError: order 4471 does not exist" is something the model can act on — it can look up the right order and try again. A validation layer that talks back to the model turns most bad arguments into a self-correcting loop instead of a dead run.
How to Apply This to Your Situation
For each tool, write down the difference between "shape-valid" and "actually valid." Shape-valid is what the schema enforces. Actually-valid includes: does this refer to something real, is this value available or in range against current state, does this combination of fields make sense together, and is this operation allowed right now. Everything on that second list is validation-layer work.
Then build one validation function per tool that runs before execution, parses into real types (which catches a surprising amount for free), checks against your domain and your data, canonicalizes the output, and returns errors as messages rather than crashes. Keep it in the trusted harness — never rely on the model or the schema to do this, because the model can be wrong and the schema doesn't run.
The pattern generalizes beyond these examples. A calendar agent validates that a room is free before booking it. A finance agent validates that an account has funds before transferring. A support agent validates that a user owns a subscription before canceling it. In every case, agent tool argument validation is the layer that checks the argument against the world, not just against itself, and in every case it's the difference between an error the model can recover from and a wrong action nobody notices until a customer complains.
Ordering Checks and Guarding Against Replays
Once you have a validation layer, the order of the checks inside it matters more than it looks. Run the cheap, self-contained checks first — parse the types, check ranges — before the expensive ones that hit your database or an external service. There's no reason to query whether an order exists if the amount field didn't even parse as a number. Fail fast on the cheap checks, and you spend database round-trips only on arguments that already cleared the basics.
Agent tool argument validation also has to think about repetition. Agents retry, and a retried tool call can arrive twice — the model didn't see the first result, so it asks again. For anything that changes state, that means "refund order 4471" can fire twice and refund twice. The validation layer is a natural place to enforce idempotency by checking whether this exact operation already happened.
[object Object], ,[object Object],(,[object Object],):
op_key = ,[object Object],
,[object Object], ctx[,[object Object],].seen(op_key):
,[object Object], ArgError(,[object Object],)
order = ctx[,[object Object],].get_order(args[,[object Object],])
,[object Object], order ,[object Object], ,[object Object],:
,[object Object], ArgError(,[object Object],)
ctx[,[object Object],].mark(op_key)
,[object Object], argsWhat this does: Builds a key from the operation and its arguments, and refuses if that exact operation was already recorded — so a duplicate refund request is caught at validation instead of double-charging. Putting the idempotency check in the validator means every state-changing tool inherits replay protection in the same place it inherits everything else, rather than each tool reinventing it.
⚡ Pro tip: Derive the idempotency key from the meaningful arguments, not from a request ID the model generates. The model may produce a new request ID on each retry even though it's semantically the same operation — so a model-supplied ID won't catch the duplicate. A key built from "refund + order + amount" catches it because the meaning is identical even when the wrapper isn't.
Next Steps
Argument validation pairs naturally with structured tool schemas — the schema shapes what the model attempts, the validator verifies what it produced. Together they're two layers of the same defense, and neither is sufficient alone.
Keep your per-tool validators versioned alongside the schemas and prompts they guard in a library like PromptABCD, so the domain rules the team learned through incidents — no past dates, no phantom orders, no overdrawn refunds — travel with the tools and don't have to be rediscovered, one late shipment at a time, in the next agent someone builds.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
