AI Agent Guardrails: Validate Output in Production
Your agent worked perfectly in staging. First few weeks in production too. Then, one Tuesday afternoon, the model returned a negative price. Or a made-up email address. Or an action that wasn't in the valid options list.
No exception was thrown. Your code processed the response, sent it downstream, and moved on. You don't find out for three days.
This is the core problem guardrails solve: an LLM can return something that looks structurally correct but is semantically impossible or dangerous. A valid number that's out of range. An email address with correct format but a fictional domain. An action your system should never execute.
If you're building AI agents to automate business processes, this architecture isn't optional — it's what separates a production-grade agent from one that silently fails at scale.
Layer 1: Schema Validation with Zod
The first layer is the simplest and the most overlooked. Before you use LLM output anywhere in your code, parse it against a strict schema.
import { z } from 'zod'
const ActionEnum = z.enum(['send_quote', 'schedule_call', 'escalate', 'close'])
const AgentOutputSchema = z.object({
action: ActionEnum,
price: z.number().positive().max(100_000),
customer_email: z.string().email(),
message: z.string().min(10).max(500),
confidence: z.number().min(0).max(1),
})
async function runAgentWithGuardrail(input: string) {
const rawOutput = await callLLM(input)
const parsed = AgentOutputSchema.safeParse(rawOutput)
if (!parsed.success) {
await logGuardrailFailure({
input,
rawOutput,
errors: parsed.error.issues,
})
return fallbackResponse()
}
return parsed.data
}
The most common mistake I see in production codebases: using schema.parse() instead of schema.safeParse(). The first throws an exception that isn't always caught cleanly. The second returns { success: boolean; data | error } — you handle failure explicitly, without surprises.
A practical rule: the more critical the agent's action, the stricter the schema should be. An agent that generates text can tolerate more flexibility than one that executes transactions, sends emails, or modifies records in a database.
Layer 2: Content Guardrails
Schema validation doesn't catch everything that can go wrong. A customer service agent can respond with perfect format and still:
- Invent a return policy that doesn't exist
- Offer an unauthorized discount
- Reply in a different language than the customer used
- Include data from a different customer in the response
For this, you need a second layer of semantic validation. Two practical approaches:
Deterministic rules — cheap and fast:
function contentGuardrails(
output: AgentOutput,
context: RequestContext
): { passed: boolean; issues: string[] } {
const issues: string[] = []
// Phrases the agent should never fabricate
const BANNED_PHRASES = [
'as per our policy',
'you are entitled to',
'we guarantee',
]
for (const phrase of BANNED_PHRASES) {
if (output.message.toLowerCase().includes(phrase)) {
issues.push(`Banned phrase detected: "${phrase}"`)
}
}
// Price outside allowed range
if (output.price < MINIMUM_PRICE || output.price > MAXIMUM_PRICE) {
issues.push(`Price ${output.price} out of allowed range`)
}
// Language consistency
const detected = detectLanguage(output.message)
if (detected !== context.customerLanguage) {
issues.push(`Language mismatch: expected ${context.customerLanguage}`)
}
return { passed: issues.length === 0, issues }
}
LLM judge — more flexible, higher cost:
async function llmJudge(
input: string,
agentOutput: string,
businessContext: BusinessContext
): Promise<{ approved: boolean; reason: string }> {
const judgePrompt = `
Evaluate whether this AI agent response is safe to send to the customer.
User input: ${input}
Agent response: ${agentOutput}
Business context: ${JSON.stringify(businessContext)}
Reject if: contains invented policies, unauthorized discounts,
data from other customers, offensive content, or major inconsistencies.
Respond in JSON: { "approved": boolean, "reason": string }
`.trim()
const result = await callLLM(judgePrompt, { model: 'claude-haiku-4-5' })
return JSON.parse(result)
}
Use the cheapest model available for the judge — Haiku instead of Sonnet. The cost of this layer drops to 10–15% of the main LLM call. For many use cases, deterministic rules are sufficient; the LLM judge adds value when content is open-ended and hard to validate with fixed rules.
Layer 3: Circuit Breaker for Repeated Failures
The first two layers act response by response. But there's a more dangerous pattern: the agent fails consistently over a period of time — because of a silent change in model behavior, a recurring edge case input, or a bug in the context injected into the prompt.
Without a circuit breaker, the agent keeps running, keeps accumulating failures, and nobody notices until a customer escalates.
class AgentCircuitBreaker {
private failures = 0
private lastFailureTime = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
private readonly THRESHOLD = 5
private readonly RESET_AFTER = 300_000 // 5 minutes
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
const elapsed = Date.now() - this.lastFailureTime
if (elapsed > this.RESET_AFTER) {
this.state = 'half-open'
} else {
throw new Error('Circuit open: agent temporarily disabled')
}
}
try {
const result = await fn()
if (this.state === 'half-open') this.reset()
return result
} catch (err) {
this.recordFailure()
throw err
}
}
private recordFailure() {
this.failures++
this.lastFailureTime = Date.now()
if (this.failures >= this.THRESHOLD) {
this.state = 'open'
alertOps(`Circuit OPEN after ${this.failures} consecutive guardrail failures`)
}
}
private reset() {
this.failures = 0
this.state = 'closed'
}
}
When the circuit breaker opens, the system stops calling the agent and activates a fallback: a predefined safe response, human escalation, or queuing for later processing. The team gets an alert and can investigate before the impact compounds.
This pattern is especially critical in agents with AI integration connected to external systems — CRMs, ERPs, billing platforms — where a cascade failure can propagate bad data across multiple systems simultaneously.
What Changes in Real Production
Most of the projects I work with arrive without any of these three layers. The agent works well in demos and well during the first weeks. The first serious incident comes when volume scales or when the model behaves unexpectedly on an edge case input that never appeared in staging.
Projects where we implement all three layers from the first sprint see agent error rates below 0.3% in production. Projects that arrive without guardrails typically have 2–8% of incorrect responses passing through undetected.
The difference isn't which model you use. It's the validation architecture around the model.
If you're building an AI agent for production and want a solid architecture from day one, tell me what you're working on — we'll review the architecture together before the first bug reaches production.