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 exposesvalidate_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 exposingvalidate_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.
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 raiseTypeError at Agent(...) construction, instead of silently disabling enforcement.
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 themax_guardrail_retries that already existed — no new setting. After retries exhaust, the reason is left on agent.last_guardrail_error and chat() returns None.
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-codedstream=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.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.small_model when it is configured — the agent’s primary model still handles user-facing work.
- An explicit
llm_instanceon the guardrail (e.g. a fully configured LLM object withapi_key/base_url) always wins — never rerouted. - Otherwise, a bare primary model-name string is passed through
get_small_model(primary_model=<primary>, fallback=<primary>). - When
small_modelis unset, the primary model is used — behaviour is byte-identical to earlier releases.
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 neitherPASS 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.
Guardrail validation unclear: after upgrading to spot outputs that the older permissive behaviour would have let through.
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 byclone_for_channel() — and the clone now carries the guardrail you configured.
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:
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:
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.
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
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.
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 aPolicyEngine via the policy parameter — not guardrails=.
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.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
Write specific, measurable criteria
Write specific, measurable criteria
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”.
Use function validators for structured data
Use function validators for structured data
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.
Return helpful error messages on failure
Return helpful error messages on failure
The
(False, "reason") message is passed back to the agent as feedback. Make it actionable — tell the agent exactly what to fix.Set max_retries conservatively
Set max_retries conservatively
Start with
max_retries=2. Increasing retries adds latency and cost. If the agent fails repeatedly, the validator criteria or instructions may need refinement.Related
Policy Engine
Allow/deny tool calls with
Agent(policy=...) — replaces policy stringsApproval
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() tooGateway Self-Lifecycle Guard
Block agent commands that would stop or restart this gateway
Configure
small_model to route guardrail validation to a cheap modelWhy streaming bypasses guardrails, and how to opt in to validation

