Skip to main content
Guardrails validate agent output against your criteria and automatically retry if the output fails. The user sends a prompt; guardrails validate input and output before the agent responds or calls tools.
Since PraisonAI PR #3790, policy-string guardrails are no longer accepted. Passing guardrails=["policy:strict", "pii:redact"] (or a GuardrailConfig(policy=…) / GuardrailConfig(policies=[…]) with a non-empty value) raises at Agent(...) construction time:
For tool allow/deny enforcement, use the dedicated policy parameter with a Policy Engine instead. guardrails= is for output validation only.
AgentTeam(guardrails=…) is not wired yet (PraisonAI #4004) — set guardrails=… on each Agent(...) in the team instead.

Quick Start

1

Simple Usage

Pass a validation function to an agent:
Since PraisonAI PR #3944, callable guardrails reliably return the agent’s answer. On releases before commit edab0de (2026-08-15), a duplicate TaskOutput class made the guardrail path silently coerce successful calls into failures and agent.start(...) returned None after the configured max_retries. If you saw None from a callable-guardrail agent on an earlier build, upgrade — no code change is required.
2

With Configuration

Use GuardrailConfig for LLM-based validation with retry settings:

Input-side validation

Any guardrail that exposes validate_input(content, **kwargs) -> (bool, str) is called before the LLM dispatch on both chat() and achat(). When it returns (False, …) the call short-circuits and returns None — no LLM cost, no tool dispatch — while plain callable and plain string guardrails stay output-only (string guardrails are marked output-only to avoid an extra synchronous LLM call per turn).
Input-side validation runs on both chat() / start() and achat() / astart(), and it fails closed — an exception inside validate_input blocks the prompt.
Since PraisonAI PR #3908, input-side validation is wired into both the sync and async agent paths. Earlier releases defined validate_input but never called it, so a guardrail meant to block a prompt silently let it through.

Class-based Guardrails (Object Protocol)

Any Python object exposing validate_input(content, **kwargs), validate_output(content, **kwargs), or validate_tool_call(tool_name, arguments, **kwargs) is accepted directly by Agent(guardrails=...). Pass a single instance, or a list of instances, and the SDK wraps them in a GuardrailChain for you.
A list of instances is chained in order:
Since PraisonAI PR #4122 (2026-08-20), class-based guardrails are wrapped into a GuardrailChain automatically. On earlier releases a bare instance (or list of instances) fell through every dispatch branch and the agent was constructed with no guardrail — no exception, no warning. If you tried the pattern before and saw content sail through unfiltered, upgrade past commit c904372 and no code change is needed.

Fail-loud on unsupported guardrail values

Values that cannot be turned into any enforceable validator raise TypeError at Agent(...) construction, instead of silently disabling enforcement.
The exception message names the value and the supported shapes so you can fix the call without hunting:
Behavioural change (PraisonAI #4122). Before this PR, unsupported guardrail values were silently discarded and the agent ran with zero enforcement — the caller believed a validator was active while none was wired. The TypeError above is now raised at construction time to surface the mistake immediately. Same “safe by default” rationale as the existing policy-string ValueError.

Which Validator Should I Use?

Pick a validator strategy based on how you need to check the output.

How It Works

Guardrails work identically in sync (.start(), .chat()) and async (.astart(), .achat()) execution paths.

Validator Model

LLM-based guardrails run on the auxiliary small_model when it is configured — the agent’s primary model still handles user-facing work.
Precedence (highest → lowest):
  1. An explicit llm_instance on the guardrail (e.g. a fully configured LLM object with api_key/base_url) always wins — never rerouted.
  2. Otherwise, a bare primary model-name string is passed through get_small_model(primary_model=<primary>, fallback=<primary>).
  3. When small_model is unset, the primary model is used — behaviour is byte-identical to earlier releases.
Applies to both Agent(guardrails=...) and Task(guardrail=...) when the guardrail is a natural-language string. Callable validators are not affected — they run in-process without an LLM. See Configuration File → Cheap auxiliary model for internal calls for the full resolver order.
Since PraisonAI PR #3632, the guardrail’s in-flow validation path (validate_input / validate_output / validate_tool_call) recognises the SDK’s LLM.get_response(prompt=..., verbose=False, markdown=False, stream=False) interface directly. Guardrails configured as a string model name or a bare LLM(model=...) instance now validate through the documented protocol methods. If you were carrying a workaround that wrapped the LLM to expose complete / invoke / __call__, you no longer need it.

Fail-closed guarantees

An LLM guardrail with no configured LLM — or an LLM of an unsupported type — blocks the output rather than silently allowing it. The guardrail is a security gate; a gate that can’t run must not open.

Ambiguous validator replies also fail closed

When the validator LLM returns a reply that is neither PASS nor FAIL: <reason> — markdown wrappers, refusals, or a reasoning preamble — the guardrail blocks the output with the reason "Guardrail validation unclear: <reply>" instead of silently passing it through. Both entry points (__call__ and _llm_validate) apply this rule.
Grep your logs for Guardrail validation unclear: after upgrading to spot outputs that the older permissive behaviour would have let through.
Fail-closed guarantee (PraisonAI PR #3545, #3574, and #3632). A missing or unsupported LLM (#3545) and an ambiguous validator reply (#3574) all still block output on an LLMGuardrail. Since #3632, the SDK’s own LLM.get_response interface is recognised by the in-flow protocol methods — string / LLM-instance guardrails now validate through the documented path instead of silently failing closed.
A GuardrailChain built only from LLM guardrails inherits the same fail-closed behaviour. A missing LLM in any member of the chain blocks the output — not just when a bare LLMGuardrail runs on its own. Since PraisonAI PR #3877 a chain is a valid Agent(guardrails=...) value; earlier releases silently dropped it to None and validated nothing.

Streaming bypass

Token-level streaming (iter_stream() / start(stream=True)) yields tokens as the model produces them — there is no full response to validate until streaming completes. Output guardrails do not apply to streamed responses, and the SDK logs a warning as soon as streaming begins on an agent with a guardrail attached:
Both entry points log this warning — iter_stream() and start(stream=True) share the same generator internally, so the bypass is loud on both surfaces. Use chat() (or the non-streaming path of start() / astart()) when you need guardrail-validated output. See Streaming → Streaming and guardrails for the trade-off.

Tool-Call Validation (validate_tool_call)

When a guardrail object exposes validate_tool_call(tool_name, arguments, **kwargs) -> (bool, dict), it is consulted before every tool call. A return of (False, ...) blocks the tool call.
Since PraisonAI PR #4122 (2026-08-20), validate_tool_call is wired for guardrail objects and GuardrailChains. On earlier releases the check-site read an attribute that was never assigned, so validate_tool_call was unreachable dead code and every tool call went through. String and LLM-string guardrails (guardrails="Be polite") are excluded from this wiring on purpose — running an extra LLM call before every tool call would be a hot-path regression.

Configuration Options

GuardrailConfig SDK Reference

Full parameter reference for GuardrailConfig
Precedence ladder — choose the level you need:
The policies field still exists on GuardrailConfig, but passing a non-empty value raises ValueError since PraisonAI PR #3790 — policy strings are not enforced through guardrails=. For tool allow/deny enforcement, use Agent(policy=PolicyEngine(...)) — see Policy Engine.
Since PraisonAI PR #4020, [defaults.guardrails] in .praisonai/config.toml is honoured — an Agent(...) with no explicit guardrails= now picks up the config-wide safety net. Earlier releases silently ignored it. See Guardrail safety net for the whole project.

Composing Guardrails with GuardrailChain

GuardrailChain composes several guardrails into one and is directly usable as Agent(guardrails=chain) — it takes one positional argument and returns a (bool, task_output) tuple, matching the Agent guardrail signature.
The chain short-circuits — it stops at the first guardrail that fails. On success the original output passes through unchanged; on failure the error message is returned. Each guardrail can expose three validation entry points, all called through the chain: Any object with matching method names satisfies GuardrailProtocol — it is duck-typed, so a guardrail only needs the methods it actually uses. Since PraisonAI PR #4122, you can also pass such an object directly to Agent(guardrails=...) without constructing a GuardrailChain yourself. See Class-based Guardrails above.
Policy-string guardrails (guardrails=["policy:strict"]) are enforced on the agent path — a policy that denies still blocks the run.

Common Patterns

Tool Policy Enforcement (replaces policy strings)

To allow or deny tool calls, attach a PolicyEngine via the policy parameter — not guardrails=.
See Policy Engine for the full rule syntax.

Function-Based Validation

This callable pattern also silently returned None before PraisonAI PR #3944. Upgrade past commit edab0de (2026-08-15) and it returns the agent’s answer — no code change is required.

Natural Language Validation

LLM guardrail validation uses your configured small_model when the guardrail is a natural-language string (no explicit LLM instance passed). See Auxiliary / Small Model Resolution.

Chaining Guardrails

Chain multiple guardrails so an output must pass every check before being accepted.
The chain is fail-closed: if any guardrail rejects, the Agent retries with the failure reason — the same on_fail behaviour as a single guardrail. On success the chain passes your original TaskOutput object straight through, so downstream steps keep the structured result rather than a coerced string.

Multi-Agent with Guardrails


Best Practices

Vague guardrails like “be good” are hard to enforce. Use concrete criteria: “must be between 100 and 200 words” or “must contain a JSON array”.
When validating JSON, code, or data formats, use a function validator. LLM validators are slower and better suited for qualitative criteria like tone or completeness.
The (False, "reason") message is passed back to the agent as feedback. Make it actionable — tell the agent exactly what to fix.
Start with max_retries=2. Increasing retries adds latency and cost. If the agent fails repeatedly, the validator criteria or instructions may need refinement.

Guardrails validate an agent’s output and can retry. To block an action before it happens (a tool call, an inbound message, or an LLM request), see Blocking Plugins in Plugins.

Policy Engine

Allow/deny tool calls with Agent(policy=...) — replaces policy strings

Approval

Add human-in-the-loop approval steps

Hooks

Intercept and modify agent behavior at lifecycle points

BEFORE_LLM on async

BEFORE_LLM / AFTER_LLM now fire on achat() too

Gateway Self-Lifecycle Guard

Block agent commands that would stop or restart this gateway
Configure small_model to route guardrail validation to a cheap model
Why streaming bypasses guardrails, and how to opt in to validation