# Job Delta — Multi-ATS Job Change Monitor (`soilair/job-delta-cross-ats-monitor`) Actor

Monitor public Greenhouse, Lever, Ashby, Recruitee, and Workable job boards for new, removed, reopened, and changed jobs, hiring signals, and strict cross-ATS duplicate matches. Unofficial community tool.

- **URL**: https://apify.com/soilair/job-delta-cross-ats-monitor.md
- **Developed by:** [Salih Can Kurnaz](https://apify.com/soilair) (community)
- **Categories:** Jobs, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 job changes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Job Delta — Multi-ATS Job Change Monitor

Monitor public **Greenhouse, Lever, Ashby, Recruitee, and Workable** job boards over time and receive only meaningful hiring changes instead of downloading the same job list on every run.

> **Unofficial community tool.** Job Delta is not affiliated with or endorsed by Greenhouse, Lever, Ashby, Recruitee, or Workable. It reads public job-board data only.

### Quick start

#### 1. Add public board URLs or slugs

You can either select the ATS and enter its stable slug, or choose `source: "auto"` and paste a full supported public board URL.

| ATS | Public URL example | Explicit `source` | Normalized `boardToken` |
|---|---|---|---|
| Greenhouse | `https://job-boards.greenhouse.io/acme` | `greenhouse` | `acme` |
| Lever | `https://jobs.lever.co/acme` | `lever` | `acme` |
| Ashby | `https://jobs.ashbyhq.com/acme` | `ashby` | `acme` |
| Recruitee | `https://acme.recruitee.com/o/platform-engineer` | `recruitee` | `acme` |
| Workable | `https://apply.workable.com/acme/` | `workable` | `acme` |

Full URLs are accepted only for supported allow-listed ATS hosts. Job Delta never fetches an arbitrary user-supplied host.

#### 2. Run a silent baseline

```json
{
  "monitorId": "competitor-hiring-watch",
  "companies": [
    {
      "name": "Greenhouse",
      "source": "auto",
      "boardToken": "https://job-boards.greenhouse.io/greenhouse"
    },
    {
      "name": "Highspot",
      "source": "auto",
      "boardToken": "https://jobs.lever.co/highspot"
    },
    {
      "name": "Ashby",
      "source": "auto",
      "boardToken": "https://jobs.ashbyhq.com/ashby"
    }
  ],
  "initialRunMode": "baseline_only",
  "includeDescriptions": true,
  "includeSalaryDetails": false,
  "emitDuplicateGroups": true,
  "removalConfirmationRuns": 2,
  "hiringChangeMinJobs": 5,
  "hiringChangeMinPercent": 25,
  "rejectSuspiciousEmptyBoards": true
}
```

The first `baseline_only` run records existing jobs silently. It does not report every current job as newly added.

#### 3. Schedule repeated runs

Save the input as an Apify Task and run it every three or six hours. Keep the same `monitorId`; changing it creates a separate monitor with separate state.

### What it detects

- New jobs (`job_added`)
- Confirmed removals (`job_removed`)
- Reopened jobs (`job_reopened`)
- Title, location, department and employment-type changes
- Remote-status and description changes
- Salary-range changes when public salary data is observed
- Hiring surges and slowdowns
- Strict advisory duplicate matches across different ATS sources
- A free `run_summary` with source health, request counts and billing status

### Why Job Delta

Job-board scrapers normally return complete snapshots. Job Delta keeps persistent state by `monitorId`, compares successful runs, and emits only changes. A job must be absent from multiple successful scans before removal is confirmed, reducing false alerts from temporary source problems.

### Common use cases

- Competitor hiring intelligence
- Recruiting-market monitoring
- Portfolio-company workforce monitoring
- Sales signals from hiring surges
- Remote-policy, salary, location and department-change alerts
- Event-driven job aggregation without repeated full snapshots

### Supported sources

| Source | Public endpoint behavior | Authentication |
|---|---|---|
| Greenhouse | Public board jobs API; optional salary-detail requests | None |
| Lever | Public postings API | None |
| Ashby | Public Job Postings API; optional compensation payload | None |
| Recruitee | Public Careers Site `/api/offers/` endpoint | None |
| Workable | Public published-jobs account endpoint | None |

No candidate data, ATS administrator account, browser automation, residential proxy, CAPTCHA solver, GPU, or LLM is required.

### Example event

```json
{
  "eventType": "location_changed",
  "company": "Acme",
  "source": "ashby",
  "boardToken": "Acme",
  "jobKey": "ashby:Acme:12345",
  "detectedAt": "2026-08-03T00:00:00Z",
  "before": {"location": "London"},
  "after": {"location": "Remote - Europe"},
  "details": {
    "sourceJobId": "12345",
    "title": "Platform Engineer",
    "applicationUrl": "https://jobs.ashbyhq.com/Acme/12345/application"
  }
}
```

### Reliability controls

- Persistent state separated by `monitorId`, source and normalized board token
- Silent baseline by default
- Configurable successful-run removal confirmation
- Suspicious-empty-board protection
- Per-company failure isolation
- HTTP retry, timeout, request and byte accounting per ATS
- Removed-job tombstones for reopening detection
- Duplicate-group replay suppression
- Allow-listed ATS URL parsing to prevent arbitrary-host requests
- Budget-aware PPE delivery: state advances only after all detected paid events are delivered

### Salary behavior

- Greenhouse: `includeSalaryDetails: true` adds one optional public detail request per current job.
- Ashby: the same option requests the public compensation payload.
- Lever, Recruitee, and Workable: publicly exposed salary fields are parsed without private credentials.
- A source that does not expose salary data is not treated as having a zero salary.

### Billing events

Job Delta uses explicit pay-per-event value events:

- `board-initialized` — a public board baseline is created
- `job-change` — a lifecycle or field-change event is delivered
- `hiring-signal` — a surge or slowdown is delivered
- `duplicate-match` — a new strict cross-ATS duplicate group is delivered

`run_summary`, failed sources, unchanged runs and replayed events are not explicitly charged. The synthetic `apify-default-dataset-item` event is disabled in the current Store pricing, so dataset rows are not charged separately.

### API quick start

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run_input = {
    "monitorId": "competitor-hiring-watch",
    "companies": [
        {
            "name": "Ashby",
            "source": "auto",
            "boardToken": "https://jobs.ashbyhq.com/ashby",
        },
        {
            "name": "TransPerfect",
            "source": "auto",
            "boardToken": "https://transperfect.recruitee.com/o/software-engineer",
        },
    ],
    "initialRunMode": "baseline_only",
}
run = client.actor("soilair/job-delta-cross-ats-monitor").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const input = {
  monitorId: 'competitor-hiring-watch',
  companies: [
    { name: 'Ashby', source: 'ashby', boardToken: 'ashby' },
    { name: 'TP-Link USA', source: 'workable', boardToken: 'tp-link-usa-corp' },
  ],
  initialRunMode: 'baseline_only',
};
const run = await client.actor('soilair/job-delta-cross-ats-monitor').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.dir(items, { depth: null });
```

### Troubleshooting

- **Every current job was emitted as new:** use `initialRunMode: "baseline_only"` on the first run.
- **The monitor appears to have forgotten history:** keep the same `monitorId` and do not enable `resetMonitor`.
- **A board returns zero jobs unexpectedly:** leave `rejectSuspiciousEmptyBoards` enabled to preserve the prior state.
- **A removal did not appear immediately:** the default requires two successful missing scans before `job_removed`.
- **Automatic source detection failed:** use a full supported board URL, or select the ATS explicitly and provide only its slug.
- **Duplicate signals are missing:** enable descriptions and `emitDuplicateGroups`; matching is intentionally strict and advisory.

### Important behavior

- Existing Greenhouse and Lever state keys remain unchanged in v1.8.
- `resetMonitor: true` intentionally recreates board baselines.
- Direct-link-only Ashby postings with `isListed: false` are not included.
- Duplicate matching is strict and advisory; it does not delete or mutate source jobs.
- If every company source fails, the Actor fails after writing a diagnostic run summary.
- If the run budget cannot cover all detected events, Job Delta does not advance state, so unpaid changes are not silently lost.

# Actor input Schema

## `monitorId` (type: `string`):

Keep this stable across scheduled runs. A different value creates separate state.

## `companies` (type: `array`):

Add public ATS boards. Select the ATS and enter its slug, or choose auto and paste a full supported board URL.

## `initialRunMode` (type: `string`):

Create a silent baseline or emit every current job as job\_added.

## `includeDescriptions` (type: `boolean`):

Fetch public descriptions for hashing, remote detection, and strict cross-ATS duplicate signals.

## `includeSalaryDetails` (type: `boolean`):

For Greenhouse, adds optional public detail requests; for Ashby, requests compensation. Other public salary fields are parsed when present.

## `emitDuplicateGroups` (type: `boolean`):

Emit an advisory duplicate\_job\_merged event when near-identical public jobs appear on different ATS boards.

## `removalConfirmationRuns` (type: `integer`):

A job must be missing from this many successful scans before job\_removed is emitted.

## `hiringChangeMinJobs` (type: `integer`):

Minimum confirmed active-job count change for hiring\_surge or hiring\_slowdown.

## `hiringChangeMinPercent` (type: `number`):

Minimum percentage change required together with the job-count threshold.

## `rejectSuspiciousEmptyBoards` (type: `boolean`):

If a board previously contained jobs and suddenly returns zero, preserve state and report a source failure.

## `maxConcurrency` (type: `integer`):

Maximum number of company-board requests processed concurrently.

## `requestTimeoutSecs` (type: `number`):

Maximum time allowed for each public ATS HTTP request before retry or failure.

## `resetMonitor` (type: `boolean`):

Ignore prior snapshots and recreate baselines for this run.

## Actor input object example

```json
{
  "monitorId": "my-job-delta-monitor",
  "companies": [
    {
      "name": "Greenhouse",
      "source": "greenhouse",
      "boardToken": "greenhouse"
    },
    {
      "name": "Highspot",
      "source": "lever",
      "boardToken": "highspot"
    },
    {
      "name": "Ashby",
      "source": "ashby",
      "boardToken": "ashby"
    }
  ],
  "initialRunMode": "baseline_only",
  "includeDescriptions": true,
  "includeSalaryDetails": false,
  "emitDuplicateGroups": true,
  "removalConfirmationRuns": 2,
  "hiringChangeMinJobs": 5,
  "hiringChangeMinPercent": 25,
  "rejectSuspiciousEmptyBoards": true,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 30,
  "resetMonitor": false
}
```

# Actor output Schema

## `events` (type: `string`):

Default dataset rendered with the event-oriented view.

## `allRows` (type: `string`):

Every dataset row, including run\_summary.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("soilair/job-delta-cross-ats-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("soilair/job-delta-cross-ats-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call soilair/job-delta-cross-ats-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=soilair/job-delta-cross-ats-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WaqesKXfk2BgBX4oc/builds/yCjCHwqRLicXVYVQ3/openapi.json
