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(). 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. 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.

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() from @stackbone/sdk/workflow pauses 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_approval guardrail 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.
BUILT WITH ❤️ FROM CANADA AND SPAIN