← All guides

Building a Sales Outreach Agent with Compliance Guardrails

How to build a Claude-powered sales outreach agent that generates personalized emails, enforces compliance rules (CAN-SPAM, GDPR), prevents spam.

Building a Sales Outreach Agent with Compliance Guardrails

A sales outreach agent without compliance guardrails is a spam machine. A well-built one enforces CAN-SPAM/GDPR opt-out handling, personalizes without fabricating facts, rate-limits to avoid inbox reputation damage, and logs everything for audit purposes. This guide builds a compliant outreach agent from the ground up, with the guardrails that keep it professional and legal.


The Compliance Requirements

Before building:

Requirement CAN-SPAM (US) GDPR (EU)
Unsubscribe mechanism Required Required
Physical address in email Required Not required
No deceptive subject lines Required Required
Consent before outreach Not required Required (cold email = legitimate interest defense)
Data retention limits Not specified Defined retention period required
Opt-out processing time 10 business days Within 1 month

Practical minimum for B2B cold outreach: unsubscribe link + physical address + honest subject + no fabricated content.


Architecture

Contact list → Filter (unsubscribe check) → Research (company/role) → 
Personalization (Claude) → Compliance check → Rate limit → Send → Log

Each stage has explicit guardrails. The agent cannot skip stages.


Stage 1: Contact Filtering and Opt-Out Check

import anthropic
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timedelta


@dataclass
class Contact:
    id: str
    email: str
    name: str
    company: str
    role: str
    linkedin_url: str = None
    last_contacted: str = None


class OptOutRegistry:
    """Permanent opt-out list — never email these addresses."""

    def __init__(self, db_path: str = "outreach.db"):
        self.conn = sqlite3.connect(db_path)
        self._init_db()

    def _init_db(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS opt_outs (
                email TEXT PRIMARY KEY,
                opted_out_at TEXT NOT NULL,
                reason TEXT
            )
        """)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS outreach_log (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email TEXT,
                sent_at TEXT,
                subject TEXT,
                personalization_notes TEXT
            )
        """)
        self.conn.commit()

    def is_opted_out(self, email: str) -> bool:
        row = self.conn.execute(
            "SELECT 1 FROM opt_outs WHERE email = ?", (email.lower(),)
        ).fetchone()
        return row is not None

    def add_opt_out(self, email: str, reason: str = "unsubscribe_link"):
        self.conn.execute(
            "INSERT OR IGNORE INTO opt_outs VALUES (?, ?, ?)",
            (email.lower(), datetime.utcnow().isoformat(), reason)
        )
        self.conn.commit()

    def was_contacted_recently(self, email: str, days: int = 30) -> bool:
        """Prevent re-contacting within N days."""
        cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat()
        row = self.conn.execute(
            "SELECT 1 FROM outreach_log WHERE email = ? AND sent_at > ?",
            (email.lower(), cutoff)
        ).fetchone()
        return row is not None

    def log_sent(self, email: str, subject: str, notes: str = ""):
        self.conn.execute(
            "INSERT INTO outreach_log (email, sent_at, subject, personalization_notes) VALUES (?, ?, ?, ?)",
            (email.lower(), datetime.utcnow().isoformat(), subject, notes)
        )
        self.conn.commit()


def filter_contacts(contacts: list[Contact], registry: OptOutRegistry) -> list[Contact]:
    """Remove opted-out and recently contacted leads."""
    approved = []
    skipped = []

    for contact in contacts:
        if registry.is_opted_out(contact.email):
            skipped.append((contact.email, "opted out"))
        elif registry.was_contacted_recently(contact.email, days=30):
            skipped.append((contact.email, "contacted < 30 days ago"))
        else:
            approved.append(contact)

    print(f"Approved: {len(approved)}, Skipped: {len(skipped)}")
    for email, reason in skipped[:5]:
        print(f"  Skipped {email}: {reason}")

    return approved

Stage 2: Personalization with Guardrails

client = anthropic.Anthropic()


PERSONALIZATION_SYSTEM = """You are a B2B sales assistant writing cold outreach emails.

STRICT RULES — violating these will cause legal and reputation damage:

1. NEVER fabricate information. If you don't know something, don't include it.
   - Bad: "I saw your recent Series B announcement" (if you don't know this)
   - Good: "I work with [role]s at companies like yours"

2. NEVER make specific claims about the recipient's performance or problems
   unless they are documented facts.
   - Bad: "Your sales team is probably struggling with..."
   - Good: "Many [role]s I work with have found..."

3. NEVER use aggressive, high-pressure, or deceptive language.

4. Keep subject lines honest and accurate.
   - Bad: "Quick question" (if it's not a quick question)
   - Good: "Intro: [Your company] for [their company]"

5. One specific personalization detail is better than three generic ones.

EMAIL REQUIREMENTS:
- 3-4 sentences maximum
- Clear value proposition in sentence 2
- Specific ask (30-min call? Demo?)
- Professional but not stiff
- No attachments references (they don't exist in this context)"""


