# Ats Job Change Monitor (`eunit/ats-job-change-monitor`) Actor

Track Greenhouse and Lever job boards for new, updated, and removed job postings. Monitor employer hiring changes, extract clean job listings, and automate career page alerts with stateful baselines. Built for recruiters, market intelligence, and job aggregators. Fast, reliable, no login required.

- **URL**: https://apify.com/eunit/ats-job-change-monitor.md
- **Developed by:** [Emmanuel Uchenna](https://apify.com/eunit) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $3.00 / 1,000 job observations

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 Change Monitor

### What does this Actor do?

Collect public employer job postings from Greenhouse and Lever, or compare them with a saved baseline to find new, updated, and removed board listings. Select the employers you want to monitor instead of starting with a broad keyword search across unrelated companies. The Actor uses public job board GET endpoints and does not collect applicant data, submit applications, or log into an account.

Use snapshots for a current research sample. Use changes mode for a recurring hiring-market watch, an employer job-feed integration, or an internal review queue. A job disappearing from a public board is reported as `removed-from-board`; that observation does not prove that a vacancy was filled.

### Get started

```json
{
  "boards": [{"provider":"greenhouse","boardToken":"stripe","companyLabel":"Stripe"}],
  "mode":"snapshot",
  "maxItems":25,
  "titleKeywords":["engineer"],
  "includeDescriptions":false
}
```

Find the board token in the employer's public careers URL. For Greenhouse, use its board token; for Lever, use the site slug. Provide a token rather than a complete URL. `companyLabel` is your label for the board, not an independently verified company attribute. This release uses the global Lever API; region-specific Lever endpoints are outside the current scope.

### Track changes across runs

Set `mode` to `changes` and supply `stateStoreName`, for example `my-employer-monitor`. Reuse that name for the same workflow. The first run emits matching jobs as new unless you select `baselineOnly: true`. A baseline-only run saves state without creating job-result events, while startup and platform usage can still apply.

Later runs emit `new`, `updated`, `removed-from-board`, or `no-longer-matches` records. Source failures, malformed responses, and incomplete pagination preserve the previous baseline and do not produce removal observations. A successful empty board can produce removals, because its complete response establishes that the previously observed postings are absent from that board.

Keep one active run per state store. Dataset and state writes are separate operations, so an interrupted run can cause a repeated observation. Deduplicate downstream by the stable `id`, observation time, and change type. A limited run updates state only for emitted changes; pending changes can be emitted in a later run.

### Input and limits

Choose 1-10 boards and at most 1000 saved result records per run. Each source board is bounded to 5000 postings and 10 MB of JSON responses. Requests use timeouts and a complete-board fetch has a 90-second bound. A source that exceeds those limits fails safely instead of replacing the baseline with a truncated result set.

`titleKeywords` and `locationKeywords` are case-insensitive substring filters. They do not classify seniority, validate geographic eligibility, or establish that a role is still accepting applications. Description output is optional and limited to 30000 characters per job. Large Greenhouse boards may require descriptions to remain disabled to fit the source-response cap.

### Output

Each dataset row includes `id`, `source_job_id`, `provider`, `board_token`, `company_label`, `title`, `location`, `department`, `employment_type`, `job_url`, `apply_url`, source timestamps when available, `observed_at`, `change_type`, and `changed_fields`. Missing source fields remain null. In Greenhouse metadata mode, an updated source timestamp can identify an update without identifying which unrequested description text changed.

Use `RUN-SUMMARY` for per-board coverage, saved counts, and pending changes. Use `ERRORS` for non-billable board failures. Preserve source job links in your destination so an analyst can verify an observation before acting on it.

### Pricing

The release proposes one `job-result` event per saved snapshot or change observation at $0.003 per event ($3 per 1000 observations). Unchanged records and failed board requests do not create this event. The proposed automatic startup event is $0.00005 per allocated GB, with platform usage separate during the canary phase. Inspect the actual Pricing tab before starting a paid run.

The proposal in `.actor/pricing.json` is not active simply because source code was deployed. Configure and verify the pricing contract in Apify Console before public release. Do not add a second default-dataset-item fee for the same result.

### Scheduling and support

Save a tested input as a task, then choose a schedule that matches the employers' update frequency and your budget. A daily or weekly observation is a point-in-time sample, not a complete history of every intermediate change. Read the dataset through the Apify API or an authorized downstream integration after the run succeeds.

For support, include the run ID, provider, public board token, and a sanitized input in an issue. Do not include API tokens or private applicant information. Source contracts: [Greenhouse Job Board API](https://developers.greenhouse.io/job-board.html) and [Lever Postings API](https://github.com/lever/postings-api).

# Actor input Schema

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

1-10 public employer boards. boardToken is the company board slug, not a URL. companyLabel is an optional label you supply.

## `mode` (type: `string`):

Snapshot emits matching jobs. Changes compares with a named baseline and emits new, updated, or removed board records.

## `stateStoreName` (type: `string`):

Required in changes mode. Reuse this name for one workflow and avoid overlapping runs.

## `maxItems` (type: `integer`):

Maximum saved result records across all selected boards. Unemitted changes remain eligible for later runs.

## `titleKeywords` (type: `array`):

Optional case-insensitive substring matches. A title must match at least one supplied keyword.

## `locationKeywords` (type: `array`):

Optional case-insensitive substring matches against source location text. This does not verify remote-work eligibility.

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

Include up to 30000 source description characters per job. Greenhouse requests with full descriptions can exceed the bounded board-response limit.

## `includeRemoved` (type: `boolean`):

In changes mode, emit removed-from-board or no-longer-matches only after a complete successful board fetch. Removal does not prove a role was filled.

## `baselineOnly` (type: `boolean`):

Changes mode only. Save the complete matching baseline without job-result events. Startup and platform usage still apply.

## Actor input object example

```json
{
  "boards": [
    {
      "provider": "greenhouse",
      "boardToken": "stripe",
      "companyLabel": "Stripe"
    }
  ],
  "mode": "snapshot",
  "stateStoreName": "my-employer-job-monitor",
  "maxItems": 100,
  "titleKeywords": [],
  "locationKeywords": [],
  "includeDescriptions": false,
  "includeRemoved": true,
  "baselineOnly": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `errors` (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": [
        {
            "provider": "greenhouse",
            "boardToken": "stripe",
            "companyLabel": "Stripe"
        }
    ],
    "stateStoreName": "my-employer-job-monitor"
};

// Run the Actor and wait for it to finish
const run = await client.actor("eunit/ats-job-change-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": [{
            "provider": "greenhouse",
            "boardToken": "stripe",
            "companyLabel": "Stripe",
        }],
    "stateStoreName": "my-employer-job-monitor",
}

# Run the Actor and wait for it to finish
run = client.actor("eunit/ats-job-change-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": [
    {
      "provider": "greenhouse",
      "boardToken": "stripe",
      "companyLabel": "Stripe"
    }
  ],
  "stateStoreName": "my-employer-job-monitor"
}' |
apify call eunit/ats-job-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eunit/ats-job-change-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/sdygrnrAEYnm90kul/builds/DyzFM3k8O9IyyOHC5/openapi.json
