Loading...
Loading...

The average cold email open rate is 21%. Response rate: 1-5%. Most cold outreach campaigns reach the wrong people with a message that reads like it was written for anyone, because it was.
AI doesn't fix cold outreach by making it more personalized-sounding. It fixes it by helping you identify the right people and say something genuinely relevant to them. Those are different problems with different solutions.
Here's the system I built that now generates roughly 60% of my pipeline with about 2 hours of weekly oversight.
Before code, let me show you what we're building:
Lead Source (Apollo, LinkedIn, Clearbit)
|
v
Qualification Layer (Claude)
|
v
Enrichment Layer (company context, recent news)
|
v
Personalization Engine (Claude)
|
v
Sequence Manager (timing, follow-up logic)
|
v
Email Sender (with reply detection)
|
v
CRM (track outcomes, improve signals)
The key insight: personalization at scale only works if the personalization layer has good information to work with. The enrichment layer is where most systems fail.
Qualification before outreach is the highest-leverage step. Reaching 100 well-qualified leads beats 1000 generic ones every time.
// qualifier.ts
import Anthropic from "@anthropic-ai/sdk";
export interface Lead {
name: string;
title: string;
company: string;
companySize?: string;
industry?: string;
linkedinUrl?: string;
email?: string;
companyWebsite?: string;
}
export interface QualificationResult {
qualified: boolean;
score: number; // 0-100
reasons: string[];
disqualifyReasons: string[];
suggestedAngle: string;
}
const anthropic = new Anthropic();
// Define your ideal customer profile
const ICP = `
Ideal Customer Profile:
- Company size: 20-500 employees
- Industry: B2B SaaS, Professional Services, or E-commerce
- Role: Founder, CEO, VP of Sales, Head of Marketing
- Pain points: Manual processes, scaling challenges, team efficiency
- NOT a fit: Enterprise companies (500+), B2C, Government, Non-profit
`;
export async function qualifyLead(lead: Lead): Promise<QualificationResult> {
const response = await anthropic.messages.create({
model: "claude-haiku-4-20250514",
max_tokens: 512,
system: `You are a sales qualification expert. Evaluate leads against our ICP and return JSON.
${ICP}
Return: { qualified: boolean, score: 0-100, reasons: string[], disqualifyReasons: string[], suggestedAngle: string }`,
messages: [
{
role: "user",
content: `Lead: ${JSON.stringify(lead)}`,
},
],
});
const text = response.content[0].type === "text" ? response.content[0].text : "{}";
try {
return JSON.parse(text);
} catch {
return { qualified: false, score: 0, reasons: [], disqualifyReasons: ["Parse error"], suggestedAngle: "" };
}
}Personalization requires context. This step gathers what you need.
// enricher.ts
import Anthropic from "@anthropic-ai/sdk";
export interface EnrichedLead extends Lead {
companyDescription?: string;
recentNews?: string[];
techStack?: string[];
estimatedRevenue?: string;
fundingInfo?: string;
companyPainPoints?: string[];
personalTriggers?: string[]; // recent posts, articles, talks by the lead
}
async function fetchCompanyContext(website: string): Promise<string> {
try {
const response = await fetch(website, {
signal: AbortSignal.timeout(5000),
});
const html = await response.text();
// Simple extraction - in production use a proper scraper
return html
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ")
.slice(0, 3000);
} catch {
return "";
}
}
const anthropic = new Anthropic();
export async function enrichLead(
lead: Lead & { qualified: boolean; suggestedAngle: string }
): Promise<EnrichedLead> {
// Fetch company context if we have their website
const companyContext = lead.companyWebsite
? await fetchCompanyContext(lead.companyWebsite)
: "";
if (!companyContext) return lead;
const response = await anthropic.messages.create({
model: "claude-haiku-4-20250514",
max_tokens: 1024,
system: `Extract structured information from company website content. Return JSON only.
Extract: { companyDescription, techStack, companyPainPoints, estimatedRevenue }`,
messages: [
{
role: "user",
content: `Company: ${lead.company}\nWebsite content:\n${companyContext}`,
},
],
});
const text = response.content[0].type === "text" ? response.content[0].text : "{}";
try {
const extracted = JSON.parse(text);
return { ...lead, ...extracted };
} catch {
return lead;
}
}This is where the system earns its keep. Genuinely personalized emails, not mail-merge personalization.
// email-generator.ts
import Anthropic from "@anthropic-ai/sdk";
import { EnrichedLead } from "./enricher.js";
export interface GeneratedEmail {
subject: string;
body: string;
personalizationNotes: string; // for review purposes
confidence: number; // 0-1, how confident the AI is this will resonate
}
const anthropic = new Anthropic();
const YOUR_CONTEXT = `
About us: We help B2B companies automate their operations with AI agents.
Key offer: We replace 3-5 manual processes with AI in 30 days, saving 10+ hours/week per process.
Proof: We've done this for 47 companies. Average ROI: 4.2x in 6 months.
CTA: 20-minute discovery call to map their automation opportunities.
`;
export async function generatePersonalizedEmail(
lead: EnrichedLead & { suggestedAngle: string }
): Promise<GeneratedEmail> {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: `You are an expert B2B copywriter. Write cold emails that don't read like cold emails.
Principles:
- Lead with something specific to THEM (not a generic compliment)
- One clear point about what you do and why it matters to them
- One specific, low-friction CTA
- Under 150 words total
- No corporate language
- Sound like a smart person, not a marketing team
${YOUR_CONTEXT}`,
messages: [
{
role: "user",
content: `Write a cold email for this lead.
Recipient: ${lead.name}, ${lead.title} at ${lead.company}
Company context: ${lead.companyDescription || "B2B company"}
They likely care about: ${lead.companyPainPoints?.join(", ") || "efficiency and growth"}
Suggested angle: ${lead.suggestedAngle}
Return JSON: { subject, body, personalizationNotes, confidence }`,
},
],
});
const text = response.content[0].type === "text" ? response.content[0].text : "{}";
try {
return JSON.parse(text);
} catch {
return {
subject: `Quick question, ${lead.name}`,
body: "Hi ${lead.name}, I'd love to learn more about what you're working on at ${lead.company}...",
personalizationNotes: "Template fallback",
confidence: 0.3,
};
}
}// pipeline.ts
import * as dotenv from "dotenv";
dotenv.config();
import { Lead } from "./qualifier.js";
import { qualifyLead } from "./qualifier.js";
import { enrichLead } from "./enricher.js";
import { generatePersonalizedEmail } from "./email-generator.js";
import * as fs from "fs/promises";
// Configuration
const CONFIDENCE_THRESHOLD = 0.7; // Only send emails AI is 70%+ confident about
const MIN_QUALIFICATION_SCORE = 60; // 0-100 scale
async function processLead(lead: Lead): Promise<void> {
console.log(`Processing: ${lead.name} at ${lead.company}`);
// Step 1: Qualify
const qualification = await qualifyLead(lead);
console.log(` Qualification score: ${qualification.score}`);
if (!qualification.qualified || qualification.score < MIN_QUALIFICATION_SCORE) {
console.log(` SKIPPED: ${qualification.disqualifyReasons.join(", ")}`);
return;
}
// Step 2: Enrich
const enriched = await enrichLead({ ...lead, ...qualification });
// Step 3: Generate email
const email = await generatePersonalizedEmail(enriched);
console.log(` Email confidence: ${email.confidence}`);
if (email.confidence < CONFIDENCE_THRESHOLD) {
console.log(` SKIPPED: Low confidence email (${email.confidence})`);
// Could save to manual review queue instead
return;
}
// Step 4: Save for review (or send directly if fully automated)
const output = {
lead: enriched,
qualification,
email,
timestamp: new Date().toISOString(),
};
await fs.appendFile(
"./output/ready-to-send.jsonl",
JSON.stringify(output) + "\n"
);
console.log(` READY: ${email.subject}`);
}
async function runPipeline(leads: Lead[]): Promise<void> {
await fs.mkdir("./output", { recursive: true });
console.log(`Processing ${leads.length} leads...\n`);
for (const lead of leads) {
await processLead(lead);
// Rate limit: 1 second between leads
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log("\nPipeline complete. Review ./output/ready-to-send.jsonl");
}
// Example leads - in production, fetch from Apollo, LinkedIn, Clearbit, etc.
const exampleLeads: Lead[] = [
{
name: "Sarah Chen",
title: "VP of Operations",
company: "Stackify",
companySize: "85",
industry: "SaaS",
email: "sarah@stackify.com",
companyWebsite: "https://stackify.com",
},
];
runPipeline(exampleLeads).catch(console.error);I keep a human review step for all emails before they send. The AI generates. I approve. I send.
This sounds like it defeats the purpose of automation, but it takes 15-20 seconds per email if the quality is high. I review 15-20 emails in 5 minutes. The time saving versus manually writing each email is enormous.
For emails the AI flags with high confidence (>0.85), I've experimented with fully automated sending. The results were comparable to reviewed emails, but I stopped after one genuinely embarrassing message to someone I knew personally. The context gap between what the AI knows and what I know about specific people is real.
My rule: under $10,000 in potential deal value, full automation. Over that threshold, human review.
| Metric | Manual Outreach | AI-Assisted |
|---|---|---|
| Leads processed per week | 15-20 | 80-120 |
| Time spent | 8-10 hours | 1.5-2 hours |
| Open rate | 28% | 34% |
| Reply rate | 4% | 11% |
| Positive reply rate | 2% | 7% |
| Meetings booked per 100 leads | 2 | 7 |
The improvement in reply rate comes from better qualification and more relevant personalization. I'm reaching people who should care about what I offer, and saying something specific to their situation.

n8n can connect anything to anything. Most people spend days fighting the setup instead of building automations. Here's how to get running fast and stay running.

Your inbox is not a to-do list. It's an interrupt machine. Here's how to build an AI system that handles 80% of your email without you reading it.

Chat widgets are easy. Production chatbots that handle real users without breaking are hard. Here's the full guide to building one that actually works.
Stop reading about AI and start building with it. Book a free discovery call and see how AI agents can accelerate your business.