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_delivery2026-02-302026-02-30The 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_idquantityminimum⚠️ 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 5th2026/02/302026-02-30The 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
dateThe 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-30dateResults and What Changed
The February 30th class of bug disappeared entirely, because the validation layer parses every date into a real
dateTheir 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.
