AINode.jsAutomation

Cold Outreach Personalizer at Scale

Build an AI cold outreach tool with Node.js that takes a CSV of leads, researches each prospect using web data, and generates hyper-personalised cold emails automatically using GPT-4o.

TT
Emily Ross
5 min read
Cold Outreach Personalizer at Scale

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-limit
bash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000

Lead 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.csv

Sample 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 →