DEV Community

Vhub Systems
Vhub Systems

Posted on

How to Build a Daily LinkedIn Outreach Tracker That Tells You Which Prospects Are Going Cold

How to Build a Daily LinkedIn Outreach Tracker That Tells You Which Prospects Are Going Cold

LinkedIn is where B2B deals start — and where they quietly die. You send a connection request. You follow up once. Then life gets busy, your prospect goes silent, and three weeks later you realize you never followed up again. The DM is buried. The deal is gone.

If you're running any kind of outbound sales motion — founder-led, SDR-led, or a solo operator doing cold outreach — this is the most consistent leak in your pipeline. Not lost deals. Forgotten ones.

The Problem: LinkedIn Outreach Has No Memory

CRMs are built for email sequences, not LinkedIn. There's no native alert when a prospect you messaged five days ago hasn't responded. Sales Navigator will show you who viewed your profile, but it won't tell you which of your outbound contacts have gone cold or surface a prioritized list of who needs a follow-up today.

The result: sequences die silently. You might remember to follow up on 30% of your conversations. The other 70% evaporate.

Manual solutions don't work either. Tracking outreach in a spreadsheet with no automation means you're doing a daily audit by hand — checking rows, updating timestamps, remembering context. At 20–40 new contacts a week, that becomes a job in itself.

What Tools Charge to Solve This

Dedicated sales engagement platforms bill this as a core feature:

  • Outreach.io: $100+ per user/month — sequences, tasks, LinkedIn integration
  • Salesloft: $75–$125 per user/month — cadences, follow-up reminders, CRM sync
  • Sales Navigator Advanced: $79–$150 per user/month — InMail, saved leads, basic sequence tracking

That's $900–$1,800/user/year for features that mostly amount to: "remind me to follow up."

For a small team or solo founder, this is hard to justify — especially when the underlying logic is simple: check if a contact hasn't responded in X days, and surface it.

The Tracker Architecture

You need three things:

  1. A log — where outreach records live (Google Sheets)
  2. A data source — to pull LinkedIn profile activity signals (Apify lanky_quantifier/linkedin-job-scraper)
  3. An alert mechanism — Slack daily digest + optional CRM task creation

Google Sheets is the backbone. Each row represents one outreach contact: name, company, LinkedIn URL, date of last contact, last interaction type, status, and a calculated "days cold" column.

Apify's linkedin-job-scraper actor can pull profile-level signals — hiring activity, posted content, open roles — which serve as a proxy for engagement and recency. A prospect whose company is actively hiring or whose profile shows recent activity is still warm. One with no signal updates in 10+ days is likely checked out.

Implementation: The Node.js Tracker Script

Install dependencies:

npm install googleapis apify-client @slack/webhook
Enter fullscreen mode Exit fullscreen mode

Step 1 — Write an outreach record when you send:

const { google } = require('googleapis');

async function logOutreach({ name, company, linkedinUrl, message }) {
  const auth = new google.auth.GoogleAuth({
    keyFile: 'credentials.json',
    scopes: ['https://www.googleapis.com/auth/spreadsheets'],
  });
  const sheets = google.sheets({ version: 'v4', auth });
  const today = new Date().toISOString().split('T')[0];

  await sheets.spreadsheets.values.append({
    spreadsheetId: process.env.SHEET_ID,
    range: 'Outreach!A:G',
    valueInputOption: 'USER_ENTERED',
    requestBody: {
      values: [[name, company, linkedinUrl, today, 'connection_sent', 'active', '']],
    },
  });
  console.log(`Logged outreach to ${name} at ${company}`);
}
Enter fullscreen mode Exit fullscreen mode

Step 2 — Calculate cold status and flag stale contacts:

