Guardrails
Guardrails intercept and modify agent input or output at runtime. They run before user input reaches the model (input guardrails) or after the model generates output (output guardrails). Each guardrail can inspect content, modify it, or block the operation entirely.
Guardrails are created using helper functions createInputGuardrail()
and createOutputGuardrail()
, which provide type safety and IDE autocomplete.
Output Guardrails
Output guardrails process model-generated content before it reaches the application or user. They can modify streaming chunks in real-time and validate the final output.
When to Use Output Guardrails
Use output guardrails to:
- Redact sensitive information (emails, phone numbers, credit cards) from model output
- Enforce content policies by blocking profanity or inappropriate content
- Limit output length to fit UI constraints or API quotas
- Transform content in real-time during streaming (e.g., replace patterns as they appear)
- Validate that model output meets structural or business requirements
Core Concepts
Handler vs StreamHandler
Output guardrails define two optional functions:
handler
runs once after the model finishes generating. It receives the complete output and returns a result indicating whether to allow, modify, or block it.
handler: async ({ output, originalOutput, context }) => {
// Validate or transform complete output
if (output.length > 1000) {
return { pass: false, action: "block", message: "Output too long" };
}
return { pass: true };
};
streamHandler
runs on each chunk as the model streams output. It can modify the chunk, drop it by returning null
, or abort the entire stream.
streamHandler: ({ part, state }) => {
if (part.type !== "text-delta") return part;
// Access delta property (with text fallback for compatibility)
const chunk = part.delta ?? part.text ?? "";
const sanitized = chunk.replace(/\d{4,}/g, "[digits]");
return { ...part, delta: sanitized, text: undefined };
};
Use streamHandler
when you need to modify content in real-time as it streams to the user. Use handler
when you need to validate or transform the complete output after streaming finishes.
Actions
A guardrail result specifies one of three actions:
allow
: Pass the content through unchangedmodify
: Replace the content withmodifiedOutput
ormodifiedInput
block
: Reject the operation and throw an error
handler: async ({ output }) => {
if (containsPII(output)) {
return {
pass: true,
action: "modify",
modifiedOutput: redactPII(output),
message: "PII redacted",
};
}
return { pass: true, action: "allow" };
};
If you set pass: false
, the framework infers action: "block"
and terminates the operation with a VoltAgentError
.
State Sharing
Both streamHandler
and handler
receive the same state
object. Use this to track information during streaming and reference it in the final handler.
streamHandler: ({ part, state }) => {
if (part.type !== "text-delta") return part;
// Initialize state on first chunk
state.counter ??= { chunks: 0, redacted: 0 };
state.counter.chunks++;
const chunk = part.delta ?? part.text ?? "";
if (chunk.includes("secret")) {
state.counter.redacted++;
return { ...part, delta: chunk.replace(/secret/g, "[redacted]"), text: undefined };
}
return part;
};
handler: async ({ output, context }) => {
const counter = context.context.get("counter") ?? { chunks: 0, redacted: 0 };
return {
pass: true,
metadata: {
totalChunks: counter.chunks,
redactedChunks: counter.redacted,
},
};
};
Aborting Streams
Call abort(reason)
in a streamHandler
to terminate streaming immediately with an error.
streamHandler: ({ part, abort }) => {
if (part.type !== "text-delta") return part;
const chunk = part.delta ?? part.text ?? "";
if (chunk.includes("forbidden")) {
abort("Content policy violation detected");
}
return part;
};
The framework marks the guardrail span with SpanStatusCode.ERROR
and throws a VoltAgentError
with your reason.
Step-by-Step Tutorial
1. Handler-Only Guardrail: Validate Final Output
This guardrail checks the complete output after the model finishes generating. It blocks responses that don't include a required disclaimer.
import { createOutputGuardrail } from "@voltagent/core";
const disclaimerGuardrail = createOutputGuardrail({
id: "require-disclaimer",
name: "Require Disclaimer",
handler: async ({ output }) => {
if (typeof output !== "string") {
return { pass: true };
}
if (!output.includes("Not financial advice")) {
return {
pass: false,
action: "block",
message: "Response must include disclaimer",
};
}
return { pass: true };
},
});
2. Stream-Only Guardrail: Modify Chunks in Real-Time
This guardrail redacts long digit sequences as they stream. Users never see the original digits.
import { createOutputGuardrail } from "@voltagent/core";
const digitGuardrail = createOutputGuardrail({
id: "redact-digits",
name: "Redact Digit Sequences",
streamHandler: ({ part }) => {
if (part.type !== "text-delta") {
return part;
}
// Extract chunk text (delta preferred, text fallback)
const chunk = part.delta ?? part.text ?? "";
if (!chunk) return part;
const redacted = chunk.replace(/\d{4,}/g, "[digits]");
// Return modified part without original text field
return {
...part,
delta: redacted,
text: undefined,
};
},
});
3. Combined Guardrail with State: Stream + Final Handler
This guardrail tracks redactions during streaming and reports them in the final handler.
import { createOutputGuardrail } from "@voltagent/core";
type RedactionState = { redactions: number };
const trackingGuardrail = createOutputGuardrail({
id: "track-redactions",
name: "Track Redactions",
streamHandler: ({ part, state }) => {
if (part.type !== "text-delta") {
return part;
}
// Extract chunk text
const chunk = part.delta ?? part.text ?? "";
if (!chunk) return part;
// Initialize state
const guardState = (state.tracking ??= { redactions: 0 } as RedactionState);
const redacted = chunk.replace(/\d{4,}/g, (match) => {
guardState.redactions++;
return "[digits]";
});
return { ...part, delta: redacted, text: undefined };
},
handler: async ({ output, originalOutput, context }) => {
// Access state from streaming phase
const guardState = context.context.get("tracking") as RedactionState | undefined;
if (!guardState || guardState.redactions === 0) {
return { pass: true };
}
return {
pass: true,
action: "modify",
modifiedOutput: output,
message: `Redacted ${guardState.redactions} digit sequence(s)`,
metadata: {
redactionCount: guardState.redactions,
originalLength: originalOutput?.length,
sanitizedLength: output?.length,
},
};
},
});