@dataclass
class EmailDraft:
    subject: str
    body: str
    personalization_used: str
    compliance_flags: list[str]


def generate_email(contact: Contact, context: dict) -> EmailDraft:
    """
    Generate a personalized, compliant email for one contact.
    context: dict with your company info, product, value prop
    """
    prompt = f"""Write a cold outreach email for this contact:

Name: {contact.name}
Company: {contact.company}
Role: {contact.role}
LinkedIn (if available): {contact.linkedin_url or 'not available'}

Your company: {context['sender_company']}
What you offer: {context['value_proposition']}
Target use case: {context['use_case']}
Desired action: {context['desired_action']}

Return JSON:
{{
  "subject": "email subject line",
  "body": "full email body (include unsubscribe instructions placeholder: [UNSUBSCRIBE_LINK])",
  "personalization_used": "what specific detail you personalized on",
  "concerns": ["list any compliance concerns you have about this email"]
}}"""

    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=800,
        system=PERSONALIZATION_SYSTEM,
        messages=[{"role": "user", "content": prompt}]
    )

    import json
    import re
    text = response.content[0].text
    json_match = re.search(r'\{.*\}', text, re.DOTALL)
    if not json_match:
        raise ValueError(f"No JSON in response: {text[:200]}")

    data = json.loads(json_match.group())

    return EmailDraft(
        subject=data["subject"],
        body=data["body"],
        personalization_used=data.get("personalization_used", ""),
        compliance_flags=data.get("concerns", [])
    )

Stage 3: Compliance Validation

def validate_compliance(draft: EmailDraft, sender_info: dict) -> tuple[bool, list[str]]:
    """
    Check the draft against compliance rules.
    Returns (is_compliant, list_of_issues).
    """
    issues = []

    # CAN-SPAM: unsubscribe mechanism must be present
    if "[UNSUBSCRIBE_LINK]" not in draft.body and "unsubscribe" not in draft.body.lower():
        issues.append("FAIL: No unsubscribe mechanism in email body (CAN-SPAM required)")

    # CAN-SPAM: physical address
    if "physical_address" in sender_info:
        if sender_info["physical_address"].lower() not in draft.body.lower():
            issues.append(f"WARN: Physical address not included (CAN-SPAM required for US)")

    # Deceptive subject line checks
    deceptive_patterns = ["re:", "fwd:", "fw:", "[urgent]", "important update"]
    subject_lower = draft.subject.lower()
    for pattern in deceptive_patterns:
        if pattern in subject_lower:
            issues.append(f"FAIL: Deceptive subject line pattern '{pattern}' (CAN-SPAM violation)")

    # Content quality checks
    fabrication_signals = [
        "i saw your recent", "congrats on", "i noticed you just",
        "you're probably struggling with", "your team is likely"
    ]
    body_lower = draft.body.lower()
    for signal in fabrication_signals:
        if signal in body_lower:
            issues.append(f"WARN: Potential fabricated claim — review: '{signal}'")

    # Subject line length
    if len(draft.subject) > 60:
        issues.append(f"WARN: Subject line too long ({len(draft.subject)} chars) — aim for <50")

    # Body length check (cold email should be concise)
    word_count = len(draft.body.split())
    if word_count > 150:
        issues.append(f"WARN: Email too long ({word_count} words) — cold emails should be <100 words")

    # Forward agent's compliance flags
    for flag in draft.compliance_flags:
        issues.append(f"AGENT_FLAG: {flag}")

    # Determine pass/fail
    has_failures = any(issue.startswith("FAIL:") for issue in issues)
    return not has_failures, issues

Stage 4: Rate Limiting

import time


class OutreachRateLimiter:
    """
    Enforce send rate limits to protect inbox reputation.
    Industry guidance: < 50 new contacts/day for cold outreach on new domains.
    """

    def __init__(self, max_per_hour: int = 10, max_per_day: int = 50):
        self.max_per_hour = max_per_hour
        self.max_per_day = max_per_day
        self.sent_this_hour = 0
        self.sent_today = 0
        self.hour_reset_time = time.time() + 3600
        self.day_reset_time = time.time() + 86400

    def can_send(self) -> tuple[bool, str]:
        now = time.time()

        # Reset counters if window passed
        if now > self.hour_reset_time:
            self.sent_this_hour = 0
            self.hour_reset_time = now + 3600

        if now > self.day_reset_time:
            self.sent_today = 0
            self.day_reset_time = now + 86400

        if self.sent_this_hour >= self.max_per_hour:
            wait_secs = int(self.hour_reset_time - now)
            return False, f"Hourly limit reached ({self.max_per_hour}/hr). Wait {wait_secs}s."

        if self.sent_today >= self.max_per_day:
            return False, f"Daily limit reached ({self.max_per_day}/day). Continue tomorrow."

        return True, "ok"

    def record_sent(self):
        self.sent_this_hour += 1
        self.sent_today += 1

