# Job Change Monitor: New and Closed Roles Only (`glitchbound/ats-jobs-monitor`) Actor

Watch company job boards and get only what changed since the last run: roles that appeared, and roles that disappeared. State lives in a store Apify keeps indefinitely, and the first run records a baseline without charging. No API key, no proxy, no diffing on your side.

- **URL**: https://apify.com/glitchbound/ats-jobs-monitor.md
- **Developed by:** [Daniel Meshulam](https://apify.com/glitchbound) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 job postings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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 Change Monitor: New and Closed Roles Only

**Run it on a schedule. Get the four roles that appeared, not the 548 that were
already there.**

A search of the Apify Store on 2026-08-01 found no other Actor that reports
job board changes. Everything else hands you the whole board and leaves the
diffing to you.

```json
{ "companies": ["stripe.com", "figma.com"] }
```

```
NEW     Staff Security Engineer          stripe.com
NEW     Product Designer, Payments       stripe.com
CLOSED  4318772008                       figma.com
```

No API key. No proxy. No diffing on your side.

### Two things decide whether a monitor actually works

Both were checked, not assumed, and both are the reason this one can be trusted.

#### The memory has to be permanent

Apify's own documentation: *"Named key-value stores are retained indefinitely.
Unnamed key-value stores expire after 7 days."*

A monitor built on the run's default storage **silently loses its memory every
week**, and then reports an entire board as new. That is worse than no monitor,
because it looks like it worked and it bills like it worked. This Actor keeps
state in a named store, and the store name is an input so you can hold separate
watchlists apart.

#### Two watchlists must never share a baseline

State is keyed per board, and the key carries a digest of the board token
rather than only a sanitised version of it. Sanitising alone collapsed
different boards onto one key: `Bosch Group` and `Bosch/Group` both became
`Bosch-Group`.

Two boards on one baseline is not a small bug. Every run would report every
posting of one board as new and every posting of the other as closed, then swap
on the next run, forever, and you would pay for every one of those rows.

#### The first run must not bill you for the baseline

On day one everything is new by definition. Being charged for 548 "new" roles
the first morning you point this at Stripe is a technicality dressed as a
feature.

So the first run **records the baseline, reports one row saying exactly that,
and charges nothing**:

```
Baseline recorded for stripe.com (greenhouse/stripe): 548 open roles.
This run is not charged. From the next run on, only roles that appear
or disappear are reported.
```

### A role disappearing is a signal too

`reportClosedRoles` is on by default. A role leaving the board means it was
filled, paused or withdrawn, and for anyone tracking a competitor's hiring that
is often the more interesting half.

### A failed check never corrupts the memory

If a board cannot be fetched, the remembered state is **left alone** and an
error row is emitted. The alternative, overwriting the baseline with nothing,
would make the next successful run report the whole board as new and charge for
it. Verified: after a failed fetch the stored baseline is byte-identical.

### Setting it up

1. Run it once. That records the baseline and costs nothing.
2. Add it to an Apify **Schedule**, daily or hourly.
3. Connect the dataset to Slack, email, a webhook or a sheet.

Changed which companies you watch? Set `resetBaseline: true` once.

### What you get

New roles carry the full posting, in the same shape as **ATS Jobs Scraper**,
plus `changeType: "new"`. Closed roles carry `changeType: "closed"` with the
job id, company, platform and board.

| Field | |
|---|---|
| `changeType` | `new` or `closed` |
| `title`, `location`, `url` | on new roles |
| `departments`, `offices` | |
| `company`, `atsPlatform`, `boardToken`, `boardUrl` | |
| `postedAt`, `updatedAt` | |
| `description` | opt in, see below |

### Why this returns data when job scrapers return 403

Indeed, Glassdoor, ZipRecruiter and LinkedIn Jobs do not originate job data.
They aggregate it from company career pages, and those pages run on systems
that publish free, keyless, public APIs, because companies want their openings
indexed. Measured from a residential address on 2026-08-01, `indeed.com/jobs`,
`glassdoor.com`, `ziprecruiter.com` and `upwork.com` all answered **403 with
zero bytes**, while these sources answered with real jobs.

Going to the source is also faster to notice. An aggregator shows you its last
crawl. This shows you the board.

### Notes

- Covers Greenhouse, Lever, Ashby, Workable, Recruitee, Rippling, Workday,
  Personio and SmartRecruiters. Watch a company by domain on the first six;
  the last three take an exact board, because on those a wrong guess is
  indistinguishable from a right one.
- `includeDescription` is off by default because it is expensive at the source:
  Greenhouse returns 111 KB for a board without descriptions and 1.97 MB with
  them. Rippling's board API has no descriptions at any setting.
- Roles are matched by the ATS's own job id, so a retitled posting is not
  reported as a new one.
- Errors are per board. One unreachable company does not end the run, and error
  rows are not charged.
- Just want the current board: **ATS Jobs Scraper**. Just want to know which
  system a company runs: **Hiring Tech Stack Detector**.

# Actor input Schema

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

Company domains, one per line, e.g. stripe.com. The Actor finds the board itself and remembers what was on it, so later runs report only roles that appeared or disappeared.

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

Watch a board directly, as platform:token, one per line: greenhouse:stripe, ashby:openai, lever:palantir.

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

The named key-value store holding what was seen last time. Named stores are kept indefinitely by Apify while unnamed ones are deleted after 7 days, which is why this Actor insists on a named one: otherwise the memory would vanish weekly and a whole board would look new. Use different names to keep separate watchlists apart.

## `reportClosedRoles` (type: `boolean`):

A role leaving the board is a signal too: filled, paused or withdrawn. On by default.

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

Discard the remembered state and treat this run as a fresh baseline. Use after changing which companies you watch.

## `includeDescription` (type: `boolean`):

Fetch description text for changed roles. Off by default because it is expensive at the source: Greenhouse returns 111 KB for a board without descriptions and 1.97 MB with them.

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

A hard ceiling on change rows for the entire run. Leave empty for no ceiling.

## `proxyConfiguration` (type: `object`):

Proxy settings. Every source is a public API a company publishes on purpose, so the default is fine.

## Actor input object example

```json
{
  "companies": [
    "stripe.com"
  ],
  "stateStoreName": "ats-jobs-monitor",
  "reportClosedRoles": true,
  "resetBaseline": false,
  "includeDescription": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per change since the previous run: roles opened and closed.

# 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 = {
    "companies": [
        "stripe.com"
    ],
    "stateStoreName": "ats-jobs-monitor"
};

// Run the Actor and wait for it to finish
const run = await client.actor("glitchbound/ats-jobs-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 = {
    "companies": ["stripe.com"],
    "stateStoreName": "ats-jobs-monitor",
}

# Run the Actor and wait for it to finish
run = client.actor("glitchbound/ats-jobs-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 '{
  "companies": [
    "stripe.com"
  ],
  "stateStoreName": "ats-jobs-monitor"
}' |
apify call glitchbound/ats-jobs-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,glitchbound/ats-jobs-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/GKPBpSeGxKqdldF5a/builds/5jWO9bveLPvWCh620/openapi.json
