Skip to content
Back to Blog

AI Agent Guardrails: Gate Risky Tool Calls with Yes/No Probabilities

See sample results

How do yes/no probabilities fit in? Start with deterministic controls: allowlists, permission checks, regex for well-defined patterns. For the fuzzy part of a policy, split it into yes/no questions such as "does this target a specific group?" or "will this tool call modify production data?", and have a decision model return P(true) for each. Add a Score question if you need severity levels. Code applies two thresholds: above the upper one, block; in between, send to a person; below the lower one, allow. If the check errors, fail closed. A probability is not authorization: high-risk actions still need permission checks and human approval.

Background on Choice, Score and Noul (yes/no probability): What is a decision model?

Try it online (sign-in required): Will this tweet get moderated?

Three checkpoints: input, output, tool

The OpenAI Agents SDK (JS) docs define three kinds of guardrail:

  • Input guardrails run on the initial user input, only for the first agent that receives it.
  • Output guardrails run on the agent's final output, only on the agent that produces it.
  • Tool guardrails run before and after each custom function tool call. A tool input guardrail can return allow (continue), rejectContent (skip the call with a message) or throwException (trip a circuit-breaker error).

The docs describe two execution modes. Input guardrails run in parallel with the agent by default (runInParallel: true) for the lowest latency, but the model may already have spent tokens or run tools when the guardrail trips. Set it to false to finish the guardrail before calling the model. The docs also suggest running a lightweight model as the guardrail before calling a more expensive one.

Alibaba Cloud's AgentScope integration guide gives another set of hooks: pre_reply checks user input before the agent replies, post_reasoning checks final output after reasoning, and toolkit middleware checks "tool name + arguments" before execution and blocks violations.

Guardrails and human approval are different things. In the OpenAI Agents SDK, approval uses a tool's needsApproval: the run pauses, returns interruptions, and resumes from the same RunState after approve or reject. By default, tool input guardrails run after approval and before execution; with toolExecution: { preApprovalInputGuardrails: true } they also run before the approval request goes out.

These frameworks are clear about where checkpoints go. What the guardrail function uses to decide is your call.

What makes the decision

Four common options:

  • Rules and regex: deterministic and cheap, good for well-defined patterns, such as the OpenAI docs example that blocks arguments containing sk- keys. They miss rephrased content. Use these first.
  • Another generative LLM call: the OpenAI docs' input guardrail example runs another agent that outputs a boolean field. It understands meaning, but outputs yes/no with no tunable threshold.
  • A dedicated moderation service: for example Alibaba Cloud's AI guardrails, plugged into AgentScope's hooks. The service defines categories and criteria.
  • Decision model Noul: returns P(true) for yes/no questions you write. You write questions for your own policy and set thresholds by error cost.

Combine them: rules catch well-defined cases, Noul handles policies that need semantic understanding, and high-risk actions still go through human approval.

Turn a policy into Noul questions

A vague policy like "don't post offensive content" is hard to judge directly. Split it into yes/no questions:

  • Does this content target a specific group?
  • Does this content ask for passwords, verification codes or other credentials?
  • Will this tool call modify production data?

The System One primitives docs say Noul estimates whether a specific condition holds and returns P(true) from 0 to 1. When several conditions can hold at once, define one Noul per condition; the probabilities refer to different propositions and don't need to sum to one. In the SDK, use booleanQuestion() and state what true and false mean in the criteria. Microsoft Learn's Microsoft-Decision-1 docs also recommend noul for gating and filtering, with the example question "does this reply promise the customer a refund?".

For severity levels, add a Score question with ordered levels such as "safe / borderline / violation / must remove". The System One docs say the hosted API requires 2–10 ordered levels, and the score is a probability-weighted value, not a percentage of correctness.

Gate tool calls before execution

Send the tool call as the model's input (state). The AgentScope guide formats it as Tool: {name}\nArguments: {json}, which is a reasonable template. Put only what the decision needs and is allowed to share into state; never secrets.

With the OpenAI Agents SDK, put the check in a tool input guardrail: return allow for low risk; rejectContent with an explanation to refuse the call but let the agent continue; throwException to stop the whole run. Payments, data deletion and outbound messages still get needsApproval so a person approves them.

