> ## Documentation Index
> Fetch the complete documentation index at: https://rocketyard.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These Actors run on the Apify platform and charge per result. Call one with POST https://api.apify.com/v2/acts/rocketyard~<actor-id>/run-sync-get-dataset-items and the header Authorization: Bearer <APIFY_TOKEN>, or connect Apify's MCP server at https://mcp.apify.com?tools=rocketyard/<actor-id>.

# Track new roles at target companies daily with a schedule and a webhook

> Find out when target companies open or close a role: a daily Apify schedule, a webhook and a short script that compares runs by job ID.

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](https://rocketyard.dev/greenhouse-jobs-scraper.md), [Lever](https://rocketyard.dev/lever-jobs-scraper.md) and [Ashby](https://rocketyard.dev/ashby-jobs-scraper.md) 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

1. In Apify Console, open **Schedules** and click **Create new**.
2. In the **Schedule setup** card, set when it runs, for example `0 7 * * *` for 07:00 every day.
3. Click **Add**, choose the Actor, and give it your input:

```json theme={null}
{ "companies": ["discord", "figma"], "keyword": "engineer" }
```

4. 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](#what-it-costs)).

## 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

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:

```js theme={null}
// 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);
```

```bash theme={null}
APIFY_TOKEN=... WEBHOOK_SECRET=... node tracker.mjs
```

We 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 `error` field, and the script then skips closed-role reporting for that run, so an outage doesn't look like mass closures.
* Replace `console.log` with whatever should notify you, such as a Slack incoming webhook.

No server? Use the Actor's [n8n, Make and Zapier steps](https://rocketyard.dev/greenhouse-jobs-scraper.md#automation) instead, or the [no-code spreadsheet path](https://rocketyard.dev/hiring-signals-from-job-descriptions.md).

## 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.

> [card] [Run Greenhouse Jobs API & Scraper on Apify](https://apify.com/rocketyard/greenhouse-jobs-scraper)
>
> \$1.00 per 1,000 jobs, no subscription. You run it with your own Apify account.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.