--- title: 'Guardrails' description: 'Safety rules an operator configures in Studio and the runtime enforces on every chat turn and workflow run.' position: 14 --- # 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: - `denylist` matches whole words: `password` does not fire on `passwordless`. Switch to pattern mode when you want a regular expression. - `length_cap` counts characters the way a reader does: an emoji costs one. - `pii` finds `credit_card`, `iban`, `national_id`, `credential`, `email`, `phone`, `ip_address` and `url`. Add only the kinds you need. Every extra kind is another chance of masking something legitimate. `url` is never on by default, because masking web addresses breaks any agent that browses or cites sources. - `denied_topics` takes prose, not words to match: put "competitor pricing" off limits and a question about your own pricing still goes through. - `content_moderation` gates only what you pick. The categories are `hate`, `harassment`, `violence`, `sexual`, `self_harm`, `illegal` and `profanity`. `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()`](/docs/sdk/humans/approval). 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: - `workspace` is the default set. It covers every agent and every workflow, including ones you add later. - `agent` attaches to one named deep agent. - `workflow` attaches 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`](/docs/sdk/platform/settings#the-keys-today). 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 `guardrail` step, next to the model calls and tool calls in the same trace. See [observability](/docs/sdk/platform/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 `schema` you declare on every tool validates it, so malformed arguments never reach your function. - For a human gate on one step, [`requestApproval()`](/docs/sdk/humans/approval) from `@stackbone/sdk/workflow` pauses the run durably and races the decision against a timeout. ```ts 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](/docs/sdk/humans/approval)**: the inbox a `require_approval` guardrail routes into. - **[`stackbone.config`](/docs/sdk/platform/config)**: reading the settings an operator picks in the dashboard, from your agent code. - **[Observability](/docs/sdk/platform/observability)**: the run timeline where every guardrail decision lands. - **[`stackbone.ai`](/docs/sdk/platform/ai)**: the model client, and the provider the checking model borrows.