Guardrails
A guardrail is a rule that inspects a message before your agent sees it, or a reply before the user does, and blocks it, masks it, or holds it for a person. You do not write guardrails in code. An operator configures them in Studio and the runtime enforces them at the border, so the same rule covers a deep agent and a workflow without either one importing anything.
Where guardrails run
The runtime checks two borders, and only these two:
- A chat turn: the runtime judges the user's message before it reaches your agent, and the agent's final reply before it reaches the user.
- A workflow run: a run started by a trigger, a schedule, or by hand carries no user message, so the runtime judges its input payload. It inspects every string inside the payload, however deeply nested.
Guardrails do not fire on internal hops. A workflow that calls a deep agent does
not re-run that agent's guardrails, because the content already crossed the
border the user came in through. Tool calls are not a guardrail boundary either:
your tool's own Zod schema still validates what the model passes it.
Configure them in Studio
Open the Guardrails screen in Studio. A guardrail is six fields:
| Field | What it means |
|---|---|
name |
What the operator calls it. A block reason names this, so make it readable. |
checkType |
Which catalog check runs. See the table below. |
target |
input (the incoming message) or output (the agent's reply). |
action |
block, redact, or require_approval. |
params |
The check's own settings. The form is generated from the check's schema. |
failOpen |
What to do when the check itself cannot run. Off by default, which fails closed. |
Studio renders the form from the same catalog the runtime enforces, so a rule
that saves is a rule that runs. It rejects three combinations at save time
rather than saving a rule that does nothing: an output rule on a check that can only judge an
incoming message, a redact action on a check that can only say yes or no, and
a require_approval action on the agent's reply. A reply that already exists
cannot be parked for review.
The catalog
Six checks ship today. The first three are deterministic and cost nothing per turn. The last three ask a model, so they add latency.
| Check | Guards | What it needs |
|---|---|---|
denylist |
input, output | A list of words, or regular expressions in pattern mode. |
length_cap |
input | A maximum number of characters. |
pii |
input, output | One row per kind of personal data, and how to handle each. |
prompt_injection |
input | Nothing. Optional guidance about your own agent. |
denied_topics |
input, output | Subjects written in plain language. |
content_moderation |
input, output | The categories of harmful content you want gated. |
A few details that decide whether a rule is useful:
denylistmatches whole words:passworddoes not fire onpasswordless. Switch to pattern mode when you want a regular expression.length_capcounts characters the way a reader does: an emoji costs one.piifindscredit_card,iban,national_id,credential,email,phone,ip_addressandurl. Add only the kinds you need. Every extra kind is another chance of masking something legitimate.urlis never on by default, because masking web addresses breaks any agent that browses or cites sources.denied_topicstakes prose, not words to match: put "competitor pricing" off limits and a question about your own pricing still goes through.content_moderationgates only what you pick. The categories arehate,harassment,violence,sexual,self_harm,illegalandprofanity.
denied_topics and content_moderation judge the whole list in one model
call. Six categories cost exactly what one costs, so switch on every category
you want gated.
What an action does
| Action | On a chat turn | On a workflow run |
|---|---|---|
block |
The turn ends. The message never reaches the agent. | The run never starts, and does not retry. |
redact |
The matched values are masked and the conversation carries on. | Each masked value goes back where it came from. |
require_approval |
The turn is held. A person decides it in the approval inbox. | Restated as a refusal (see below). |
redact works only on a check that can transform content, which today
means pii. Each kind you list gets a strategy: placeholder replaces the value
entirely, partial keeps the last few characters readable, and hash turns it
into a stable unreadable token.
require_approval reuses the same inbox as
requestApproval(). Approving the turn runs it. Rejecting
it ends the turn, naming the rule. A workflow run has nobody waiting and no
thread to replay, so the runtime restates that action as a plain refusal rather
than promising a review it never queued.
Redaction preserves the shape of a payload: same keys, same array lengths, and non-string values untouched, so a workflow's own input schema still accepts what comes out.
Where a rule applies
A guardrail applies nowhere until you attach it. A rule you wrote but never attached is switched off, whatever the screen says about it being enabled.
Three scopes:
workspaceis the default set. It covers every agent and every workflow, including ones you add later.agentattaches to one named deep agent.workflowattaches to one named workflow.
A rule reached through both the default set and an explicit attachment still runs once. Attaching to an agent or a workflow requires its name, so an attachment can never cover nothing.
Runs the platform starts on your behalf are exempt: indexing an uploaded document, mapping a trigger payload, and scoring an eval case. The exemption follows the platform's start call, never the workflow's name, so the runtime still judges a run you start yourself on the same workflow.
The checking model
prompt_injection, denied_topics and content_moderation need a model. Pick it
once in Studio under Guardrail checking model in workspace settings, which is
the guardrailModel key your code can read through
stackbone.settings. Leave it unset and the
runtime uses the platform default (openai/gpt-4o-mini).
The call goes through the model provider the operator already configured, so there is no second credential to manage. Budget for one call per checked content: a turn with model-backed rules on both the incoming message and the reply makes two.
When a check cannot run
A model call can time out or rate-limit. By default that counts as a violation
and the runtime stops the turn, because a guard that opens in silence is worse
than a noisy one. Set failOpen on a specific rule to let the turn through instead.
Either way, the decision says which of the two happened. A notice carries
cause: 'policy' when your content broke a rule, and cause: 'infrastructure'
when the check itself died. The two need different fixes.
An unreadable answer from the checking model counts as a failure.
What you see when one fires
Every decision shows up in two places:
- On the chat stream, as an inline notice carrying the rule's name, the check type, the side of the turn, the action, and the reason. A redaction notice names the kinds of data the rule masked and never the values, because echoing the value would copy the data straight back out of the message it was removed from.
- On the run timeline, as a
guardrailstep, next to the model calls and tool calls in the same trace. See observability.
Streaming caveat
Judging a reply needs the whole reply, so a turn carrying an output guardrail
buffers the message before sending it. That turn loses token-by-token streaming.
A turn with input rules only never buffers, so keep output rules to the agents
that need them.
There is no stackbone.guardrails in the SDK
You configure guardrails; your code never calls them. There is no
stackbone.guardrails.check() on the ambient client, and your agent code does
not import anything to get the protection described here.
For the checks that belong inside your own code, use what already ships:
- For tool input, the Zod
schemayou declare on every tool validates it, so malformed arguments never reach your function. - For a human gate on one step,
requestApproval()from@stackbone/sdk/workflowpauses the run durably and races the decision against a timeout.
import { requestApproval } from '@stackbone/sdk/workflow';
const decision = await requestApproval({
// No `token`: the SDK assigns a resume key that is unique to this run.
topic: 'high-risk-action',
payload: { summary: input.summary },
title: 'Approve this action',
timeout: '24h',
fallback: 'reject',
});
if (decision.status !== 'approved') {
return { performed: false, reason: decision.status };
}What's next
- Human-in-the-loop: the inbox a
require_approvalguardrail routes into. stackbone.config: reading the settings an operator picks in the dashboard, from your agent code.- Observability: the run timeline where every guardrail decision lands.
stackbone.ai: the model client, and the provider the checking model borrows.