AINode.jsAutomation

Cold Outreach Personalizer at Scale

TT
TopicTrick Team
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 →