A production AI agent should never treat “the model produced something plausible” as equivalent to “the system is allowed to continue.”
The model is probabilistic. Your application is responsible for deciding whether inputs are acceptable, outputs are structurally valid, tool calls are authorized, side effects are safe, and important actions actually succeeded.
That responsibility belongs in a validation layer.
A good validation layer is not one giant safety prompt. It is a set of checks placed at the boundaries where bad model behavior can become bad system behavior.
The useful mental model is:
input
↓
model
↓
structured decision
↓
tool/action proposal
↓
execution
↓
real-world state
↓
final answer
Each arrow is a validation opportunity.
> Use models to propose and interpret. Use deterministic validation to decide what the system will accept, execute, persist, or return.
Why validation matters more for agents than chatbots
A text chatbot can be wrong in its answer.
An agent can be wrong in its action.
That difference is huge.
Consider:
User: “Refund the duplicate charge.”
The model decides to call:
{
"tool": "refund_invoice",
"invoice_id": "INV-42",
"amount": 50000
}
Before executing that call, the system must answer questions the model should not be trusted to enforce alone:
Does the user own INV-42?
Is 50,000 the correct currency/amount?
Is that amount refundable?
Was this invoice already refunded?
Does this amount require approval?
Is this exact operation a duplicate?
Those are validation questions.
Validation is a layered architecture
A production system usually needs several different validation layers.
| Layer | What it protects |
|---|---|
| Input validation | Bad/malicious/invalid requests |
| Context validation | Wrong tenant, stale or unauthorized context |
| Structured-output validation | Invalid model output shape |
| Tool-call validation | Wrong tool or invalid arguments |
| Authorization/policy validation | Forbidden operations |
| Execution-result validation | Malformed or untrusted tool results |
| Postcondition verification | Actions that claimed success but did not happen correctly |
| Final-output validation | Unsafe, unsupported, or policy-invalid responses |
Trying to collapse all of these into one LLM judge usually creates a weaker system.
1. Input validation: reject bad requests early
Some inputs should never reach the expensive or privileged agent workflow.
Examples:
missing required parameters
oversized payload
unsupported file type
invalid tenant/resource ID
malicious content
request outside product scope
Input validation can be deterministic:
schema
size limit
allowed MIME type
known enum
rate limit
or model-assisted when the property is semantic:
is this request within the support agent’s domain?
Blocking vs parallel validation
OpenAI’s current Agents SDK exposes input guardrails that can either run in parallel with the agent or block the agent until the check completes.
That trade-off is important.
parallel validation
→ lower latency
→ model/tool work may start before guardrail finishes
blocking validation
→ higher latency
→ no model/tool work begins if request is rejected
For high-risk tool access, blocking validation is often the safer boundary.
2. Context validation: make sure the model sees the right truth
An agent may receive context from:
session state
RAG
memory
tools
user profile
subagents
Before that context is trusted, validate its scope and freshness.
Tenant scope
A RAG result should satisfy:
document.tenant_id == current_tenant
before entering the prompt.
Resource ownership
A tool result should be returned only if the authenticated user is permitted to access it.
Freshness
A cached state such as:
subscription_status = active
may be invalid if it was loaded hours ago and the task requires current billing state.
The model cannot validate freshness if it does not know the authoritative source.
3. Structured outputs: validate shape before meaning
Whenever model output is consumed by code, prefer a structured contract over free-form text.
Instead of:
“This appears to be a billing issue and it probably needs escalation.”
use:
{
"category": "billing",
"needs_escalation": true,
"reason": "duplicate charge"
}
OpenAI’s current Structured Outputs support lets developers constrain output to a supported JSON Schema.
That removes an entire class of interface failures:
- missing fields
- wrong field types
- unexpected labels
- parser ambiguity
But schema-valid does not mean correct
This can be perfectly valid JSON:
{
"category": "shipping",
"needs_escalation": false
}
while still being semantically wrong for a billing complaint.
So validate in two stages:
1. structure valid?
2. decision correct enough for this workflow?
Use deterministic schemas at every machine boundary
Common structured boundaries include:
router output
tool arguments
subagent result
memory record
approval proposal
final API response
Example router contract:
{
"route": "billing|technical|account|human",
"confidence": 0.0,
"reason": "string"
}
Example delegation contract:
{
"status": "completed|blocked|needs_input",
"findings": [],
"open_questions": []
}
Schema validation is cheap. Use it.
4. Tool-call validation: validate before execution
Tool calls are the most important boundary in an agent system because they can affect the outside world.
A production tool pipeline should look more like:
model proposes tool call
↓
JSON/schema validation
↓
authorization
↓
business-rule validation
↓
risk / approval check
↓
execute
not:
model proposes tool call
→ execute immediately
Tool schema validation is necessary but insufficient
Suppose the tool schema says:
{
"invoice_id": "string",
"amount": "number"
}
The model generates:
{
"invoice_id": "INV-42",
"amount": 1000000
}
The arguments are structurally valid.
But the call may violate:
invoice refundable balance
user authorization
currency assumptions
refund policy
approval threshold
Validation must include application semantics.
OpenAI tool guardrails show where this boundary belongs
The current OpenAI Agents SDK supports tool input guardrails and tool output guardrails around function tools.
Tool input guardrails can:
- allow the tool call
- reject it and return model-visible feedback
- halt execution with a tripwire
Tool output guardrails can inspect or reject the result after execution.
The broader architecture lesson is vendor-neutral:
> Validation belongs around each privileged tool call, not only around the first user prompt and final answer.
Validate tools using application context
A validator often needs trusted runtime context unavailable to the model.
Example:
current_user_id
current_tenant_id
allowed_store_ids
approval_state
policy_version
Tool validator:
requested invoice belongs to tenant?
requested amount <= refundable balance?
approval present if required?
Do not ask the model to reconstruct authorization from conversation text.
5. Authorization is validation, but it should stay deterministic
The system should enforce:
who
can perform what action
on which resource
under which conditions
before executing a privileged tool.
Examples:
support agent can read order
support agent cannot issue large refund
billing agent can prepare refund proposal
finance approver can authorize high-value refund
These rules belong in code or a real authorization/policy engine.
Prompts can guide behavior but should not be the only control.
Separate “valid” from “authorized”
A tool call can be valid and unauthorized.
Example:
{
"tool": "delete_user",
"user_id": "user_123"
}
The schema is valid.
The user ID exists.
The caller still may not have permission.
Keep these stages conceptually separate:
syntax/schema
→ resource validity
→ authorization
→ business policy
→ execution
This makes failures easier to debug and audit.
6. Business-rule validation should not be delegated to the model
Suppose a SaaS product has this rule:
Annual plans can be refunded only within 7 days,
except duplicate charges which are always reviewable.
The model may retrieve and understand that policy.
The backend should still enforce the critical rule when money changes hands.
A deterministic check might be:
if duplicate_charge:
allow_review_path()
elif days_since_purchase <= 7:
allow_standard_refund()
else:
reject_refund()
Models are useful for classification and explanation.
Critical invariants belong in code.
7. Human approval is a validation stage
Some actions are not safe to fully automate even after normal validation passes.
Examples:
large refund
production deployment
external legal message
delete customer data
permission escalation
high-value purchase
Use a workflow like:
model proposes action
↓
deterministic validation
↓
risk threshold exceeded?
├── no → execute
└── yes → human approval
Approval should happen after the exact action is known.
Bad:
“May I take care of this?”
Better:
“Refund ₹48,200 to invoice INV-42 using payment account X?”
Approval does not replace validation
A human should not be asked to approve malformed or impossible operations.
The order should be:
schema valid
→ authorization valid
→ business rules valid
→ human approval if needed
→ execute
not:
model proposes anything
→ human must detect every technical problem
Good validation reduces approval fatigue.
8. Validate tool outputs too
Tool results are often treated as trustworthy merely because they came from an API.
But tool output can contain:
- malformed data
- stale state
- user-controlled text
- unexpected nulls
- prompt-injection content
- partial responses
A tool output contract should validate structure:
required fields
field types
status code
resource identity
and, where needed, content:
result belongs to requested tenant
returned amount matches expected currency
text from third party is untrusted evidence
Tool-output validation protects context quality
Imagine a search tool returns 10 MB of HTML.
Technically the call succeeded.
Operationally it is terrible model context.
Validation/normalization can enforce:
max result size
allowed fields
safe text extraction
deduplication
redaction
pagination
The goal is not only security. It is keeping context high-signal.
9. Postcondition verification: did the action actually happen?
This is one of the most important validation layers and one of the most commonly missing.
Suppose the model calls:
create_refund()
and receives:
200 OK
That does not necessarily mean the intended business state is correct.
For important mutations, verify the resulting state.
execute refund
↓
lookup refund by operation ID
↓
status == succeeded?
amount == expected?
invoice == expected?
This is postcondition verification.
Verify side effects, not prose
A model may say:
“The customer has now been refunded.”
That sentence is not evidence.
The evidence is:
{
"refund_id": "RF-991",
"status": "succeeded",
"amount": 2499
}
returned from the authoritative downstream system.
The final answer should be generated only after the system knows the postcondition.
Ambiguous outcomes need a dedicated state
Distributed systems create a classic problem:
write succeeds downstream
response is lost
caller sees timeout
A naive agent retries and duplicates the action.
Validation should model three states:
confirmed_success
confirmed_failure
outcome_unknown
If the outcome is unknown:
verify by operation/idempotency ID
→ then decide whether retry is safe
Do not treat timeout as synonymous with failure.
Idempotency is part of validation architecture
Side-effecting operations should carry stable logical identities.
Example:
idempotency_key = run_id + logical_action_id
The validator can reject or collapse duplicates.
This is especially important for:
payments
refunds
email sends
order creation
file deletion
external messages
An agent can repeat itself. The system should remain safe anyway.
10. Final-output validation
Even after the workflow executed safely, the response shown to the user may need validation.
Examples:
PII leakage
unsupported factual claim
missing required citation
forbidden internal details
wrong output schema
claim that mutation succeeded when it did not
Final-output checks can be deterministic or model-assisted.
Deterministic
JSON schema
required fields
no raw secrets
citation IDs exist
Model-assisted
Does this answer overclaim evidence?
Does it violate support policy?
Is it appropriate for the user-facing channel?
Use the cheapest reliable grader for the task.
Validation should know what source is authoritative
A reliable agent architecture needs an explicit source-of-truth hierarchy.
Example:
Payment status
→ payment provider / billing DB
User preference
→ canonical memory store
Refund policy
→ approved policy knowledge base
Current order state
→ order service
The model can combine those facts.
It should not decide which source is authoritative every time from scratch.
RAG validation: retrieval can be wrong even when generation is right
A RAG pipeline should validate:
permission scope
source version
recency
source authority
relevance
citation provenance
If a deprecated policy and current policy both exist, a relevance score alone may not be enough.
Metadata filters can enforce:
status = active
policy_version = current
before generation.
Memory validation prevents stale or poisoned state
Persistent memory can influence future runs.
A memory write should validate:
is this worth remembering?
which scope owns it?
is it explicit or inferred?
is it sensitive?
can it expire?
what is its provenance?
Do not let arbitrary web content or tool output directly become trusted long-term policy memory.
Memory writes are state mutations and deserve validation too.
Multi-agent validation needs trust boundaries
If Agent A delegates to Agent B, Agent B’s result is not automatically trusted.
Validate:
which agent produced this?
which task was it responding to?
was it authorized to access the data?
does the result match the expected contract?
A subagent can be mistaken or manipulated.
Treat its output as attributed evidence, not higher-priority instructions.
Validation failures need explicit behavior
Do not silently “fix” every failure.
Different failures need different responses.
| Failure | Good behavior |
|---|---|
| Invalid JSON/schema | Retry/repair within bounded limit |
| Invalid tool args | Return structured error to model |
| Authorization denied | Stop; do not retry |
| Business rule rejected | Choose valid alternative or explain |
| Human approval rejected | Stop action |
| Tool output malformed | Retry/read fallback if safe |
| Mutation outcome unknown | Verify before retry |
| Final output violates policy | Regenerate or escalate |
The validation layer should produce typed outcomes so the runtime knows what happens next.
Avoid generic “retry validation failed” loops
If the validator says:
authorization denied
then retrying the same call three times is nonsense.
If it says:
missing required argument: invoice_id
then returning the error to the model for correction may work.
Classify validation failures.
This prevents loops and saves tokens.
Validation should be observable
For every important check, record safe metadata:
validator name
stage
pass/fail
failure class
resource/action
policy version
approval state
latency
Do not necessarily log the entire sensitive payload.
A trace could look like:
run
├─ input_schema: pass
├─ retrieval_acl: pass
├─ tool_args_schema: pass
├─ authorization: pass
├─ refund_limit: approval_required
├─ human_approval: approved
├─ execute_refund: success
├─ postcondition: pass
└─ final_output_guard: pass
That is much easier to debug than “agent returned error.”
Validation rules need versioning
If a policy changes, you should know which version validated each run.
Useful metadata:
validator_version
policy_version
schema_version
tool_version
Example:
refund_policy_v12
If a customer dispute occurs later, you can reconstruct what rule was active.
Do not turn every validator into another LLM call
A common overengineering mistake is:
LLM generates output
→ LLM validator
→ LLM safety validator
→ LLM policy validator
→ LLM quality validator
This adds:
- latency
- cost
- nondeterminism
- new failure modes
Use deterministic code whenever the condition is objective.
Good deterministic checks:
schema
range
ownership
permissions
state transition
idempotency
balance
file path
URL allowlist
Use an LLM validator only when the property itself is semantic.
A production validation pipeline
A useful architecture looks like:
User Request
│
▼
Input Validation
│
▼
Authorized Context Assembly
│
▼
LLM / Agent
│
▼
Structured Output Validation
│
▼
Proposed Tool Call
│
├─ schema validation
├─ authorization
├─ business policy
├─ risk classification
└─ approval check
│
▼
Execute Tool
│
▼
Tool Output Validation
│
▼
Postcondition Verification
│
▼
Final Output Validation
│
▼
User
Not every workflow needs every stage.
But every privileged boundary should have a deliberate answer to “what validates this?”
Example: safe refund agent
User:
> “Refund the extra annual-plan charge.”
Step 1: identity
Application supplies authenticated customer_id.
Step 2: model decision
Agent identifies duplicate billing and proposes:
{
"invoice_id": "INV-42",
"action": "refund_duplicate_charge"
}
Step 3: deterministic validation
System checks:
invoice belongs to customer
charge is duplicated
amount is refundable
not already refunded
Step 4: approval
If amount exceeds threshold, workflow pauses.
Step 5: execute
Call downstream payment API with idempotency key.
Step 6: postcondition
Read refund state by operation ID.
Step 7: final response
Only after verification does the model say:
Your refund was successfully issued.
This is much safer than adding “be careful with refunds” to the system prompt.
Validation vs guardrails vs evals
These terms overlap but have different operational roles.
Validation
Runtime checks that decide whether execution can continue.
Guardrails
A mechanism for implementing certain runtime input/output/tool checks.
Evals
Offline or online measurements used to test system quality across many cases.
A useful relationship is:
evals discover common failure
↓
team fixes architecture
↓
critical invariant becomes runtime validation
Not every eval should become a runtime guardrail.
Not every runtime invariant needs an LLM eval.
Production checklist
Before calling an agent workflow production-ready, verify:
- User input has schema/size/type checks
- Retrieved context is authorization-scoped
- Machine-consumed model outputs are structured
- Tool inputs are schema-validated
- Tool calls are independently authorized
- Business rules are deterministic where they must always hold
- High-risk actions have approval gates
- Tool outputs are validated and treated according to trust level
- Important mutations are idempotent
- Ambiguous mutation outcomes have a verification path
- Critical actions have postcondition checks
- Memory writes have provenance/scope validation
- Subagent results are contract-validated
- Final user output has required checks
- Validation failures are typed, not generic strings
- Validators/policies are versioned and observable
- LLM validators are used only when semantic judgment is actually required
Final takeaway
A reliable agent should not be trusted because it is smart.
It should be trusted because the system validates the boundaries where intelligence becomes action.
Use structured outputs for machine interfaces. Validate tool arguments before execution. Enforce authorization and business invariants outside the model. Verify important postconditions after writes. Treat tool and subagent outputs according to their trust level. Version and trace the checks.
> The model can choose what it wants to do. The validation layer decides what the application will actually allow to happen.
That separation is one of the most important differences between an AI demo and production software.

Discussion (0)