Skip to main content
Back to blog

The Custom Guardrail DSL, Explained: Predicates, Trigger Syntax, and the Evaluation Matrix

A practitioner's reference for Devotel Orbit's tenant-authored guardrail rules. Every rule field, the two trigger expression forms, what block, redact, and warn each do on each evaluation leg, five worked rules you can adapt, and the line between your rules and the platform's carrier gates.

Orbit Editorial Team

When Devotel Orbit shipped the Custom Guardrail DSL, the announcement covered why it exists: the five built-in scanners (prompt-injection classification, PII detection, harmful-content filtering, blocked topics, sensitive words) cover the universal violation classes, but no shipped scanner knows your business vocabulary. The analytics guides cover the other end, how you measure whether a rule earns its place. This post is the reference in between: what a tenant-authored rule can predicate on, how trigger expressions are written and compiled, what each action verb does on each leg, and five worked rules you can adapt directly.

The rule surface, field by field

A rule is a JSON object in safety_config.custom_guardrail_rules, the same config blob that already carries blocked_topics, sensitive_words, and locale_pii_patterns. You set it on agent create or update, and up to 25 rules per agent keeps the per-turn scan bounded.

FieldRequiredValuesSemantics
directionyesinput, outputinput scans the user message before it reaches the model; output scans the model reply before it is returned or spoken.
matchyescontains, regexThe trigger expression form (below).
patternyesstring, 1 to 200 characters after trimThe literal text for contains, or the regex source for regex.
actionyesblock, redact, warnThe action verb (below).
severitynolow, medium, high, criticalDefaults to high for block and warn, low for redact. Severity, not the verb, decides blocking: any violation recorded at high or critical stops the turn.
id, namenostrings up to 64 charactersStable labels for violation entries and analytics. Without an id, the platform assigns one by position.

Trigger expression forms

contains is a case-insensitive literal substring scan. Regex metacharacters in the pattern are treated as literal text, so a contains rule is safe by construction and is the right default for vocabulary: a competitor name, a product term, a phrase your legal team flagged.

regex compiles your pattern as a JavaScript RegExp with the i flag, for shape matching: ticket numbers, SKU prefixes, member IDs. A pattern that does not compile is rejected at save time with a 422 that names the rule index and field, so a typo can never become a silently dead rule.

Both forms are text-level. Neither understands paraphrase or intent, and a phrasing that avoids your literal or your shape will not fire. That is the determinism guarantee working as designed, and it is why custom rules layer on top of the fixed scanners instead of replacing them.

Channel scope

Rules attach to an agent, not to a channel. The evaluation legs run on every turn that agent handles, on whichever lane carries the conversation: web chat, voice, WhatsApp, or SMS. There is deliberately no per-channel field in the DSL. To scope a rule to one channel, scope the agent: a rule on your SMS campaign agent is an SMS-scoped rule. If one agent serves several lanes and only one lane needs the rule, phrase the pattern around that lane's vocabulary or split the agent.

The three action verbs

  • block raises a violation and hands the decision to the same severity reducer the fixed scanners use. At high or critical the turn stops with a refusal; at low or medium the violation is recorded without stopping the turn.
  • redact replaces every match with [REDACTED] before the text moves on: before the model sees it on input, before the reply is returned or spoken on output. The scrub emits a low-severity violation so the firing is visible in the log as a count and category, never as the redacted value.
  • warn records the violation and leaves the text untouched — no refusal, no [REDACTED] swap. It is the observe-first rollout verb: measure the firing rate, then flip to block once you trust it. One caution: blocking follows severity, not the verb. The turn stops on any violation recorded at high or critical, whatever the action — and warn defaults to high. An observe-first rule therefore pairs warn with low or medium, so the watch counts every firing without ever stopping a turn.

The evaluation matrix

Two legs, at fixed positions in the turn pipeline:

  • Input leg: after the prompt-injection scanner, PII detection, blocked topics, and encoding checks, before the pass decision. Your contains rule sees the same cleaned-up text the fixed scanners see.
  • Output leg: after PII redaction, the content filter, and the citation check, before the pass decision. A value the PII redactor already masked will not re-fire your rule.

Text length caps apply before evaluation: 50,000 characters on input, 100,000 on output. Evaluation is deterministic: the same text and the same rules produce the same violations every time, with no model, clock, or network in the matching path, so a rule is reproducible in your own tests and identical in production.

Two boundaries worth knowing. Rules fire on agent turns; bulk broadcasts that never invoke an agent are governed by your messaging-side controls, not by an agent's safety config. And validation is two-layered: the write path rejects bad shapes with a 422, while the runtime skips a malformed rule rather than throwing, so a poisoned rule can never break a turn.

Five worked rules