async function checkColdProspects() {
  const auth = new google.auth.GoogleAuth({
    keyFile: 'credentials.json',
    scopes: ['https://www.googleapis.com/auth/spreadsheets'],
  });
  const sheets = google.sheets({ version: 'v4', auth });

  const res = await sheets.spreadsheets.values.get({
    spreadsheetId: process.env.SHEET_ID,
    range: 'Outreach!A:G',
  });

  const rows = res.data.values || [];
  const today = new Date();
  const COLD_THRESHOLD_DAYS = 5;
  const coldProspects = [];

  for (const row of rows.slice(1)) { // skip header
    const [name, company, linkedinUrl, lastContact, lastType, status] = row;
    if (status === 'responded' || status === 'closed') continue;

    const lastDate = new Date(lastContact);
    const daysSince = Math.floor((today - lastDate) / (1000 * 60 * 60 * 24));

    if (daysSince >= COLD_THRESHOLD_DAYS) {
      coldProspects.push({ name, company, linkedinUrl, daysSince, lastType });
    }
  }

  return coldProspects;
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — (Optional) Enrich with Apify signals:

const { ApifyClient } = require('apify-client');

async function getLinkedInSignals(linkedinUrl) {
  const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

  const run = await client.actor('lanky_quantifier/linkedin-job-scraper').call({
    startUrls: [{ url: linkedinUrl }],
    maxItems: 5,
  });

  const { items } = await client.dataset(run.defaultDatasetId).listItems();
  return items.length > 0 ? 'active' : 'quiet';
}
Enter fullscreen mode Exit fullscreen mode

This call is optional — run it only on contacts flagged as cold to keep costs near zero.

The Slack Daily Digest

Run this on a cron job at 8 AM each morning:

const { IncomingWebhook } = require('@slack/webhook');

async function sendColdProspectDigest() {
  const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL);
  const coldProspects = await checkColdProspects();

  if (coldProspects.length === 0) {
    await webhook.send({ text: '✅ No cold prospects today. Pipeline is warm.' });
    return;
  }

  const blocks = coldProspects.map(p => ({
    type: 'section',
    text: {
      type: 'mrkdwn',
      text: `*${p.name}* — ${p.company}\n📅 ${p.daysSince} days since last contact (${p.lastType})\n💬 Suggested: Send a brief check-in or share a relevant resource\n🔗 ${p.linkedinUrl}`,
    },
  }));

  await webhook.send({
    text: `🧊 ${coldProspects.length} prospect(s) going cold — action needed`,
    blocks: [
      {
        type: 'header',
        text: { type: 'plain_text', text: `Cold Prospect Digest — ${new Date().toDateString()}` },
      },
      ...blocks,
    ],
  });

  console.log(`Digest sent: ${coldProspects.length} cold prospects`);
}
Enter fullscreen mode Exit fullscreen mode

The output is a prioritized, actionable Slack message. Name, company, days since contact, last touch type, and a suggested next step — every morning, automatically.

CRM Sync: Push Cold Flags as Tasks

If you're using HubSpot or Pipedrive, you can auto-create tasks so cold prospects show up in your normal workflow:

async function createHubSpotTask({ name, company, daysSince }) {
  const response = await fetch('https://api.hubapi.com/crm/v3/objects/tasks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
    },
    body: JSON.stringify({
      properties: {
        hs_task_subject: `Follow up: ${name} @ ${company}${daysSince}d cold`,
        hs_task_body: 'LinkedIn prospect flagged cold. Send a brief check-in.',
        hs_task_status: 'NOT_STARTED',
        hs_task_priority: 'HIGH',
        hs_timestamp: Date.now(),
      },
    }),
  });
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

For Pipedrive, swap the endpoint and field names — the pattern is identical.

Run this in the same daily digest job to ensure cold prospects surface as CRM tasks without manual data entry.

Scheduling It

Add a cron entry to run the digest each weekday morning:

0 8 * * 1-5 node /path/to/cold-tracker.js
Enter fullscreen mode Exit fullscreen mode

Or deploy to a service like Railway, Render, or a simple EC2 instance. The script runs in under 10 seconds for most outreach volumes.

What This Costs

Component Cost
Google Sheets API Free (within quota)
Apify enrichment (optional, cold contacts only) $0–$3/month at typical volumes
Slack webhook Free
HubSpot/Pipedrive task creation Free tier covers this
Total $0–$5/month

Compare that to $75–$100+/user/month for Outreach or Salesloft — that's $900–$1,200/year for a follow-up reminder system.

What You Get

The tracker doesn't replace a full sales engagement platform. It doesn't give you email sequencing, call logging, or pipeline analytics. What it does give you is the one thing that kills most outbound sequences: visibility into which conversations are going cold before they're dead.

You know who to follow up with today. You know how long it's been. You have a suggested action. And it all lands in Slack — where you're already working.

Five minutes of follow-up on the right cold prospect is worth more than sending 50 new connection requests.

Extensions Worth Building Next

  • Profile activity enrichment: Run the Apify actor on all cold contacts weekly to detect recent posts or job changes — a prospect who just got promoted is worth re-engaging
  • Warm handoff flag: Auto-close contacts who've been cold for 30+ days and add them to a re-engage list for next quarter
  • Stage tracking: Add a stage column (connected → replied → call_booked → closed) and calculate conversion rate per stage
  • Slack action buttons: Add "Mark Responded" and "Snooze 3 Days" buttons directly in the Slack message using Block Kit interactive components

The core is under 100 lines of Node.js. The rest is wiring.


Built with Apify, Google Sheets API, and Slack Webhooks.

Top comments (0)