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.

StageRule typeBlocks or modifies
1DNC — do-not-contact listBlocks matching email or domain
2Competitor block — domain deny-listBlocks jobs referencing competitor domains
3PII — personally identifiable informationRedacts or blocks based on piiMode
4Brand 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); // GuardrailConfig

The 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]");
Tip

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,
});
Warning

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",
});
ModeBehaviorUse when
redactStrips PII, job runs with sanitized payloadHandler is safe to receive data but PII shouldn't flow through
blockStops the job entirelyHandler 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.",
});
Note

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:

  1. Sets the job status to guardrail.blocked
  2. Fires a guardrail.blocked webhook event (if configured)
  3. 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.

Tip

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

FieldTypeDescription
dncEmailsstring[]Email addresses to block
dncDomainsstring[]Domains to block
competitorDomainsstring[]Domains that trigger a competitor block
piiRedactionEnabledbooleanWhether PII detection is active
piiMode"redact" | "block"What to do when PII is detected
brandToneJudgeEnabledbooleanWhether brand tone scoring is active
brandToneRubricstringPlain-text description of acceptable tone

Next steps