AINode.jsAutomation
Cold Outreach Personalizer at Scale
TT
TopicTrick Team
Cold Outreach Personalizer at Scale
Generic cold emails get deleted. Personalised ones get replies. This tool takes a CSV of leads, researches each prospect's company and role, then generates a unique, personalised cold email for each one — in bulk, automatically.
This is Tool 13 of the Build 50 AI Automation Tools course.
What You'll Build
- Read a CSV of leads (name, email, company, LinkedIn URL)
- Fetch context about each prospect's company
- Generate personalised cold emails for each lead
- Export results as CSV with email drafts ready to send
Setup
bash
mkdir outreach-personalizer && cd outreach-personalizer
npm init -y
npm install express axios cheerio openai csv-parse csv-stringify dotenv p-limitbash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000Lead CSV Format
csv
firstName,lastName,email,company,title,companyUrl,linkedinUrl
Sarah,Chen,sarah@acme.com,Acme Corp,CTO,https://acme.com,https://linkedin.com/in/sarahchen
Tom,Wilson,tom@techstart.io,TechStart,VP Engineering,https://techstart.io,Personalization Service
js
// src/services/outreachService.js
import axios from 'axios';
import * as cheerio from 'cheerio';
import OpenAI from 'openai';
import pLimit from 'p-limit';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const limit = pLimit(3);
async function researchCompany(companyUrl) {
if (!companyUrl) return '';
try {
const { data } = await axios.get(companyUrl, { timeout: 10_000 });
const $ = cheerio.load(data);
$('script, style').remove();
return $('body').text().replace(/\s+/g, ' ').slice(0, 2000);
} catch { return ''; }
}
async function generatePersonalizedEmail({
lead,
companyContext,
yourProduct,
yourName,
tone = 'professional',
}) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are an expert cold email copywriter. Write a short, highly personalised cold email.
Rules:
- Maximum 150 words total
- Start with a personalised observation about their company (not generic flattery)
- Connect their situation to a specific pain point your product solves
- One clear, low-friction call to action
- No fluff, no "I hope this email finds you well"
- Do not mention being an AI
Sender: ${yourName}
Product/Service: ${yourProduct}
Tone: ${tone}
Return ONLY JSON: {
"subject": "email subject line (under 60 chars, curiosity-driven)",
"body": "complete email body",
"personalizationHook": "the specific fact used to personalize",
"characterCount": number
}`,
},
{
role: 'user',
content: `PROSPECT:
Name: ${lead.firstName} ${lead.lastName}
Title: ${lead.title || 'not known'}
Company: ${lead.company}
Company context: ${companyContext || 'No context available'}`,
},
],
temperature: 0.8,
response_format: { type: 'json_object' },
});
return JSON.parse(response.choices[0].message.content);
}
export async function personalizeOutreach(leads, config) {
const { yourProduct, yourName, tone } = config;
const results = [];
await Promise.all(leads.map(lead => limit(async () => {
try {
const companyContext = await researchCompany(lead.companyUrl);
const email = await generatePersonalizedEmail({ lead, companyContext, yourProduct, yourName, tone });
results.push({
...lead,
success: true,
subject: email.subject,
emailBody: email.body,
personalizationHook: email.personalizationHook,
});
} catch (err) {
results.push({ ...lead, success: false, error: err.message });
}
await new Promise(r => setTimeout(r, 500));
})));
return results;
}CSV Processing + Server
js
// src/server.js
import 'dotenv/config';
import express from 'express';
import multer from 'multer';
import { parse } from 'csv-parse/sync';
import { stringify } from 'csv-stringify/sync';
import { personalizeOutreach } from './services/outreachService.js';
const app = express();
app.use(express.json());
const upload = multer({ storage: multer.memoryStorage() });
app.post('/personalize', upload.single('leads'), async (req, res, next) => {
try {
const { yourProduct, yourName, tone } = req.body;
if (!req.file || !yourProduct || !yourName) {
return res.status(400).json({ error: 'leads CSV, yourProduct, and yourName required' });
}
const leads = parse(req.file.buffer.toString(), { columns: true, skip_empty_lines: true });
const results = await personalizeOutreach(leads, { yourProduct, yourName, tone });
// Return as CSV
if (req.headers.accept === 'text/csv') {
const csv = stringify(results, { header: true });
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="personalized-emails.csv"');
return res.send(csv);
}
res.json({
success: true,
processed: results.length,
successful: results.filter(r => r.success).length,
results,
});
} catch (err) { next(err); }
});
app.use((err, _req, res, _next) => res.status(500).json({ error: err.message }));
app.listen(process.env.PORT ?? 3000, () => console.log('Outreach Personalizer running'));Testing
bash
# Generate personalized emails
curl -X POST http://localhost:3000/personalize \
-F "leads=@leads.csv" \
-F "yourProduct=AI-powered project management for engineering teams" \
-F "yourName=Alex Johnson" \
-F "tone=professional"
# Get results as CSV
curl -X POST http://localhost:3000/personalize \
-H "Accept: text/csv" \
-F "leads=@leads.csv" \
-F "yourProduct=AI project management" \
-F "yourName=Alex Johnson" \
> personalized-emails.csvSample output:
json
{
"firstName": "Sarah",
"company": "Acme Corp",
"subject": "Engineering velocity at Acme",
"emailBody": "Hi Sarah,\n\nI noticed Acme just shipped your microservices migration — impressive work moving 200+ services in 6 months.\n\nEngineering teams at that scale usually hit a wall with sprint planning across distributed teams. We built [Product] to give CTOs like you real-time visibility into cross-team dependencies without another meeting.\n\nWould 15 minutes this week make sense to show you how it works?\n\nAlex",
"personalizationHook": "Recent microservices migration milestone from company blog"
}Build 50 AI Automation Tools — Tool 13 of 50
Cold outreach personalization is live. Continue to Tool 14 to build an AI email classifier and auto-tagger.
Summary
- Research-first personalization scrapes company context to generate genuinely specific opening lines
- p-limit controls concurrency — processes leads in parallel without overwhelming scraped sites
- CSV in, CSV out makes integration with any CRM or email sending tool straightforward
- 150-word limit in the prompt enforces email brevity — the most common mistake in cold outreach
- Always comply with CAN-SPAM and GDPR — include opt-out links and only email business contacts
Continue to Tool 14: Email Classifier & Auto-Tagger →
