DEV Community

Vhub Systems
Vhub Systems

Posted on

How to Build an Automated Job Change Alert That Notifies You When a Past Prospect Gets a New Role

Job changes are the single highest-intent buying signal in B2B sales — and most reps miss them entirely.

Here's the scenario: you pitched a solid prospect six months ago. They were interested but the timing wasn't right. Then they moved to a new company, took a VP role, and now have both the authority and the budget to buy. You had no idea. Someone else got that deal.

LinkedIn notifications are unreliable. CRM contact records go stale within weeks of a rep leaving them untouched. And manually checking every cold prospect's profile once a week is not a system — it's a chore that never gets done.

This article shows you how to build an automated job change alert that monitors your past prospects and fires a Slack notification the moment someone in your watch list changes roles — without paying for ZoomInfo or LinkedIn Sales Navigator.


What the Paid Tools Charge

The commercial solutions for this problem are expensive:

  • ZoomInfo — starts at $15,000+/year for team plans. Includes job change alerts, but bundled into a platform most SMBs can't justify
  • LinkedIn Sales Navigator — $79–$150/user/month. Job change notifications exist but lag significantly and require daily manual checking
  • Lusha — $39–$79/user/month per seat for intent signals including job changes

All three require annual contracts, seat minimums, or both. For a team of five reps, you're looking at $5,000–$75,000/year just for contact intelligence.

The DIY version costs $3–$8/month.


The Architecture

The system has four parts:

  1. Google Sheets watch list — a spreadsheet with one row per contact: name, linkedinUrl, lastKnownTitle, lastKnownCompany, lastChecked
  2. Apify linkedin-job-scraper — runs against each LinkedIn profile URL and returns current title and company
  3. Node.js diff logic — compares current vs stored role; flags any change
  4. Slack alert + CRM write-back — notifies your team and updates the contact record automatically

You run the full pipeline on a daily schedule via cron or Apify's built-in scheduler.


Implementation

Step 1 — Set Up the Google Sheets Watch List

Create a Google Sheet with these columns:

| name | linkedinUrl | lastKnownTitle | lastKnownCompany | lastChecked | jobChangeDetected |
Enter fullscreen mode Exit fullscreen mode

Each row is a past prospect you want to monitor. Populate lastKnownTitle and lastKnownCompany from your CRM at setup time.

Enable the Google Sheets API in your Google Cloud project and download service account credentials as credentials.json.

Step 2 — Load the Watch List

const { google } = require('googleapis');
const sheets = google.sheets('v4');

async function getWatchList(auth, spreadsheetId, range) {
  const res = await sheets.spreadsheets.values.get({
    auth,
    spreadsheetId,
    range,
  });
  const rows = res.data.values || [];
  return rows.slice(1).map(row => ({
    name: row[0],
    linkedinUrl: row[1],
    lastKnownTitle: row[2],
    lastKnownCompany: row[3],
    rowIndex: rows.indexOf(row) + 2,
  }));
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — Scrape Current Roles with Apify

Use the linkedin-job-scraper actor to pull current profile data for each contact.

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

const client = new ApifyClient({ token: process.env.APIFY_API_TOKEN });

async function scrapeProfiles(profileUrls) {
  const run = await client.actor('lanky_quantifier/linkedin-job-scraper').call({
    profileUrls,
    maxItems: profileUrls.length,
  });
  const { items } = await client.dataset(run.defaultDatasetId).listItems();
  return items; // [{ linkedinUrl, currentTitle, currentCompany, ... }]
}
Enter fullscreen mode Exit fullscreen mode

Batch contacts in groups of 20–50 to stay within rate limits and keep costs low (~$0.005–$0.01 per profile).

Step 4 — Diff and Detect Changes

function detectJobChanges(watchList, scrapedData) {
  const changes = [];
  for (const contact of watchList) {
    const scraped = scrapedData.find(s => s.linkedinUrl === contact.linkedinUrl);
    if (!scraped) continue;

    const titleChanged = scraped.currentTitle !== contact.lastKnownTitle;
    const companyChanged = scraped.currentCompany !== contact.lastKnownCompany;

    if (titleChanged || companyChanged) {
      changes.push({
        name: contact.name,
        linkedinUrl: contact.linkedinUrl,
        oldTitle: contact.lastKnownTitle,
        oldCompany: contact.lastKnownCompany,
        newTitle: scraped.currentTitle,
        newCompany: scraped.currentCompany,
        rowIndex: contact.rowIndex,
      });
    }
  }
  return changes;
}
Enter fullscreen mode Exit fullscreen mode

Slack Alert on Job Change

When a change is detected, send a Slack notification with enough context to act immediately:

const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);

