Guardrails are a pre-dispatch content policy layer. Before Rotor calls your callback URL, it runs each job payload through a configurable pipeline of checks. A job that fails a check is blocked — it never reaches your handler.
This protects you from:
- Accidentally emailing opted-out contacts
- Leaking PII in job payloads that flow into downstream systems
- Referencing competitor names in AI-generated outreach
- Sending off-brand copy that passed no human review
Guardrails apply to all queues in your workspace. There is no per-queue override.
The pipeline
Four rule types run in order, cheapest first. Each stage short-circuits on block — if a job is blocked by the DNC check, the remaining stages don't run.
| Stage | Rule type | Blocks or modifies |
|---|---|---|
| 1 | DNC — do-not-contact list | Blocks matching email or domain |
| 2 | Competitor block — domain deny-list | Blocks jobs referencing competitor domains |
| 3 | PII — personally identifiable information | Redacts or blocks based on piiMode |
| 4 | Brand tone — LLM judge (Claude Haiku) | Blocks if score falls below rubric threshold |
Running cheaper checks first (DNC and competitor are simple string lookups) means most blocked jobs exit in stage 1 or 2 without incurring LLM costs.
Viewing your current config
import { Rotor } from "@rotorsh/sdk";
const rotor = new Rotor({ apiKey: process.env.ROTOR_API_KEY! });
const config = await rotor.guardrails.get();
console.log(config); // GuardrailConfigThe GuardrailConfig object has a grc_ prefixed ID and contains the current state of all four rule types.
Enabling each rule type
DNC — do-not-contact list
Block jobs where the payload contains an email address or domain on your DNC list.
await rotor.guardrails.update({
dncEmails: ["[email protected]", "[email protected]"],
dncDomains: ["noemail.example.org"],
});Add entries incrementally without re-posting the full list:
// Append a single unsubscribe
await rotor.guardrails.appendDnc({
emails: ["[email protected]"],
});
// Append a domain block
await rotor.guardrails.appendDnc({
domains: ["competitor-customer.io"],
});Remove a single email:
await rotor.guardrails.removeDncEmail("[email protected]");Use appendDnc for real-time unsubscribes — call it from your unsubscribe webhook handler so the contact is protected immediately without rebuilding the full list.
Managing a large DNC list
For bulk imports (thousands of emails), build the full list and pass it to update:
// Build from your CRM unsubscribe export
const emails = await fetchUnsubscribedEmails(); // string[]
await rotor.guardrails.update({
dncEmails: emails,
});update replaces the entire DNC list for the fields you pass. If you pass dncEmails, the previous dncEmails value is replaced. If you only want to add entries, use appendDnc instead.
Competitor block
Block jobs that reference any of your competitor's domains anywhere in the payload.
await rotor.guardrails.update({
competitorDomains: ["rivalcrm.com", "othertool.io", "competitorapp.co"],
});Rotor scans the serialized job payload for any occurrence of these domain strings. A job mentioning rivalcrm.com in a message body, subject line, or URL field is blocked.
PII detection
PII detection scans the payload for patterns like email addresses, phone numbers, SSNs, and credit card numbers. You choose the response mode.
Redact mode
Detected PII is replaced with a placeholder before the job is dispatched. The job runs with the redacted payload.
await rotor.guardrails.update({
piiRedactionEnabled: true,
piiMode: "redact",
});A phone number like 555-867-5309 becomes [REDACTED-PHONE] in the payload your handler receives.
Block mode
The job is blocked entirely if PII is detected. Use this when your handler should never receive raw PII.
await rotor.guardrails.update({
piiRedactionEnabled: true,
piiMode: "block",
});| Mode | Behavior | Use when |
|---|---|---|
redact | Strips PII, job runs with sanitized payload | Handler is safe to receive data but PII shouldn't flow through |
block | Stops the job entirely | Handler must never see raw PII |
Brand tone judge
The brand tone judge uses Claude Haiku to score the job payload against a rubric you define. If the score falls below the threshold, the job is blocked.
await rotor.guardrails.update({
brandToneJudgeEnabled: true,
brandToneRubric:
"Professional and helpful. Do not use urgency language, scarcity claims, or manipulative CTAs. " +
"Avoid phrases like 'Act now', 'Limited time', 'Don't miss out'. " +
"Tone should feel like a knowledgeable peer, not a sales rep.",
});Brand tone is stage 4 — it runs last. It only fires if the job passed DNC, competitor, and PII checks. This means you're only paying for LLM inference on payloads that are otherwise clean.
Writing an effective rubric
The rubric is a plain-text description of acceptable tone. Be specific:
- Describe what good looks like ("conversational, concise, direct")
- Explicitly call out what to avoid ("no urgency language", "no superlatives like 'best' or 'revolutionary'")
- Include examples of acceptable and unacceptable phrases if needed
Vague rubrics produce inconsistent results. Specific rubrics produce reliable blocks.
When a job is blocked
When any guardrail stage blocks a job, Rotor:
- Sets the job status to
guardrail.blocked - Fires a
guardrail.blockedwebhook event (if configured) - Records which stage blocked it and why
The job does not reach your callback handler. It does not retry.
The guardrail.blocked webhook payload
{
"event": "guardrail.blocked",
"data": {
"jobId": "job_abc123...",
"queueName": "ai-outreach",
"blockedBy": "dnc",
"reason": "Email address matched DNC list",
"payload": { "to": "[email protected]", "subject": "..." }
}
}blockedBy is one of: dnc, competitor, pii, brand_tone.
Use case: AI outbound agent
A common Rotor pattern for GTM teams: an AI agent generates personalized outreach emails, enqueues them to an ai-outreach queue, and a callback handler sends via your ESP. Guardrails protect every job before it leaves the system.
// Configure guardrails once
await rotor.guardrails.update({
// Block opted-out contacts
dncEmails: await loadUnsubscribes(),
dncDomains: await loadBouncedDomains(),
// Never mention competitors by name
competitorDomains: ["rivalcrm.com", "otherplatform.io"],
// Redact any PII the LLM accidentally included
piiRedactionEnabled: true,
piiMode: "redact",
// Score brand tone before send
brandToneJudgeEnabled: true,
brandToneRubric:
"Helpful and direct. No urgency language. No superlatives. " +
"Reads like a message from a thoughtful colleague, not marketing copy.",
});
// Enqueue AI-generated emails — guardrails run automatically
await rotor.jobs.enqueueBatch("ai-outreach",
emails.map((email) => ({ payload: email }))
);Any job that references an opted-out contact, a competitor, slips in a phone number, or fails the tone check is silently dropped — and you get a guardrail.blocked webhook so you can review and correct the source.
Hook guardrail.blocked events into your Slack channel. If the brand tone judge starts blocking frequently, it usually means your prompt has drifted — review the rubric and your system prompt together.
Full config reference
| Field | Type | Description |
|---|---|---|
dncEmails | string[] | Email addresses to block |
dncDomains | string[] | Domains to block |
competitorDomains | string[] | Domains that trigger a competitor block |
piiRedactionEnabled | boolean | Whether PII detection is active |
piiMode | "redact" | "block" | What to do when PII is detected |
brandToneJudgeEnabled | boolean | Whether brand tone scoring is active |
brandToneRubric | string | Plain-text description of acceptable tone |