The System One agent integration docs say a model result is not authorization: the operator's identity and permissions, action allowlists and required approvals are all checked outside the model. A missing answer or failed request is "unresolved", never an automatic yes or no. So when the guardrail call errors, treat it as not passed (fail closed), not as allow. For the allowlist approach in tool routing, see Route agent tool calls with a Choice question.

Three bands and human review

When the question asks "is this a violation?":

  • P(true) above the upper threshold → block
  • between the thresholds → human review
  • below the lower threshold → allow

If you phrase it as "is this safe?", flip the direction. Neither threshold should be a generic percentage. The System One docs say thresholds come from your own eval cases and the consequences of mistakes; Microsoft Learn also recommends setting them by the cost of false positives and false negatives. Where a miss is expensive (payments), lower the upper threshold so more requests reach review; where a false block is expensive, narrow the review band. For picking thresholds on labelled samples and confirming on held-out data, see An LLM-as-a-judge alternative. For a Noul threshold → action table, see AI agent stop conditions.

Microsoft Learn adds: use clear, neutral wording for questions and options; add an abstain option like "cannot tell" when a choice shouldn't be forced; consider randomizing option order and testing whether order affects results; and for consequential decisions about people (credit, employment, housing, health, legal), use the model only as decision support with meaningful human review.

Evaluate misses and false blocks separately

On your own labelled set, count two things separately: should have blocked but didn't (misses) and shouldn't have blocked but did (false blocks). They have different costs, and one combined accuracy number hides that. The System One agent integration docs likewise ask you to track wrong choices and missed reviews separately and to include inputs that try to force the model to change its choice.

Include in your test set: the same meaning phrased differently, prompt injection (for example "ignore previous rules" inside arguments), and content that sounds harsh but is harmless. Microsoft Learn's known limitations include scores shifting with question and option wording and order, and, when used as a safety filter, missing subtle harmful content or flagging harmless content. Only your own data will show how big these effects are.

TypeScript example

The dev team's examples/decision-articles/08-agent-guardrails.ts defines three booleanQuestion()s: does it target a specific group, does it ask for credentials or secrets, and will this tool call modify production data or send an outbound message. The maximum of the three probabilities is the risk value, so any single rule can trigger the guardrail. Risk ≥ BLOCK_IF_RISK_AT_LEAST (0.8) blocks; ≥ REVIEW_IF_RISK_AT_LEAST (0.4) goes to your human-review callback; otherwise it's allowed. Both thresholds are examples to tune on labelled cases. The comments also note that P(true) isn't a confirmed fact or authorization, and that System One has no moderation, approval or needsApproval API.

ts/**
 * Article 8 — Agent guardrails ("Agent 护栏", /zh-CN/blog/agent-guardrails).
 *
 * Split a moderation policy into independent yes/no questions via
 * `booleanQuestion()` (wire type `noul`, SDK answer `.probability`). Those
 * probabilities do not sum to 1. This file combines them with max() — any
 * rule can trip the guardrail — then maps the combined risk onto two
 * application-owned cutoffs: block / human review / allow.
 *
 * P(true) is not a confirmed fact and is not authorization. Human review is
 * an application callback. System One has no moderation, approval, or
 * `needsApproval` API. Packages: @system-one-ai/core, adapter-system-one,
 * transport-fetch. Endpoint: POST /v1/systemone.
 */
import { createSystemOne, booleanQuestion } from '@system-one-ai/core';
import type { EvaluationClient } from '@system-one-ai/core';
import { systemOneAdapter } from '@system-one-ai/adapter-system-one';
import { createFetchTransport } from '@system-one-ai/transport-fetch';

export const guardrailQuestions = {
  targetsGroup: booleanQuestion(
    'Does this content target a specific group with abuse or harassment?',
    { true: 'The text attacks a named group', false: 'The text does not target a group' },
  ),
  requestsSecrets: booleanQuestion(
    'Does this content ask for credentials, API keys, or other secrets?',
    { true: 'The user is asked to reveal a secret', false: 'No secret is requested' },
  ),
  changesProduction: booleanQuestion(
    'Would executing this tool call change production data or send an external message?',
    { true: 'The call writes, deletes, pays, or posts', false: 'The call is read-only or local' },
  ),
};

