Guardrail node
Judge a value against a policy you write in plain language, then route to an Allowed or Blocked branch
Purpose
The Guardrail node checks one value against a policy you write in plain language, then routes the workflow down one of exactly two branches: Allowed or Blocked.
Use it where a Condition node cannot help. A Condition node compares values with operators, so it answers questions such as "is confidence above 0.8". A Guardrail node asks a model to judge, so it answers questions such as "does this message contain a credential", "is this request about a competitor", or "does this draft follow our tone of voice".
Typical placements:
- Right after the Start node, to screen an incoming request before any work is done
- Just before a Respond node, to check what the workflow is about to send back
- Around a tool call, so a model cannot be talked into using a tool for something it should not
How it differs from a Condition node
| Condition node | Guardrail node | |
|---|---|---|
| Decides by | Operators on values | A model judging against your policy |
| Branches | As many as you define | Exactly two, fixed: Allowed and Blocked |
| Branch names | Yours | Fixed labels |
| Model | Only for the LLM condition type | Always |
| Blocked data | Flows on | Withheld, see What a block does to the data |
The two-branch shape is fixed, unlike a Condition node whose branch list you define. That is because the node has exactly two handles on the canvas, and those handles are what the branches are. You cannot add a third.
Configuration
The rules the guardrail enforces, written in plain language. This is the whole instruction the model judges against, so be specific about what fails rather than what passes.
Reject the request if it contains an API key, a password, a private key,
or any other credential. Ordinary mentions of security topics are fine.Write the exception cases in as well. A policy that says only "reject credentials" tends to block any message that mentions the word password.
A template expression selecting the one value to judge. Two forms work:
- A bare path, which is what the variable picker writes:
input.message - One or more placeholders inside text:
Subject: {{input.subject}}. Body: {{input.body}}
The model sees only this text, so include everything the policy needs to judge and nothing else.
Two branches, labelled allowed and blocked. Both need a target.
Requiring a target on the Blocked branch is deliberate. It makes what a block does a wiring decision you can see on the canvas, rather than a hidden setting. To stop the run on a block, wire Blocked to a failing End node. To answer politely instead, wire it to a Respond node.
The refusal text a Blocked branch can render. It is published as _guardrail.message, and it is capped at 1,000 characters.
Leave it empty and MagOneAI publishes a generic default:
Can't perform this action as it's against the policies or guidelines.This is the only explanation a block publishes. See What a block does to the data for why the model's own reason is not returned.
The per-evaluation timeout in seconds. Default 10, minimum 1, maximum 60.
A timeout is never treated as a violation. A guardrail that ran out of time determined nothing, so the activity fails rather than taking the Blocked branch.
Max attempts is the total number of attempts including the first. Default 1, maximum 10. Only transient errors are retried.
Retry on errors selects which error categories are eligible. Leave it empty to retry every transient error. See Retry categories.
The model comes from the Start node
A Guardrail node carries no model selector of its own. It inherits the workflow default model, which is the one you set on the Start node in the canvas.
That keeps a policy check on the same model as the rest of the workflow by default. If you need a guardrail on a specific model, for example a self-hosted one so sensitive text never leaves your perimeter, change the workflow default and pin the nodes that need a different model instead. See Model configuration.
Three outcomes, not two
The node has two branches but three possible outcomes. Keeping them apart is the main thing to understand about this node.
Allowed
The verdict passed. The workflow takes the Allowed target, and the data that arrived at the node passes through to the next node unchanged.
The node also publishes a verdict object:
{
"_guardrail": {
"passed": true,
"reason": null,
"checked": "the value that was judged"
}
}Blocked
The verdict failed. The workflow takes the Blocked target. This is not an error, so it is never retried, and the run continues down the branch you wired.
The output carries only the verdict:
{
"_guardrail": {
"passed": false,
"message": "your custom message, or the generic default"
}
}Execution failure
The evaluation could not produce a verdict at all: a timeout, a transport error, an unreadable reply from the model, or no resolvable model configuration.
Neither branch is taken. The activity fails and the workflow's own retry behaviour decides what happens next. A failure is not a block, because nothing was determined.
What a block does to the data
On a block, the node drops the upstream data. It also drops two things it knows:
checked, the offending value itselfreason, the model's explanation of why the value failed
Both are written to the activity log, so a block is fully auditable. Neither is put in the output.
This is not caution for its own sake. The End node lifts the final activity output into the execution output verbatim, and the model's reason quotes the offending value back. A guardrail that blocked an API key and then returned "the text contains the key sk-abc123" would publish the very credential it just caught.
Withholding is the node's whole purpose. Labelling a violation while still returning it is not a guardrail.
If the Blocked branch needs to say something useful, put it in Custom message, which is published safely as _guardrail.message.
Retry categories
MagOneAI's LLM gateway rewrites every provider error into a sanitised sentence before an activity sees it, because provider error bodies sometimes echo fragments of API keys. By the time an error reaches this node, the status code and the provider's original wording are gone.
That is why you pick a category rather than typing a pattern. A pattern such as 429|timeout would match nothing, and you would silently get no retries at all.
| Category | Label in the UI | Covers |
|---|---|---|
rate_limit | Rate limit (429) | The provider throttled the request. Usually clears on retry. |
provider_unavailable | Provider unavailable (5xx) | An upstream outage or overload. |
timeout | Timeout | The request or the guardrail evaluation ran out of time. |
connection | Connection error | A network failure reaching the provider. |
unreadable_verdict | Unreadable verdict | The model replied with something that was not a usable pass or fail. |
A sensible starting point is the four transient categories: rate_limit, provider_unavailable, timeout and connection.
An exhausted provider account is not a rate limit. Running out of credit also arrives as a 429, but retrying cannot fix it, so it is deliberately outside the rate_limit category.
unreadable_verdict is usually a model or prompt problem, not a blip. If you see it often, the model is too small for the policy or the policy is ambiguous. Retrying hides the cause.
An unknown category is refused when you save the node, rather than ignored at run time, where a typo would look exactly like "never retries".
Examples
Example 1: Screen an incoming request for credentials
{
"id": "credential-check",
"type": "guardrail",
"config": {
"policy": "Reject the request if it contains an API key, password, private key, or access token. Discussing security topics in general is acceptable.",
"input_to_validate": "{{input.message}}",
"branches": [
{ "label": "allowed", "goto": "handle-request" },
{ "label": "blocked", "goto": "refusal-response" }
],
"custom_message": "Please remove any credentials from your message and try again.",
"timeout_seconds": 10,
"max_attempts": 3,
"retry_on_errors": ["rate_limit", "timeout", "connection"]
}
}Example 2: Check a draft before it is sent
Place the guardrail between the agent that writes the reply and the Respond node that sends it.
{
"id": "tone-check",
"type": "guardrail",
"config": {
"policy": "Reject the draft if it promises a refund, a discount, a delivery date, or any other commitment on the company's behalf. Apologies and explanations are acceptable.",
"input_to_validate": "draft-reply.text",
"branches": [
{ "label": "allowed", "goto": "send-reply" },
{ "label": "blocked", "goto": "escalate-to-human" }
]
}
}Wiring Blocked to a Human task node rather than a refusal turns the guardrail into a review trigger. See Human task node.
Example 3: Stop the run on a block
To end the execution instead of answering, wire the Blocked branch to an End node configured to fail. The block is still visible in the execution timeline, and _guardrail.message is still published.
Best practices
A one-line policy over-blocks. State what is acceptable as well as what is not, and the false-positive rate drops sharply.
A policy that mixes credentials, tone and scope gives you one bit of information when it fails. Three guardrails in sequence tell you which rule was broken, and each can route somewhere different.
Blocked requires a target, so use it. A Respond node that explains the refusal, a Human task node for review, or a failing End node are all better than routing straight back into the happy path.
The node sends the selected value to a model on every run, so judging a whole document costs real tokens and real latency. Select the field that matters.
If the rule is "amount must be under 10,000", use a Condition node. It is free, instant and exact. Save the guardrail for judgements that need language understanding.
Write one test case that should pass and one that should fail, and confirm each lands where you expect. See Testing workflows.
Troubleshooting
Cause: The node was saved without one of the two required fields, or a workflow was imported with them missing.
Fix: Fill both fields. The node re-checks them at run time, so it will not run with either one empty.
Cause: The Blocked handle is not connected to anything.
Fix: Connect it. To stop the run on a block, connect it to a failing End node.
Cause: The model did not answer inside the timeout.
Fix: Raise the timeout, up to 60 seconds, or reduce the size of the value being judged. Remember that the activity fails here. It does not block.
Causes to check: the policy states only what is forbidden with no exceptions, the selected value includes far more text than the policy is about, or the workflow default model is too small to follow a nuanced policy.
Cause: You are reading the data on the Blocked branch. A block deliberately drops upstream data and emits only _guardrail.
Fix: Read the data on the Allowed branch, where it passes through unchanged.
Cause: The node has no Custom message, so the generic default is published.
Fix: Set a custom message. The model's own reason is never returned, by design.