A company that posts 2 open roles in January and 28 in March is not the same company. Budget just opened up. Headcount is growing. A buying decision is somewhere in that plan. For the rep who spots it first, it's the most actionable signal in B2B sales.
The problem: most teams find out about it months later — or never. A rep manually checks LinkedIn jobs for a handful of accounts once in a while. Nothing fires when a target account triples its open role count. No CRM field tracks headcount velocity. The window closes without anyone noticing it opened.
This post shows how to build a simple hiring spike monitor that watches your target accounts, detects when open role count surges past a threshold, sends a Slack alert with context, and tags the account in your CRM.
Why Hiring Spikes Are One of the Best B2B Buying Signals
When a company's job posting count spikes — especially in functions like engineering, ops, or the department your product serves — it tells you three things at once:
- They have budget. Hiring is expensive. A company posting 20+ roles has approved headcount.
- They're in growth mode. Growth mode means new tools, new processes, new vendor evaluations.
- The timing is right. Decisions get made during hiring surges, not after the dust settles.
Enterprise intent data platforms sell exactly this signal. Bombora charges $15,000–$40,000/year. 6sense starts at $60,000+/year. ZoomInfo TalentOS runs $15,000+/year. What they're actually doing: watching job boards for your target accounts and surfacing the spikes. The raw data is public. The infrastructure to collect it is the only thing between you and the same signal.
You can build that infrastructure for $4–$9/month.
The Architecture
Three components:
- Target account list — Google Sheets with company name, LinkedIn company jobs URL, and baseline open role count
-
Apify
linkedin-job-scraper— runs against each company's jobs URL to get current open role count and job titles - Delta logic — flags any account where current count ≥ 2× baseline, or where 10+ new roles appeared since last check
When a spike is detected, a Slack alert fires and the account gets tagged in HubSpot/Pipedrive with a high-priority outreach task.
Step 1: Set Up Your Target Account List in Google Sheets
Create a Google Sheet with these columns:
| companyName | linkedinJobsUrl | baselineRoleCount | lastChecked |
|---|---|---|---|
| Acme Corp | https://www.linkedin.com/company/acme-corp/jobs | 4 | 2026-03-01 |
| Beta Inc | https://www.linkedin.com/company/beta-inc/jobs | 11 | 2026-03-01 |
baselineRoleCount is the role count when you first added the account. The script updates lastChecked on each run and logs the delta.
Step 2: Install Dependencies
npm init -y
npm install googleapis apify-client axios dotenv
Create a .env file:
APIFY_API_TOKEN=your_apify_token
GOOGLE_SHEETS_ID=your_sheet_id
GOOGLE_SERVICE_ACCOUNT_JSON=./service-account.json
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz
HUBSPOT_API_KEY=your_hubspot_key
SPIKE_THRESHOLD_MULTIPLIER=2
SPIKE_THRESHOLD_ABSOLUTE=10
Step 3: Load Target Accounts From Google Sheets
const { google } = require('googleapis');
const path = require('path');
async function loadTargetAccounts() {
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve(process.env.GOOGLE_SERVICE_ACCOUNT_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.GOOGLE_SHEETS_ID,
range: 'Sheet1!A2:D',
});
return (res.data.values || []).map((row, idx) => ({
rowIndex: idx + 2,
companyName: row[0],
linkedinJobsUrl: row[1],
baselineRoleCount: parseInt(row[2], 10) || 0,
lastChecked: row[3] || null,
}));
}
Step 4: Scrape Current Open Role Count With Apify
const { ApifyClient } = require('apify-client');
const apify = new ApifyClient({ token: process.env.APIFY_API_TOKEN });
async function getCurrentRoleCount(linkedinJobsUrl) {
const run = await apify.actor('lanky_quantifier/linkedin-job-scraper').call({
startUrls: [{ url: linkedinJobsUrl }],
maxItems: 100,
});
const { items } = await apify.dataset(run.defaultDatasetId).listItems();
const roleTitles = items.map(item => item.title || item.jobTitle || '').filter(Boolean);
return {
count: items.length,
topTitles: roleTitles.slice(0, 3),
};
}
Step 5: Detect Hiring Spikes
function detectSpike(account, currentCount) {
const { baselineRoleCount } = account;
const multiplierThreshold = parseFloat(process.env.SPIKE_THRESHOLD_MULTIPLIER || 2);
const absoluteThreshold = parseInt(process.env.SPIKE_THRESHOLD_ABSOLUTE || 10, 10);
const delta = currentCount - baselineRoleCount;
const multiplier = baselineRoleCount > 0 ? currentCount / baselineRoleCount : Infinity;
const isSpike =
(multiplier >= multiplierThreshold && delta >= 3) ||
delta >= absoluteThreshold;
return {
isSpike,
delta,
multiplier: baselineRoleCount > 0 ? multiplier.toFixed(1) : 'N/A',
pctGrowth: baselineRoleCount > 0
? Math.round(((currentCount - baselineRoleCount) / baselineRoleCount) * 100)
: 100,
};
}
Step 6: Send Slack Alert When Spike Detected
const axios = require('axios');
async function sendSpikeAlert(account, currentCount, topTitles, spikeInfo) {
const message = {
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `🚀 Hiring Spike Detected: ${account.companyName}`,
},
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Baseline Roles:*\n${account.baselineRoleCount}` },
{ type: 'mrkdwn', text: `*Current Open Roles:*\n${currentCount}` },
{ type: 'mrkdwn', text: `*Delta:*\n+${spikeInfo.delta} roles` },
{ type: 'mrkdwn', text: `*Growth:*\n${spikeInfo.pctGrowth}% (${spikeInfo.multiplier}×)` },
],
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Top new roles:* ${topTitles.length > 0 ? topTitles.join(' · ') : 'N/A'}`,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View Open Roles' },
url: account.linkedinJobsUrl,
style: 'primary',
},
],
},
],
};
await axios.post(process.env.SLACK_WEBHOOK_URL, message);
}
Step 7: Write Back to HubSpot
When a spike is detected, update the HubSpot company record and create an outreach task for the AE.
async function updateHubSpotAccount(companyName, currentRoleCount) {
const headers = {
Authorization: `Bearer ${process.env.HUBSPOT_API_KEY}`,
'Content-Type': 'application/json',
};
// Search for the company record
const searchRes = await axios.post(
'https://api.hubapi.com/crm/v3/objects/companies/search',
{
filterGroups: [{
filters: [{
propertyName: 'name',
operator: 'CONTAINS_TOKEN',
value: companyName,
}],
}],
properties: ['name', 'hs_object_id'],
},
{ headers }
);
const company = searchRes.data.results[0];
if (!company) return;
const companyId = company.id;
// Update company properties
await axios.patch(
`https://api.hubapi.com/crm/v3/objects/companies/${companyId}`,
{
properties: {
open_role_count: String(currentRoleCount),
account_signal_tag: 'Hiring Spike',
signal_detected_date: new Date().toISOString().split('T')[0],
},
},
{ headers }
);
// Create high-priority outreach task
await axios.post(
'https://api.hubapi.com/crm/v3/objects/tasks',
{
properties: {
hs_task_subject: `[Hiring Spike] Reach out to ${companyName} — ${currentRoleCount} open roles detected`,
hs_task_priority: 'HIGH',
hs_task_type: 'TODO',
hs_timestamp: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
hs_task_body: `Account shows hiring spike: ${currentRoleCount} open roles. Reach out now — growth mode = buying window.`,
},
associations: [{
to: { id: companyId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 192 }],
}],
},
{ headers }
);
}
Step 8: Main Loop
require('dotenv').config();
async function main() {
const accounts = await loadTargetAccounts();
console.log(`Checking ${accounts.length} target accounts...`);
for (const account of accounts) {
try {
console.log(`Scraping: ${account.companyName}`);
const { count: currentCount, topTitles } = await getCurrentRoleCount(account.linkedinJobsUrl);
const spikeInfo = detectSpike(account, currentCount);
if (spikeInfo.isSpike) {
console.log(`SPIKE: ${account.companyName} — ${account.baselineRoleCount} → ${currentCount} roles`);
await sendSpikeAlert(account, currentCount, topTitles, spikeInfo);
await updateHubSpotAccount(account.companyName, currentCount);
} else {
console.log(`No spike: ${account.companyName} (${currentCount} roles, baseline ${account.baselineRoleCount})`);
}
// Add a delay to avoid rate limits
await new Promise(r => setTimeout(r, 2000));
} catch (err) {
console.error(`Error checking ${account.companyName}:`, err.message);
}
}
console.log('Done.');
}
main();
Schedule this with node-cron or as a GitHub Action on a weekly cadence:
# .github/workflows/hiring-spike-check.yml
name: Hiring Spike Monitor
on:
schedule:
- cron: '0 7 * * 1' # Every Monday at 7am
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- run: node main.js
env:
APIFY_API_TOKEN: ${{ secrets.APIFY_API_TOKEN }}
GOOGLE_SHEETS_ID: ${{ secrets.GOOGLE_SHEETS_ID }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
HUBSPOT_API_KEY: ${{ secrets.HUBSPOT_API_KEY }}
What the Slack Alert Looks Like
When a target account crosses the spike threshold, your team sees:
🚀 Hiring Spike Detected: [Company Name]
Baseline Roles: 4 Current Open Roles: 27
Delta: +23 roles Growth: 575% (6.8×)
Top new roles: Head of Engineering · Senior Product Manager · DevOps Lead
[View Open Roles →]
The AE gets a HIGH-priority HubSpot task in their queue within seconds.
Cost Breakdown
| Component | Cost |
|---|---|
Apify linkedin-job-scraper (~$0.05–0.10/company/run × 50 accounts/week) |
$1–$2/month |
| Google Sheets API | Free |
| Slack webhooks | Free |
| HubSpot API (Starter tier) | Free up to CRM limit |
| GitHub Actions (public repo) | Free |
| Total | $1–$3/month |
Compare that to: Bombora ($15,000–$40,000/year), 6sense ($60,000+/year), ZoomInfo TalentOS ($15,000+/year). You're detecting the same hiring spike signal for $4–$9/month including compute overhead.
What You Ship
- A Google Sheets-backed target account list your whole team can update
- A weekly automated scrape that compares current vs. baseline role count per company
- A Slack alert with company name, delta, growth percentage, and top 3 new role titles
- A HubSpot company tag ("Hiring Spike") and a HIGH-priority task automatically created for the AE
- A GitHub Actions schedule that runs every Monday morning before stand-up
The reps who time their outreach to growth signals — instead of spraying the same message to everyone — close faster. This system makes that timing automatic.
Top comments (0)