export type GuardrailRule = keyof typeof guardrailQuestions;
export type GuardrailAction = 'block' | 'review' | 'allow';

export interface GuardrailProbabilities {
  targetsGroup: number;
  requestsSecrets: number;
  changesProduction: number;
}

/** High risk: false allow is costly. Tune on labeled cases; not a hosted default. */
export const BLOCK_IF_RISK_AT_LEAST = 0.8;
/** Mid band: send to a person. Must be below the block cutoff. */
export const REVIEW_IF_RISK_AT_LEAST = 0.4;

export interface HumanReviewInput {
  text: string;
  tool?: string;
  args?: string;
  risk: number;
  probabilities: GuardrailProbabilities;
}

export function createHostedClient() {
  const apiKey = process.env.SYSTEM_ONE_API_KEY;
  const baseURL = process.env.SYSTEM_ONE_BASE_URL;
  const model = process.env.SYSTEM_ONE_MODEL;
  if (!apiKey || !baseURL || !model) {
    throw new Error('Set SYSTEM_ONE_API_KEY, SYSTEM_ONE_BASE_URL (…/v1), and SYSTEM_ONE_MODEL from GET /v1/models.');
  }
  return createSystemOne({
    apiKey, baseURL, model, adapter: systemOneAdapter,
    transport: createFetchTransport(), timeoutMs: 30_000, maxRetries: 0,
  });
}

/** Conservative combine: the highest P(true) among independent Noul rules. */
export function combineRiskProbability(probabilities: GuardrailProbabilities) {
  return Math.max(probabilities.targetsGroup, probabilities.requestsSecrets, probabilities.changesProduction);
}

export function applyGuardrailPolicy(risk: number): GuardrailAction {
  if (risk >= BLOCK_IF_RISK_AT_LEAST) return 'block';
  if (risk >= REVIEW_IF_RISK_AT_LEAST) return 'review';
  return 'allow';
}

/**
 * @param sendToHumanReview Your approval queue. Not a System One method.
 */
export async function screenWithGuardrails(
  client: EvaluationClient,
  input: { text: string; tool?: string; args?: string },
  sendToHumanReview: (input: HumanReviewInput) => Promise<string>,
) {
  const result = await client.evaluate({
    state: {
      text: input.text,
      tool: input.tool ?? null,
      args: input.args ?? null,
    },
    questions: guardrailQuestions,
  });
  const probabilities: GuardrailProbabilities = {
    targetsGroup: result.answers.targetsGroup.probability,
    requestsSecrets: result.answers.requestsSecrets.probability,
    changesProduction: result.answers.changesProduction.probability,
  };
  const risk = combineRiskProbability(probabilities);
  const action = applyGuardrailPolicy(risk);

  if (action === 'block') {
    return { action, risk, probabilities, model: result.model };
  }
  if (action === 'review') {
    const reviewed = await sendToHumanReview({
      text: input.text,
      tool: input.tool,
      args: input.args,
      risk,
      probabilities,
    });
    return { action, risk, probabilities, model: result.model, reviewed };
  }
  return { action, risk, probabilities, model: result.model };
}

async function main() {
  const text = process.env.EXAMPLE_MESSAGE ?? 'Please post this refund to the production ledger.';
  const decision = await screenWithGuardrails(
    createHostedClient(),
    { text, tool: process.env.EXAMPLE_TOOL, args: process.env.EXAMPLE_TOOL_ARGS },
    async () => {
      throw new Error('No System One human-review API. Wire your approval queue here.');
    },
  );
  console.log(JSON.stringify(decision, null, 2));
}

if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split(/[\\/]/).pop() ?? '')) {
  main().catch((error: unknown) => {
    console.error(error instanceof Error ? error.message : error);
    process.exitCode = 1;
  });
}

