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

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

Why Hiring Spikes Are One of the Best B2B Buying Signals

The Architecture

Step 1: Set Up Your Target Account List in Google Sheets

Step 2: Install Dependencies

Step 3: Load Target Accounts From Google Sheets

Step 4: Scrape Current Open Role Count With Apify

Step 5: Detect Hiring Spikes

Step 6: Send Slack Alert When Spike Detected

Step 7: Write Back to HubSpot

Step 8: Main Loop

What the Slack Alert Looks Like

Cost Breakdown

What You Ship 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. 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: 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. When a spike is detected, a Slack alert fires and the account gets tagged in HubSpot/Pipedrive with a high-priority outreach task. Create a Google Sheet with these columns: baselineRoleCount is the role count when you first added the account. The script updates lastChecked on each run and logs the delta. When a spike is detected, update the HubSpot company record and create an outreach task for the AE. Schedule this with node-cron or as a GitHub Action on a weekly cadence: When a target account crosses the spike threshold, your team sees: The AE gets a HIGH-priority HubSpot task in their queue within seconds. 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. 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 500;">npm init -y -weight: 500;">npm -weight: 500;">install googleapis apify-client axios dotenv -weight: 500;">npm init -y -weight: 500;">npm -weight: 500;">install googleapis apify-client axios dotenv -weight: 500;">npm init -y -weight: 500;">npm -weight: 500;">install googleapis apify-client axios dotenv APIFY_API_TOKEN=your_apify_token GOOGLE_SHEETS_ID=your_sheet_id GOOGLE_SERVICE_ACCOUNT_JSON=./-weight: 500;">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 APIFY_API_TOKEN=your_apify_token GOOGLE_SHEETS_ID=your_sheet_id GOOGLE_SERVICE_ACCOUNT_JSON=./-weight: 500;">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 APIFY_API_TOKEN=your_apify_token GOOGLE_SHEETS_ID=your_sheet_id GOOGLE_SERVICE_ACCOUNT_JSON=./-weight: 500;">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 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, })); } 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, })); } 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, })); } 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), }; } 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), }; } 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), }; } 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, }; } 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, }; } 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, }; } 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); } 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); } 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); } 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 } ); } 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 } ); } 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 } ); } 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(); 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(); 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(); # .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: -weight: 500;">npm -weight: 500;">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 }} # .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: -weight: 500;">npm -weight: 500;">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 }} # .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: -weight: 500;">npm -weight: 500;">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 }} 🚀 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 →] 🚀 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 →] 🚀 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 →] - 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. - 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 - A Google Sheets-backed target account list your whole team can -weight: 500;">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