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
- Claude Agent SDK: Build Automation Agents — SDK fundamentals
- How to Test Claude Agents — Testing compliance rules
- Building a Content Generation Agent — Another agent pipeline
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.