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:
-
Google Sheets watch list — a spreadsheet with one row per contact:
name,linkedinUrl,lastKnownTitle,lastKnownCompany,lastChecked -
Apify
linkedin-job-scraper— runs against each LinkedIn profile URL and returns current title and company - Node.js diff logic — compares current vs stored role; flags any change
- 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 |
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,
}));
}
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, ... }]
}
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;
}
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,
},
],
},
],
});
}
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}` } }
);
}
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)