async function notifySlack(change) {
  const reEngagementTemplate = `Hi ${change.name.split(' ')[0]}, congrats on the new role at ${change.newCompany}! We worked together previously and I'd love to reconnect — happy to show you what we've built since then.`;

  await slack.chat.postMessage({
    channel: '#job-change-alerts',
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*🚨 Job Change Detected: ${change.name}*`,
        },
      },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*Was:*\n${change.oldTitle} @ ${change.oldCompany}` },
          { type: 'mrkdwn', text: `*Now:*\n${change.newTitle} @ ${change.newCompany}` },
        ],
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Suggested message:*\n_${reEngagementTemplate}_`,
        },
      },
      {
        type: 'actions',
        elements: [
          {
            type: 'button',
            text: { type: 'plain_text', text: 'View LinkedIn Profile' },
            url: change.linkedinUrl,
          },
        ],
      },
    ],
  });
}
Enter fullscreen mode Exit fullscreen mode

The alert lands in #job-change-alerts with the old role, new role, and a ready-to-use re-engagement message. Your rep clicks the LinkedIn button, personalizes the message, and sends it — no research required.


CRM Write-Back

After alerting, update the contact record in HubSpot or Pipedrive automatically so your CRM doesn't stay stale.

HubSpot:

const axios = require('axios');

async function updateHubSpotContact(email, newTitle, newCompany) {
  await axios.patch(
    `https://api.hubapi.com/crm/v3/objects/contacts/${email}?idProperty=email`,
    {
      properties: {
        jobtitle: newTitle,
        company: newCompany,
        hs_lead_status: 'IN_PROGRESS',
      },
    },
    { headers: { Authorization: `Bearer ${process.env.HUBSPOT_API_KEY}` } }
  );

  // Create a follow-up task
  await axios.post(
    'https://api.hubapi.com/crm/v3/objects/tasks',
    {
      properties: {
        hs_task_subject: `Re-engage ${newTitle} at ${newCompany}`,
        hs_task_priority: 'HIGH',
        hs_task_type: 'CALL',
        hs_timestamp: Date.now() + 24 * 60 * 60 * 1000, // 24h from now
      },
    },
    { headers: { Authorization: `Bearer ${process.env.HUBSPOT_API_KEY}` } }
  );
}
Enter fullscreen mode Exit fullscreen mode

The task auto-creates with HIGH priority and a 24-hour due date — it shows up in the rep's queue the next morning.


Cost Breakdown

Component Cost
Apify linkedin-job-scraper ~$0.005–$0.01/profile
500 contacts checked daily ~$2.50–$5/month
Google Sheets API Free
Slack API Free
HubSpot/Pipedrive API Free (included in plan)
Total $3–$8/month

Compare that to $15,000+/year for ZoomInfo or $79–$150/user/month for Sales Navigator. For a five-person team, this system replaces $5,000–$9,000/year in tool spend.


What You Get

  • Daily automated scans across your entire past-prospect watch list
  • Instant Slack alert the moment any contact changes roles — with a suggested re-engagement message
  • CRM updated automatically — no stale contact records
  • Follow-up task created — shows up in the rep's queue the next day
  • Zero manual work after setup

Job changes are the warmest re-engagement signal you'll ever get. Most teams let them slip by. This system makes sure you never miss one.

The Apify actor that powers it — linkedin-job-scraper — runs with a 90%+ success rate and returns structured profile data per run. The full pipeline runs in under 5 minutes for 500 contacts.

Build this once and your cold list starts working for you automatically.

Top comments (0)