Track new roles at target companies daily with a schedule and a webhook
The job board Actors return the roles open right now and keep no history, so “what’s new” means comparing today’s run with yesterday’s. This guide sets that up with an Apify schedule, a webhook and a short script. It works the same with the Greenhouse, Lever and Ashby Actors, whose records all carry a stable jobId.
You could build the same thing on the free job board APIs, with your own cron job and no Actor fee. The Actors save you the per-platform parsing and give you one record format, but you still run the comparison.
1. Schedule a daily run
Section titled “1. Schedule a daily run”- In Apify Console, open Schedules and click Create new.
- In the Schedule setup card, set when it runs, for example
0 7 * * *for 07:00 every day. - Click Add, choose the Actor, and give it your input:
{ "companies": ["discord", "figma"], "keyword": "engineer" }- Click Enable. New schedules start disabled.
Keep the filters tight: every run charges for every job it stores, not only the new ones (cost below).
2. Call your script when a run finishes
Section titled “2. Call your script when a run finishes”Open the Actor in Apify Console, go to its Integrations tab and add a webhook:
- Event:
ACTOR.RUN.SUCCEEDED - URL: your server’s address, ending in
/apify/and a long random secret
Apify then sends a POST with the run under resource, including the defaultDatasetId the script reads. The webhook fires for every run of that Actor in your account. If you also run it by hand with other input, save the input as an Apify task and attach the schedule and webhook to the task instead.
3. Compare runs by job ID
Section titled “3. Compare runs by job ID”This script has no dependencies and needs Node 18 or newer. It keeps the jobs it has seen in a file and prints what changed:
// tracker.mjs: run with APIFY_TOKEN and WEBHOOK_SECRET set, behind an HTTPS address Apify can reach.import { existsSync, readFileSync, writeFileSync } from 'node:fs';import { createServer } from 'node:http';
const SEEN_FILE = 'seen-jobs.json';
async function handleRun(run) { const url = `https://api.apify.com/v2/datasets/${run.defaultDatasetId}/items?fields=jobId,title,company,url,error`; const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.APIFY_TOKEN}` } }); if (!res.ok) throw new Error(`dataset read failed: HTTP ${res.status}`); const items = await res.json(); const jobs = items.filter((i) => i.jobId); const failedBoards = items.filter((i) => i.error);
const seen = existsSync(SEEN_FILE) ? JSON.parse(readFileSync(SEEN_FILE, 'utf8')) : {}; const open = Object.fromEntries(jobs.map((j) => [j.jobId, j])); for (const j of jobs) if (!seen[j.jobId]) console.log(`NEW ${j.company}: ${j.title} ${j.url}`); // A board that failed this run says nothing about its jobs, so only call roles closed after a clean run. if (!failedBoards.length) for (const j of Object.values(seen)) if (!open[j.jobId]) console.log(`CLOSED ${j.company}: ${j.title}`); writeFileSync(SEEN_FILE, JSON.stringify(failedBoards.length ? { ...seen, ...open } : open));}
createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== `/apify/${process.env.WEBHOOK_SECRET}`) return res.writeHead(404).end(); let body = ''; for await (const chunk of req) body += chunk; res.end('ok'); const { eventType, resource } = JSON.parse(body); if (eventType === 'ACTOR.RUN.SUCCEEDED') await handleRun(resource).catch(console.error);}).listen(process.env.PORT ?? 3000);APIFY_TOKEN=... WEBHOOK_SECRET=... node tracker.mjsWe ran it against a local stand-in for the dataset API, covering new jobs, closed jobs, a failed board and a failed run. It hasn’t run against Apify itself. Three things to know:
- The first run reports every job as new, because nothing has been seen yet.
- A board that fails gets an item with an
errorfield, and the script then skips closed-role reporting for that run, so an outage doesn’t look like mass closures. - Replace
console.logwith whatever should notify you, such as a Slack incoming webhook.
No server? Use the Actor’s n8n, Make and Zapier steps instead, or the no-code spreadsheet path.
What it costs
Section titled “What it costs”You pay for every job stored in every run. Discord and Figma had 172 open jobs on 2026-11-05, so a daily run with no filter stores about 5,160 jobs a month: $5.16 at $1.00 per 1,000 jobs, plus a small start fee per run. Filters cut that directly, because filtered-out jobs aren’t charged. Maximum cost per run caps a run.
