# ATS Job Monitor — Greenhouse & Lever (`synthex-labs/ats-job-monitor`) Actor

Track new, updated and no-longer-listed jobs across Greenhouse and Lever boards. Persistent history, filters, salary fields and JSON/CSV exports.

- **URL**: https://apify.com/synthex-labs/ats-job-monitor.md
- **Developed by:** [Dawid](https://apify.com/synthex-labs) (community)
- **Categories:** Jobs, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 complete board scans

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ATS Job Monitor — Greenhouse & Lever

**Turn public career boards into a repeatable hiring-change feed.** Collect jobs from specified Greenhouse and Lever company boards, keep a baseline between runs and see what was first seen, added, updated or no longer listed. Export clean JSON and CSV for recruiting research, job-board feeds or hiring-signal workflows.

No login, candidate database, paid AI API or browser automation is required. The Actor reads the official public Job Board/Postings APIs. It does not apply to jobs, access applicant records or infer company growth from job counts.

### Quick start

1. Enter up to five public company board URLs. The prefilled example uses Figma on Greenhouse and Zoox on Lever.
2. Set **Monitor ID**, e.g. `my-hiring-watch`. Reuse it on future runs in the same Apify account. Leave it unset for a one-off extraction.
3. Run once to initialize the baseline. The first jobs are labelled **first_seen**, not “newly published”.
4. Run again sequentially with the same Monitor ID to see **new**, **updated**, **unchanged** and **no_longer_listed** jobs.
5. Download the full JSON report, Jobs CSV or Changes CSV from Output. Enable **Return only changes** for a smaller change feed.

To repeat automatically, create your own Apify task/schedule with these inputs. This Actor does not create schedules or send email/Slack messages. Platform webhooks can be configured separately in your account.

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/figma",
    "https://jobs.lever.co/zoox"
  ],
  "monitorId": "my-hiring-watch",
  "onlyChanges": true,
  "keywords": ["engineer", "data"],
  "locations": ["remote", "poland"]
}
```

### Supported sources

| Provider | Accepted board input | Access |
| --- | --- | --- |
| Greenhouse | `https://job-boards.greenhouse.io/company`, `https://boards.greenhouse.io/company`, or `greenhouse:company` | Official public Job Board API. |
| Lever (global instance) | `https://jobs.lever.co/company` or `lever:company` | Official public Postings API, with bounded pagination. |

Use a **company board**, not an individual job URL. The company token must match the real board. Custom career domains, embedded board URLs, Lever EU, Ashby, Workday, LinkedIn, global keyword discovery and private boards are not supported in this release. No claims are made about platforms outside this table.

### What you receive

Job rows include a stable `id` (`provider:board:source_id`), original source ID, company/board, title, locations, department/team, source employment/workplace type, source salary object when available, source dates, job/apply URL and description text. Descriptions are limited to 4,000 characters in reports/state; the fingerprint uses the full normalized text, so longer description changes are still detected.

Greenhouse does not provide a standard salary/workplace field in this endpoint, so these remain null; the Actor does not guess. Lever salary values are copied from the source as decimal strings with currency and interval, without currency conversion or annualization. Lever creation time is `source_created_at`, not a claim of publication time. Greenhouse `source_published_at` is used only when explicitly provided. Source timestamps may be null.

| Output | Contents |
| --- | --- |
| Default dataset | `record_type: job` rows plus explicit error rows; a summary row is written when there are no visible rows. |
| `OUTPUT` | Summary, per-board completeness/errors, counts before report filters, baseline status, warnings and visible job/change rows. |
| `JOBS-CSV` | Visible current jobs and no-longer-listed records, including statuses, dates and changed fields. |
| `CHANGES-CSV` | Same columns with unchanged jobs omitted; includes first observations. |

CSV is UTF-8 with BOM. Nested values are JSON within cells. Formula-like text is escaped for spreadsheet use. CSV snapshots are suitable for Excel import; no native XLSX workbook is generated.

### Change semantics and useful safeguards

- **first_seen**: no previous complete baseline for this board. A job might already have been published long before you started monitoring.
- **new**: source ID absent from the previous complete baseline and present now. This is new to your monitor, not proof of a new position or increased headcount.
- **updated**: normalized content changed. `changed_fields` and `previous_values` explain the difference. Source update timestamps alone do not create an update event.
- **unchanged**: same ID and content fingerprint; last-seen time advances.
- **no_longer_listed**: ID absent from a later complete public API response. It does not prove the role was filled, cancelled or expired. Its last-seen time remains the last actual observation.

Each board has a separate baseline. HTTP failures, malformed responses, duplicate source IDs, known incomplete pages, size/job limits or spending-limit failures **preserve the prior baseline** instead of treating a board as empty. A genuinely successful complete empty response can mark previous jobs as no longer listed. Partial runs expose errors and completeness; if every board fails, the run fails after writing safe diagnostics.

Lever pagination is bounded and checked for duplicate IDs and exhaustion. The upstream API is not a transactional snapshot; rapidly changing boards during pagination can still affect observations. No extractor can establish hiring intent or global completeness from a public posting list.

### Filters and persistent storage

