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.
Streaming is included since PraisonAI PR #4462, confirmed and re-shipped in #4470 (fixes #4446). iter_stream() and start(stream=True) now run _validate_input_with_guardrail inside _start_stream_impl before the first token is yielded, matching the behaviour of chat() / achat(). A blocked prompt yields the single chunk [Input blocked by guardrail: <reason>] and stops — the durable-run record is never opened. The output-side warning (output guardrails do not apply to streamed responses) is unchanged.
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), validate_tool_call(tool_name, arguments, **kwargs), or validate_tool_result(tool_name, result, **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.

Retry preserves the conversation

When a guardrail rejects an answer, the retry now continues the same conversation instead of restarting it with one fresh message. Bounded by the max_guardrail_retries that already existed — no new setting. After retries exhaust, the reason is left on agent.last_guardrail_error and chat() returns None.
Behaviour before PraisonAI #4929. The retry already ran, but it built one fresh user message — prompt + "Note: Previous response failed validation due to: …" — and sent that alone. The system prompt, retrieved context, tool results, and the rejected answer never reached the retry, so the model was asked to fix an answer it could no longer see. The fix appends the rejected assistant turn plus the reason as a user turn and re-calls with full history. No config change — retries simply continue the conversation now. The test invariant second[:len(first)] == first proves the conversation is continued, not restarted.

Reading the rejection reason

agent.last_guardrail_error (Optional[str]) holds the reason from the final rejected attempt after retries exhaust. chat() still returns None on exhaustion; read last_guardrail_error to find out why.

Sync agents no longer stream during retry

Before #4929, the retry hard-coded stream=True regardless of the agent’s configuration, so a sync agent logged an ERROR and fell back on every retry. That is fixed — a sync agent retries on the sync path, no code change required.
When you compose guardrails with a GuardrailChain, a rejection now surfaces the underlying guardrail’s reason directly, instead of burying it under a "Guardrail error: …" prefix — so last_guardrail_error and the retry feedback carry the message your validator returned.

Guarding a single tool

Agent(guardrails=…) fires for every tool call. To validate, rewrite, or block one tool’s arguments or result, declare input_guardrails= / output_guardrails= on the tool itself — built on the same GuardrailResult / GuardrailChain machinery.

Per-Tool Guardrails

Guard one tool’s arguments and results without hand-wrapping the function

Since MervinPraison/PraisonAI#4469, string / LLMGuardrail output guardrails and multi-agent task guardrails on the async path (.astart(), .achat(), arun_task) offload the blocking validator LLM call to a worker thread via loop.run_in_executor(...). Concurrent tasks under asyncio.gather(...) no longer stall on one task’s guardrail validation. Contextvars (trace emission, session context) are preserved across the offload via copy_context_to_callable, so custom guardrails see the same contextual state as on the sync path.
Two agents run concurrently with astart() — the second no longer stalls while the first’s string guardrail validates:

Validator Model

LLM-based guardrails inherit your configured LLM. When guardrail is a string, PraisonAI resolves the underlying LLM by preferring agent.llm_instance over agent.llm. The guardrail LLM therefore inherits the api_key, base_url, and any custom client you passed to the agent — previously the string form silently discarded these overrides because agent.llm is always a bare model-name string.
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.

Guardrails on served agents (bot gateway, invoke API)

When you serve an agent through the bot gateway or the invoke API, each channel and each HTTP call runs on a per-channel clone built by clone_for_channel() — and the clone now carries the guardrail you configured.
Since PraisonAI PR #4285 (closes #4284), Agent.clone_for_channel() forwards the configured guardrail to every per-channel clone. On earlier releases the clone was constructed with no output guardrail — every Slack/Telegram/webhook channel served through the bot gateway, and every HTTP call served through the invoke API, ran unguarded. Operators who set a PII/secret/profanity guardrail and then served the agent got no guardrail on exactly the traffic that came from untrusted users. Upgrade to a release that includes #4285 — no code change is required. See Agent Cloning → Guardrails and approval travel with each clone.

Streaming and guardrails

Token-level streaming (iter_stream() / start(stream=True)) yields tokens as the model produces them. Input guardrails still run — the full prompt is known before the first token, so _validate_input_with_guardrail gates the stream the same way it gates chat(). Output guardrails do not apply — there is no full response to validate until streaming completes, so the SDK logs a warning when an output guardrail is attached to a streaming agent:
Both entry points behave the same — iter_stream() and start(stream=True) share the same generator internally, so input is gated and the output-bypass warning is loud on both surfaces. A blocked prompt stops the stream after a single chunk, before any token is generated:
Use chat() (or the non-streaming path of start() / astart()) when you also 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.

Tool-Result Validation (validate_tool_result)

When a guardrail object exposes validate_tool_result(tool_name, result, **kwargs) -> (bool, Any), it is consulted on the tool’s raw output before it re-enters the LLM context. A return of (False, ...) rejects the result (fail-closed); a return of (True, rewritten) replaces it with the rewritten value. validate_output only ever sees the model’s paraphrase of a tool result — a leaked secret or a prompt-injection payload in the raw output was previously invisible to guardrails. validate_tool_result closes that gap.
Redact in-place instead of rejecting:
Behaviour (fail-closed, mirrors validate_tool_call): Async and MCP parity. The check runs on both chat() / execute_tool and achat() / execute_tool_async, including the async MCP branch — a successful MCP tool result is gated the same way a native tool result is. Framework-issued denial markers pass through untouched. A result already tagged guardrail_denied, policy_denied, approval_denied, permission_denied, or approval_error is never re-inspected. But an ordinary tool-authored {"error": ..., "content": "..."} dict (a crawl result with a partial-failure error and content, or a shell tool’s recoverable failure) is still validated — untrusted content in an error dict can still carry a secret or an injection payload.
Since PraisonAI PR #4859 (merged 2026-09-05, fixes #4855), validate_tool_result is a first-class guardrail surface. On earlier releases there was no protocol method to write for tool-output validation — a leaked secret or prompt-injection payload in a raw tool result flowed straight into the LLM context. String and LLM-string guardrails (guardrails="Be polite") are excluded from this wiring on purpose — running an extra LLM call after every tool result would be a hot-path regression, mirroring the tool-call exclusion.

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 four 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