This example isn't wired into an OpenAI Agents SDK tool input guardrail and doesn't use Score for severity. When you integrate it, map the returned block / review / allow to your framework's rejectContent, needsApproval, allow and so on.

Hosted billing: every 4KB of request is one block, and anything under 4KB still counts as one; cost = blocks × questions × the model's price per block. Three guardrail questions means three questions' worth of blocks per check. New accounts get 100 free credits on sign-up; see pricing.

Related reading

Guardrail decision methods compared (checked 2026-10-11)

MethodOutputThreshold by error cost?Best forSource
Rules / regexSource: OpenAI Agents SDK guardrails · Verified 2026-10-11Match / no match; code returns `allow` or `rejectContent`No, match or notWell-defined patterns such as keysOfficial docs example
Generative LLM judgeSource: OpenAI Agents SDK guardrails · Verified 2026-10-11Guardrail agent outputs a structured result (a boolean in the example) that trips the circuit breakerNo probability to tune in the exampleSemantic checks; docs suggest a lightweight modelOfficial docs
Dedicated moderation serviceSource: Alibaba Cloud: AgentScope + AI guardrails · Verified 2026-10-11Blocks, flags or replaces content at the hooksYes: the console sets hit thresholds on a 0–100 confidence score (step 1) for fine-grained risk labels; the API returns RiskLevel (high / medium / low / none), and the docs suggest acting on high and reviewing medium[Alibaba Cloud text moderation API](https://www.alibabacloud.com/help/en/content-moderation/latest/guard) · 2026-10-11Source: Alibaba Cloud AI guardrails overview · Verified 2026-10-11General content safety; the service defines categoriesOfficial docs
Decision model Noul (System One)Source: System One primitives · Verified 2026-10-11P(true) per yes/no question, 0 to 1Yes, set by the app from eval cases and error consequencesYour own policy; system-one.dev offers Jev, Clef Flash and ClefOfficial docs
Decision model noul (Microsoft-Decision-1)Source: Microsoft Learn · Verified 2026-10-11Probability of a yes/no condition; recommended for gating and filteringYes, docs recommend setting by false-positive and false-negative costCalled on Microsoft Foundry; not enabled on System OneOfficial docs

Frequently asked questions

What are agent guardrails, and what do input, output and tool guardrails cover?
Checks that run automatically on user input, model output and around tool calls. Per the OpenAI Agents SDK docs: input guardrails check the initial user input, output guardrails check the final output, and tool guardrails run before and after each function tool call and can allow it, reject it or throw a circuit-breaker error.
Rules, an LLM or a decision model?
Combine them. Use deterministic allowlists and rules first for well-defined patterns. A generative LLM understands meaning but usually only gives yes/no. A decision model's Noul returns probabilities for your own questions, so you can threshold by error cost. Whatever decides, keep human approval for high-risk actions.
How do I write moderation questions with Noul, and when do I add Score?
Split each policy into specific yes/no questions, define each separately, and spell out what true and false mean in the criteria. Add a Score when you need severity levels like "safe / borderline / violation / must remove".
How do I set thresholds, and what about the middle?
Two thresholds, three bands: above the upper, block; in between, a person; below the lower, allow. Derive thresholds from your own labelled cases and error costs, not generic percentages. The more a miss costs, the wider the review band.
If the probability is high, can the action just run? Can guardrails replace permissions and approval?
No. The System One docs say a model result is not authorization: permissions, action allowlists and required approvals are checked outside the model. Payments, data deletion and outbound messages still need confirmation per your policy. If the guardrail call fails, fail closed.
How do I defend against prompt injection?
Put injection samples (such as "ignore previous rules" in arguments) into your eval set and track misses separately. Microsoft Learn warns that scores are sensitive to wording and that safety filters can miss subtle harmful content, so a guardrail can't be your only defence; keep permission controls and human approval.
Will guardrails slow my agent down?
The OpenAI Agents SDK offers two modes: run guardrails in parallel with the agent for the lowest latency (but the model or tools may already have started when it trips), or finish the guardrail before calling the model to avoid wasted tokens and accidental tool calls. Actual latency depends on your model and deployment; we haven't measured it.