`keywords`: up to 20 case-insensitive substrings, matched with OR against title/team/department. `locations`: up to 20 case-insensitive substrings, OR against source locations/country/workplace type. The two groups combine with AND. Updates also match previous values so a job changing out of a filter can still be reported.

Filters and `onlyChanges` affect reporting **after** comparison, not the saved scope. Changing a filter does not delete jobs from the baseline. Baselines only cover the boards you supplied; they do not discover new companies.

State lives in a named key-value store derived from Actor ID, initiating Apify user ID and Monitor ID. Different users/monitors are isolated by storage name. Retain the store to retain history; deleting it initializes a new first observation. `resetBaseline: true` explicitly resets only selected boards after successful delivery.

**Run one instance of a Monitor ID at a time.** Concurrent runs are unsupported. The Actor detects committed revision conflicts and avoids overwriting them, but this is not an atomic lock or compare-and-swap. Dataset, report and state writes are separate operations: a storage failure may leave delivered reports with an older/partially advanced baseline. Always check `baseline_status` before treating a run as a committed comparison. Retrying can repeat change rows; deduplicate downstream by stable job ID/change contents/run ID as appropriate.

### Pricing and limits

**$0.005 per Actor start + $0.02 per successfully completed board scan**, with platform usage included and no per-job or per-change fee. One board normally costs $0.025; five boards cost $0.105 at the default memory allocation. The start event is charged once per started GB of RAM, minimum one; increasing RAM above 1 GB increases the start fee.

An empty but valid complete board scan is chargeable. Invalid or incomplete board fetches are not charged the board event. Runtime validation/network failures can still incur the start fee. Filters do not reduce the board fee because the complete board must be fetched to preserve the baseline. Apify spending limits are respected; unpaid scans are not delivered or committed. Check the live Pricing tab before running.

Limits: 1–5 unique boards per run; at most 1,000 jobs per board and 3,000 overall; at most 20 MiB per API response. Larger/incomplete boards are reported as errors rather than silently truncated. Greenhouse uses one API response; Lever uses up to 11 pages, including an exhaustion check. The collection phase has a 100-second deadline. Default allocation: 256 MiB RAM, 180-second platform timeout; the Actor has a 150-second application deadline.

### Data and support

Only public job APIs are contacted, through fixed HTTPS hosts with no redirects. No external AI service, proxy subscription, account cookie, candidate contact or application-submission endpoint is used. Inputs, reports and baselines follow your Apify storage retention/access settings; you control their visibility. Descriptions can contain public contact information supplied by employers; process and share outputs appropriately.

For support, use the Actor Issues tab with a run ID, board URL and a minimal example of the problem. Do not include credentials or applicant data. This is a focused v1 product; source outages and posting-schema changes may require maintenance. Usage statistics and price comparisons in the repository research are not revenue forecasts.

# Changelog

This Actor's version history is a separate document: https://apify.com/synthex-labs/ats-job-monitor/changelog.md

# Actor input Schema

## `boards` (type: `array`):

1–5 URLs such as https://job-boards.greenhouse.io/figma or https://jobs.lever.co/zoox. provider:slug shorthand is accepted. Complete board scans only; no custom career domains or individual job URLs.

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

Reuse this identifier for persistent change detection in your own account. Run one instance of a given monitorId at a time. Leave unset for a one-off extraction.

## `onlyChanges` (type: `boolean`):

Hide unchanged jobs from reports. All jobs remain in the baseline. First observations are first_seen; they are not claims of newly published jobs.

## `keywords` (type: `array`):

Case-insensitive substring OR match in job title, team or department. Filtering does not shrink the stored baseline.

## `locations` (type: `array`):

Case-insensitive substring OR match in source locations, country or workplace type. Greenhouse remote status is not inferred.

## `resetBaseline` (type: `boolean`):

Explicitly replace the selected boards’ saved baselines after successful delivery. Requires monitorId; other boards remain unchanged.

## Actor input object example

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/figma",
    "https://jobs.lever.co/zoox"
  ],
  "monitorId": "my-hiring-watch",
  "onlyChanges": false,
  "resetBaseline": false
}
```

# Actor output Schema

## `report` (type: `string`):

No description

## `jobsCsv` (type: `string`):

No description

## `changesCsv` (type: `string`):

No description

## `dataset` (type: `string`):

No description

# 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 = {
    "boards": [
        "https://job-boards.greenhouse.io/figma",
        "https://jobs.lever.co/zoox"
    ],
    "monitorId": "my-hiring-watch"
};

// Run the Actor and wait for it to finish
const run = await client.actor("synthex-labs/ats-job-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 = {
    "boards": [
        "https://job-boards.greenhouse.io/figma",
        "https://jobs.lever.co/zoox",
    ],
    "monitorId": "my-hiring-watch",
}

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

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

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

```

## CLI example

```bash
echo '{
  "boards": [
    "https://job-boards.greenhouse.io/figma",
    "https://jobs.lever.co/zoox"
  ],
  "monitorId": "my-hiring-watch"
}' |
apify call synthex-labs/ats-job-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,synthex-labs/ats-job-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

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