DEV Community

Vhub Systems
Vhub Systems

Posted on

How to Build an Account Expansion Alert That Notifies You When a Target Company Starts Hiring Fast

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:

  1. They have budget. Hiring is expensive. A company posting 20+ roles has approved headcount.
  2. They're in growth mode. Growth mode means new tools, new processes, new vendor evaluations.
  3. 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:

  1. Target account list — Google Sheets with company name, LinkedIn company jobs URL, and baseline open role count
  2. Apify linkedin-job-scraper — runs against each company's jobs URL to get current open role count and job titles
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
  }));
}
Enter fullscreen mode Exit fullscreen mode

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),
  };
}
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 }
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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 →]
Enter fullscreen mode Exit fullscreen mode

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)