Cost
A check before the main AI model can stop an off-task request before the expensive call runs.
Home · Agents · 09 · Guardrails
Concept card · Agents
What this gets you. Check what goes into and comes out of your agent, and stop or reroute a run when a check fails.
Next on this goal: Failure modes.
A guardrail is an automatic check at a fixed point in your agent's run. When the check fails, the run gets a fixed reply, a retry, a rerouted handoff, or a stop.
Oct 26: OWASP sources now point to the 2026 list, where some entries have new numbers.
Oct 21: First card. A check can stop or reroute a run before a bad reply or a bad tool call lands. Sources include Anthropic, Google, the Model Context Protocol, OpenAI, OWASP and NIST.
From Prompt injection: where can untrusted text enter a run?
In the user's message, and also in documents and tool results the agent reads later. What these checks defend against: Prompt injection.
A guardrail is an automatic check at a fixed point in your agent's run: on the input, around a tool call, on the reply, or on the run as a whole. When the check fails, it changes what happens next. The run gets a fixed reply, a retry, a rerouted handoff, or a stop.
The steps, in order.
Example: made up to show the idea.
The same support assistant, with checks in five places.
| Where | Check | On fail |
|---|---|---|
| Input | Asks for a password or secret | A fixed reply: how to reset your own password |
Before create_ticket |
The account id matches the signed-in customer | An error goes back to the loop to fix; after 2 failures, stop |
After search_docs |
Text marked untrusted, such as the pasted note | Kept as data, labelled, not treated as policy |
| Output | The reply cites a doc, and no account number appears | Redact, or swap in "I don't know" and hand off. |
| Run | 4 tool calls, 1 ticket per conversation, a time limit | Stop and hand the transcript to the support person. |
A customer asks: "Open a ticket for account 4471." That isn't the signed-in account, so the check before create_ticket fails. The loop gets "account doesn't match", asks the customer to confirm, and no ticket is filed. Later the output checker times out. The reply is held and handed to a person instead of being sent unchecked.
| If the check is about | Put it | On fail |
|---|---|---|
| what the user asked | before the main AI model | a fixed reply, or hand to a person |
| a tool's arguments | right before that tool runs | return the error to fix, or stop |
| what a tool returned | right after that tool | label or drop the text, then continue |
| the reply | before it leaves | redact, or swap in a safe reply |
| how long or how often | on the whole run | stop and hand off |
| a check that can't run | anywhere | pause, don't pass |
Beyond the schema: does this value make sense for this user? See Tool calling.
A check before the main AI model can stop an off-task request before the expensive call runs.
Checking a tool's values catches the wrong account before a ticket lands on it.
OWASP, LLM10:2026 Improper Output Handling says to treat the model like any other user and validate what it sends on. Encode its output for the page it lands on.
Pick one tool your agent calls that changes something. Write one check for its arguments, put it right before the call, and write what happens when it fails, and when the check itself is down.
Checking only the first message. In a run with several steps or agents, the input check runs once. Text that arrives later from tools doesn't meet it. Put a check next to the tool.
A check makes sure create_ticket gets the signed-in customer's account id. Where does it go?
The output check times out. What's the safer default?
Which job is a guardrail's, rather than a permission's?
Next: CI checks are guardrails on a code change, and a person still merges. Working with a coding agent.
Example: made up to show the idea.
A six-event trace for the same support assistant. The first two and the last are worked for you. Place the check for the three events in between.
search_docs result that includes the pasted note. After-tool check: labelled untrusted, kept as data. Why: the note is text the model reads, not a rule your app follows.e3: create_ticket is called with account 4471. The signed-in account is 2208. Where does the check go, and what happens?
e4: the reply contains a full account number. Where does the check go?
e5: this is the 5th tool call, and the run limit is 4. What happens?
For one tool your agent calls, write the check before it, the check after it, and what happens when each fails or can't run.
Before create_ticket: the account id must match the signed-in customer. On fail, return the error and stop after two tries. After search_docs: label untrusted text and keep it as data. If either check can't run, pause and hand the transcript to the support person.
A guardrail is a decision you wrote down before the request arrived. The main model doesn't get to waive it.
OpenAI, Guardrails and human review places input guardrails on the first agent and output guardrails on the last. Tool guardrails run on the tools they're attached to. OpenAI Agents SDK, Guardrails says the same about a chain: an input check covers the start, an output check covers the end, and a tool check runs on every guarded call.
A parallel input check is the SDK's default. It can be faster, and the main agent may already have run tools before the check trips. A blocking check, run_in_parallel set false, stops those tools first. Block when a tool can change something. A tripwire then stops the run, reroutes to the person named on Permissions, or returns a fixed reply. Don't let the main model decide whether its own check matters.
Model Context Protocol, Tools says servers must validate inputs, enforce access controls, rate-limit, and sanitize outputs. Clients should validate results before passing them to the language model, set timeouts, and log tool usage. A check that lives only in the prompt is the same miss as on Permissions: the model can still propose the call.
Model output can carry markup or code. OWASP, LLM10:2026 Improper Output Handling says to encode output for the page it lands on and to use parameterized queries. Google, Safety and Security for AI Agents says to escape model output in the UI, and to run in-tool checks and before-tool callbacks. A callback can also have a cheap, fast model screen inputs and outputs.
Anthropic, Building effective agents (published Dec 19, 2024) says a separate screening call tends to perform better than one call that both does the task and checks it. Treat that verdict as one signal, the way Safety treats a moderation flag, and test it the way you test any other check. Test your checks: Evaluation basics.
Cap steps, repeated failures, calls per minute, and how long the run may wait. Your stop rules from Agent loop are the done line, the turn limit, and the failure limit. A guardrail adds the rate and the timeout, and it says what happens when a check is down.
NIST, Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile asks, in MANAGE 2.4, for an assigned way to supersede, disengage, or deactivate a system whose outcomes don't match the intended use. MS-4.2-004 says to record when a person or another system overrides a decision.
They catch what you thought to check. A missed check can still reach a tool that is on the list. Keep Permissions so an act tool isn't available unless this task needs it, and so a person still approves the action. What your product won't do at all: Safety.
See the five questions on the support assistant. That step checks whether an answer matches the docs.
For when you wire the checks yourself: where each one runs, what blocks, and how to count hits and misses. The rules and the queue are yours.
A check that errors or times out holds the run for a person. That's failing closed.
A sketch, not a library. Call it where the Agent loop notes run a tool.
// Sketch. RULES, tools and queue are yours.
export async function guardedCall(call, user, tools, key) {
const tool = tools[call.tool];
const v = await checkIn(call, user); // blocking
if (v.verdict === "block")
return { ok: false, error: v.why }; // loop fixes
if (v.verdict === "hold")
return queue.hold(call, v.why); // pause, hand off
if (tool.acts && !(await queue.ask(call, user)))
return { ok: false, error: "not approved" };
return checkOut(await tool.run(call.args, { key }));
}
export async function checkIn(call, user) {
const rule = RULES[call.tool];
if (!rule) return { verdict: "block", why: "no rule" };
try {
const why = await within(rule(call.args, user), 300);
return why ? { verdict: "block", why }
: { verdict: "pass" };
} catch { return { verdict: "hold", why: "check down" }; }
}
function checkOut(res) {
if (!res.ok) return res; // errors stay errors
const text = JSON.stringify(res.data ?? "").toLowerCase();
const flags = FLAGS.filter((f) => text.includes(f));
return { ...res, untrusted: true, flags }; // data
}
const RULES = {
search_docs: async () => null, // reads: still a rule
create_ticket: async (args, user) =>
args.account === user.account ? null
: "account doesn't match",
};
const FLAGS = ["ignore previous", "system prompt"];
const within = (p, ms) => Promise.race([p,
new Promise((_, no) => setTimeout(no, ms, "timeout"))]);
checkIn() returns before the tool runs.ok: false and can fix the call.Trade-offs. A stricter rule misses less and blocks more good calls. A false block stalls a task; a miss lets a bad act run. Lean strict on act tools. Each blocking check adds its time to every call: keep rules fast, and time-limit any check that calls a model.
Label cases by hand: good calls that should pass, bad ones that should stop. Per tool, count hits, misses, false blocks and the slowest time. Add each later miss as a case.
const by = {};
for (const c of cases) {
const t0 = Date.now();
const v = await checkIn(c.call, c.user);
const stop = v.verdict !== "pass";
const r = (by[c.call.tool] ??= { hit: 0, miss: 0,
falseBlock: 0, pass: 0, maxMs: 0 });
if (c.bad) stop ? r.hit++ : r.miss++;
else stop ? r.falseBlock++ : r.pass++;
r.maxMs = Math.max(r.maxMs, Date.now() - t0);
}
Agent evaluation covers graded sets and repeat runs →
Your account check times out on a create_ticket call. What should happen, and how does your eval count it?
The run holds for a person, and no ticket is filed. The eval counts a stop: a hit if the case was bad, a false block if it was good.