1. The no-refund promise. A support agent should explain your refund policy, not invent refunds. Anchor on the promise phrasing in the model's reply:

{
  "id": "no-refund-promise",
  "name": "No unprompted refund promises",
  "direction": "output",
  "match": "regex",
  "pattern": "(we will|we'll|I will|I'll) (refund|credit) (you|your)",
  "action": "block",
  "severity": "high"
}

Ship it as warn at severity: "medium" for a week first. The verb alone is not enough: at the default high, the recorded violation would already stop turns, which is exactly what an observe-first week must not do. When the violation feed shows it firing only on genuine promise phrasings, restore high and flip to block, and the reply stops before it reaches the customer.

2. No medical advice. A wellness retailer's agent may describe products but must not prescribe doses. Dose phrasings have a reliable shape:

{
  "id": "no-dosage-advice",
  "name": "No dosage instructions",
  "direction": "output",
  "match": "regex",
  "pattern": "(take|taking) [0-9]+ ?(mg|ml|mcg|iu)",
  "action": "block",
  "severity": "critical"
}

critical is deliberate: this is a category where a recorded-but-allowed miss is worse than an occasional stopped turn.

3. Escalation to human on keyword. The DSL detects and measures; your escalation configuration acts. A warn rule on trigger phrases guarantees the keyword is counted even on turns the model handled without handing off:

{
  "id": "escalation-triggers",
  "name": "Human escalation keywords",
  "direction": "input",
  "match": "regex",
  "pattern": "\\b(human|real person|agent|supervisor|lawyer)\\b",
  "action": "warn",
  "severity": "medium"
}

The medium severity is doing quiet work here: it keeps the watch below the block threshold, so keyword turns are counted and carried on. Pair it with the agent's transfer-to-human tooling so the detected trigger also routes the conversation. The rule gives you the firing rate; the handoff config gives the customer the person they asked for.

4. Per-channel opt-out block. Short opt-out keywords need word boundaries, which is regex territory: a substring scan for "stop" would also fire on "nonstop". On your SMS agent:

{
  "id": "opt-out-keyword-sms",
  "name": "Opt-out keyword halts the agent lane",
  "direction": "input",
  "match": "regex",
  "pattern": "\\b(stop|unsubscribe|opt ?out|cancel)\\b",
  "action": "block",
  "severity": "high"
}

The blocked turn stops the agent from composing any continuation on the message that carries the keyword, and your suppression list remains the durable opt-out store on the messaging side. Because rules are per-agent, attaching this to the SMS agent is exactly the per-channel scope the rule wants.

5. PII egress guardrail. The built-in PII detector covers the standard classes and locale_pii_patterns adds regional identifiers; your own identifier formats are yours to write. Member numbers with a label prefix:

{
  "id": "member-id-egress",
  "name": "Scrub member numbers from replies",
  "direction": "output",
  "match": "regex",
  "pattern": "(member|account|reservation) (id|no\\.?)[: ]?[0-9]{6,12}",
  "action": "redact"
}

Every match becomes [REDACTED] before the reply leaves, and the low-severity violation tells you how often the scrub fired. Layer this with the platform's PII egress controls rather than treating either as sufficient alone.

Tenant-authored versus platform-shipped

Two layers, two owners. Your rules live in safety_config, editable by the roles that edit agent config (owner, admin, developer), versioned with the agent, and recorded in the audit log on every change, including the warn to block flip. They are off until you author them.

The platform ships gates you do not configure and cannot waive: sender and campaign clearance (KYC verification, campaign registration, sender-ID registration) that decide whether a message may enter a carrier at all; the five fixed scanners; and carrier-side filters applied on the SMS lane independently of Orbit. These apply regardless of your settings and, for the carrier gates, before any agent runs.

The test for which layer a control belongs in is simple: if the right policy differs per tenant, it is tenant-authored and lives in the DSL; if it differs per carrier or per regulation, it is platform-shipped. The DSL claims only the first layer, which is what makes it safe to hand you full authoring control.

Measuring whether a rule earns its keep

Every rule above starts observable. The per-turn debug view shows each violation entry against the turn, the audit log records every edit to your rule set, and the firing rate per policy is the number that tells you when to flip a verb. For the full loop, allowed-versus-blocked outcomes, tuning one rule at a time, and reading effectiveness across every guardrail check, the companion posts on per-policy guardrail analytics and guardrail effectiveness monitoring cover the measurement side in depth.

A workable rollout order: author the rule as warn at low or medium severity, watch a day of firings, fix the pattern against the false positives, raise the severity and flip the verb, and confirm the blocked or redacted turns in the debug view. Fifteen minutes of configuration, observable before it ever blocks a turn.

Further reading

The Custom Guardrail DSL, Explained: Predicates, Trigger Syntax, and the Evaluation Matrix — Orbit by Devotel