Full Pipeline

def run_outreach_campaign(
    contacts: list[Contact],
    context: dict,
    sender_info: dict,
    dry_run: bool = True
) -> dict:
    """
    Full compliant outreach pipeline.
    dry_run=True: generate emails but don't send.
    """
    registry = OptOutRegistry()
    rate_limiter = OutreachRateLimiter(max_per_hour=10, max_per_day=50)

    stats = {"filtered": 0, "drafted": 0, "compliance_pass": 0,
             "compliance_fail": 0, "sent": 0, "errors": 0}

    approved_contacts = filter_contacts(contacts, registry)
    stats["filtered"] = len(contacts) - len(approved_contacts)

    for contact in approved_contacts:
        # Rate limit check
        can_send, reason = rate_limiter.can_send()
        if not can_send:
            print(f"Rate limit: {reason}")
            break

        try:
            # Generate email
            draft = generate_email(contact, context)
            stats["drafted"] += 1

            # Finalize email (replace placeholder, add address)
            unsubscribe_url = f"https://yoursite.com/unsubscribe?email={contact.email}"
            final_body = draft.body.replace("[UNSUBSCRIBE_LINK]", unsubscribe_url)
            if "physical_address" in sender_info:
                final_body += f"\n\n{sender_info['physical_address']}"

            # Compliance check
            is_compliant, issues = validate_compliance(draft, sender_info)

            if not is_compliant:
                print(f"COMPLIANCE FAIL for {contact.email}:")
                for issue in issues:
                    print(f"  {issue}")
                stats["compliance_fail"] += 1
                continue

            stats["compliance_pass"] += 1

            if dry_run:
                print(f"\n[DRY RUN] To: {contact.email}")
                print(f"Subject: {draft.subject}")
                print(f"Personalized on: {draft.personalization_used}")
                if issues:
                    print(f"Warnings: {issues}")
            else:
                # Send via your email provider (SendGrid, Postmark, etc.)
                # send_email(contact.email, draft.subject, final_body, sender_info)
                registry.log_sent(contact.email, draft.subject, draft.personalization_used)
                rate_limiter.record_sent()
                stats["sent"] += 1

            time.sleep(2)  # Be a good citizen

        except Exception as e:
            print(f"Error for {contact.email}: {e}")
            stats["errors"] += 1

    return stats


# Usage
campaign_context = {
    "sender_company": "Acme Analytics",
    "value_proposition": "We help B2B SaaS teams reduce churn by identifying at-risk accounts 30 days earlier",
    "use_case": "Customer success teams with 50-500 accounts",
    "desired_action": "15-minute intro call"
}

sender_info = {
    "name": "Alex Kim",
    "email": "alex@acme.com",
    "physical_address": "123 Main St, San Francisco, CA 94105"
}

stats = run_outreach_campaign(my_contacts, campaign_context, sender_info, dry_run=True)
print(stats)

Frequently Asked Questions

Is cold email legal in 2026? B2B cold email remains legal in most jurisdictions under CAN-SPAM (US) and GDPR's legitimate interest basis (EU) if you have a genuine business reason to contact the recipient, honor opt-outs, and don't use deceptive tactics. Always consult legal counsel for your specific situation.

How do I build the unsubscribe endpoint? Create GET /unsubscribe?email=[email] that adds the email to your opt-out registry and shows a confirmation page. The opt-out must be processed within 10 business days (CAN-SPAM). For GDPR, process immediately.

Can I scrape LinkedIn for contacts? LinkedIn's Terms of Service prohibit scraping. Use legal data sources: LinkedIn Sales Navigator, Apollo.io, ZoomInfo, or manually curated lists from public sources.

What email sending service should I use? For outreach: Postmark or SendGrid. Both have good deliverability, suppression list management, and unsubscribe link tools. Avoid Gmail/Outlook SMTP for bulk outreach — it will get your personal domain flagged.


Related Guides


Go Deeper

Agent SDK Cookbook — $49 — Full sales agent implementation: SendGrid integration, webhook-based opt-out handler, campaign analytics dashboard, A/B testing for subject lines, and GDPR documentation templates.

→ Get the Agent SDK Cookbook — $49

30-day money-back guarantee. Instant download.

AI Disclosure: Written with Claude Code; patterns validated against CAN-SPAM and GDPR requirements.

Tools and references