# ATS New Jobs Monitor (`looped_warp/ats-new-jobs-monitor`) Actor

Monitor Greenhouse, Lever, Ashby, Workable, and SmartRecruiters boards and output only NEW and REMOVED job postings since the previous run.

- **URL**: https://apify.com/looped_warp/ats-new-jobs-monitor.md
- **Developed by:** [Yingjie Liu](https://apify.com/looped_warp) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 job results

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

### What does ATS New Jobs Monitor do?

**ATS New Jobs Monitor** watches public job boards on **Greenhouse**, **Lever**, **Ashby**, **Workable**, and **SmartRecruiters** and writes **only the jobs that appeared or disappeared since the previous run**. It talks to each ATS's public JSON API — no browser, no proxy, and no unofficial HTML scraping.

Give it company names, careers URLs, board URLs, or board tokens. The Actor auto-detects the ATS, normalizes every posting into one schema, diffs against a snapshot stored in a named Key-Value Store, and can ping a webhook or Slack with a change summary.

This is a hiring-signal feed, not a full-text job search engine. It does not scrape LinkedIn, Indeed, or Workday.

### Why monitor ATS job boards?

Company career sites are the source of truth for **new roles**. Aggregators lag, duplicate, and mix stale listings. Hitting the ATS JSON endpoint directly is faster, cheaper, and stable enough to schedule every hour.

Typical uses:

- **Sales lead hiring signals** — a burst of Account Executive or Solutions Engineer roles often means a team is ramping quota. Alert your SDRs the same day a role goes live.
- **Job seeker new-job alerts** — watch a short list of target companies and get only the NEW postings, not the whole board every time.
- **Recruiting competitive intel** — see which departments competitors are growing, and which roles they just closed (REMOVED).
- **RevOps / GTM research** — fold hiring changes into a CRM or spreadsheet via the Apify API, Zapier, Make, or n8n.

Platform extras you get for free by running this as an Apify Actor: scheduling, dataset export (JSON/CSV/Excel), webhooks, and a spend cap via `ACTOR_MAX_TOTAL_CHARGE_USD`.

### What data can ATS New Jobs Monitor extract?

| Field | Type | Description |
| --- | --- | --- |
| `changeType` | string | `NEW`, `REMOVED`, or `EXISTING` |
| `company` | string | Company display name |
| `ats` | string | `greenhouse`, `lever`, `ashby`, `workable`, or `smartrecruiters` |
| `title` | string | Job title |
| `department` / `team` | string | Org fields when the ATS publishes them |
| `location` | string | Primary location |
| `isRemote` | boolean | Remote flag or location text that looks remote |
| `employmentType` | string | Full-time, contract, intern, … |
| `salary` | object | `min`, `max`, `currency` when the board publishes pay |
| `postedAt` / `updatedAt` | string | ISO-8601 timestamps |
| `url` | string | Public posting URL |
| `descriptionText` | string | Optional plain text (off by default) |
| `firstSeenAt` | string | When this monitor first saw the job |
| `jobId` | string | ATS identifier used for diffing |

You can download the dataset as JSON, HTML, CSV, or Excel from the run's Output tab.

### How to monitor new jobs on Greenhouse, Lever, and Ashby

1. Open the Actor in [Apify Console](https://console.apify.com) and click **Try for free**. The default input already fetches a small live Greenhouse board (Airtable) so the first run is non-empty.
2. Add your own companies. A name (`stripe`), a board URL (`https://jobs.lever.co/palantir`), or a token (`notion`) all work.
3. For a **scheduled NEW/REMOVED monitor**, set a stable **Monitor ID** and leave **Output mode** on `changesOnly`. Empty Monitor ID is a stateless demo: every run outputs current jobs (`all`), capped at 25.
4. Optionally set keyword, location, department, or remote-only filters.
5. (Optional) Paste a Slack incoming webhook or a generic webhook URL.
6. Click **Start**. Schedule the same input (with Monitor ID) hourly or daily so the snapshot stays current.
7. Read NEW/REMOVED rows in the dataset, or consume them from the API.

Locally:

```bash
npm install
npx apify-cli run --purge --input-file .actor/sample-input.json
```

The sample input uses live public boards: Greenhouse `stripe`, Lever `palantir`, Ashby `notion`, Workable `huggingface`, and SmartRecruiters `DeliveryHero`. Netflix is no longer on Lever (`api.lever.co/v0/postings/netflix` returns 404). OpenAI's Ashby board (`openai`) is live but large (~14 MB JSON); `notion` is the lighter Ashby example.

### How much will it cost to monitor job postings?

Pricing is **pay-per-event**. Configure the same event names in Apify Console when you publish:

| Event | When it fires | Price |
| --- | --- | --- |
| `actor-start` | Once at the beginning of every run | $0.005 |
| `company-detection` | When the Actor has to probe which ATS a company uses | $0.002 |
| `job-result` | Each job row pushed to the dataset | $0.0015 |

Examples:

- Default / empty input (Store auto-test): Airtable Greenhouse, ≤25 jobs → about `$0.005 + 4 × $0.0015 ≈ $0.011`
- 10 companies with known board URLs, 25 NEW jobs → `$0.005 + 25 × $0.0015 = $0.0425`
- Named monitor, no changes → `$0.005` (empty dataset — this is why Store tests leave Monitor ID empty)
- First run with **emit all as NEW** on 400 open roles → `$0.005 + 400 × $0.0015 = $0.605`

Skip `company-detection` charges by setting `ats` (or passing a `jobs.lever.co` / `boards.greenhouse.io` / `jobs.ashbyhq.com` URL). Leave **Include job description text** off unless you need it — descriptions do not change the event price, but they make runs heavier.

Users can cap spend with **max total charge** in Console (`ACTOR_MAX_TOTAL_CHARGE_USD`). The Actor stops pushing results and saves a consistent snapshot when the limit is reached, so the next run resumes instead of skipping jobs silently.

Apify platform compute is billed separately. Runs are JSON API calls, so compute is typically a few seconds to a couple of minutes.

### Input

See the **Input** tab for the full form. The important fields:

- **Companies** — `identifier` (required), optional `name`, `ats`, and `token`. Default/prefill is Airtable's Greenhouse board so `{}` and Store auto-tests still produce rows.
- **Monitor ID** — leave empty for one-shot / Store tests (stateless `all`, max 25 jobs). Set a stable ID for scheduled `changesOnly` diffs.
- **Snapshot Key-Value Store name** — named store that survives across runs. Default `ats-new-jobs-monitor-snapshots`.
- **Output mode** — `changesOnly` is the scheduled-monitor default (NEW + REMOVED only). Empty Monitor ID always behaves as `all`.
- **First run behavior** — emit everything as NEW, or baseline with no dataset items. Only applies when Monitor ID is set.
- **Max jobs** — `0` = no cap on named monitors. Stateless runs cap at 25 unless you set a positive value.
- **Filters** — include/exclude keywords, locations, departments, remote only.
- **Webhooks** — generic JSON POST and/or Slack incoming webhook.

Unknown companies are logged and skipped. One bad token will not fail the run.

### Output

Each dataset item looks like this:

```json
{
  "jobId": "8023928",
  "company": "Stripe",
  "ats": "greenhouse",
  "title": "Account Executive, Bridge",
  "department": "Sales",
  "team": null,
  "location": "London",
  "isRemote": false,
  "employmentType": null,
  "salary": null,
  "postedAt": "2026-07-30T10:59:38.000Z",
  "updatedAt": "2026-08-04T11:02:31.000Z",
  "url": "https://stripe.com/jobs/search?gh_jid=8023928",
  "descriptionText": null,
  "changeType": "NEW",
  "firstSeenAt": "2026-10-20T13:00:00.000Z",
  "boardToken": "stripe",
  "monitorId": "sales-signals"
}
```

A run summary is saved to the default key-value store as `OUTPUT` (counts, detected boards, and per-company errors).

### Supported ATS endpoints

Public, unauthenticated JSON only:

| ATS | Endpoint |
| --- | --- |
| Greenhouse | `https://boards-api.greenhouse.io/v1/boards/{token}/jobs` |
| Lever | `https://api.lever.co/v0/postings/{token}?mode=json` |
| Ashby | `https://api.ashbyhq.com/posting-api/job-board/{token}?includeCompensation=true` |
| Workable | `https://apply.workable.com/api/v1/widget/accounts/{token}` |
| SmartRecruiters | `https://api.smartrecruiters.com/v1/companies/{id}/postings` |

No Playwright, no Apify Proxy by default. If a vendor starts blocking datacenter IPs, add a proxy later — it is not required today.

### How to publish this Actor

1. Create an Apify account and run `npx apify-cli login` (or set `APIFY_TOKEN`).
2. From this directory: `npx apify-cli push`.
3. In **Apify Console → Actor → Publication → Monetization**, choose **Pay per event**.
4. Create events that match `.actor/pay_per_event.json`: `actor-start` ($0.005), `job-result` ($0.0015, primary), `company-detection` ($0.002).
5. Do **not** also enable the synthetic `apify-actor-start` event — this Actor charges `actor-start` in code. Using both would double-bill starts.
6. Set a README, categories, and a store listing, then publish.

Local monetization test:

```bash
ACTOR_TEST_PAY_PER_EVENT=true npx apify-cli run --input-file .actor/sample-input.json
```

### FAQ

#### How does incremental mode work?

Set a **Monitor ID**. Each ID stores the last filtered job set in a named Key-Value Store. The next run diffs `ats + boardToken + jobId`. Jobs only in the new set are NEW; jobs only in the snapshot are REMOVED. Changing filters changes what is stored, so NEW/REMOVED will follow the filtered view.

If **Monitor ID is empty**, the run is stateless: no snapshot, `outputMode=all`, and a 25-job cap. Apify Store auto-tests use this path so the default dataset stays non-empty every day.

#### What happens on the first run?

**Emit all current jobs as NEW** (default) fills the dataset so you can verify the watch list. **Baseline silently** writes the snapshot and outputs nothing; the following run reports true changes only. An immediate second run against an unchanged Greenhouse / Lever / Ashby / Workable board should push **zero** items in `changesOnly` mode. Large SmartRecruiters boards can add or drop postings between two calls a few seconds apart; those rows are real NEW/REMOVED events.

#### Why was my company skipped?

The identifier did not match a live public board. Check the run log and the `OUTPUT` record. Common cases: the company uses Workday (out of scope), the Lever/Greenhouse slug changed, or SmartRecruiters needs the exact `company.identifier` (case can matter).

#### Does this scrape employee personal data?

No. It only reads roles a company has chosen to publish on its job board. Treat job descriptions as potentially containing personal data if you enable `includeDescription`, and comply with GDPR/CCPA for your use case.

#### How do I run unit tests?

```bash
npm test
```

Tests cover ATS URL detection, job normalization, snapshot diffing, filters, and default-input (stateless) resolution. They do not call live APIs.

#### Where should I send feedback?

Use the Actor **Issues** tab on Apify. For API integration examples, open the **API** tab on the Actor page.

# Actor input Schema

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

Each item can be a company name, careers URL, ATS board URL, or board token. Leave ATS blank to auto-detect among Greenhouse, Lever, Ashby, Workable, and SmartRecruiters using public JSON APIs. The default is a small live Greenhouse board so empty Store auto-tests still return rows.

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

Set this to enable incremental NEW/REMOVED diffs across scheduled runs. Leave empty (the Store auto-test / Try Actor default) for a stateless run that always outputs current jobs (`outputMode=all`, capped). Do not reuse an empty ID for a daily monitor — that would skip the snapshot.

## `snapshotStoreName` (type: `string`):

Named Key-Value Store used to persist snapshots across runs. Use a dedicated store per environment (dev/prod).

## `incremental` (type: `boolean`):

When a Monitor ID is set, diff against the previous snapshot and (with changesOnly) output only NEW and REMOVED jobs. Ignored when Monitor ID is empty — those runs are always stateless.

## `outputMode` (type: `string`):

For scheduled monitors (Monitor ID set): `changesOnly` is what you want — only NEW and REMOVED rows. `all` also pushes EXISTING open jobs. Empty Monitor ID always behaves as `all`.

## `firstRunMode` (type: `string`):

What to do when no snapshot exists yet. `emitAllAsNew` outputs every current job as NEW. `baselineSilent` saves the snapshot and outputs nothing, so the next run only reports true changes.

## `includeKeywords` (type: `array`):

Keep a job if its title, department, team, or location contains any of these phrases (case-insensitive). Leave empty to keep all.

## `excludeKeywords` (type: `array`):

Drop a job if its title, department, team, or location contains any of these phrases (case-insensitive).

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

Keep jobs whose location string contains any of these values (case-insensitive). Remote jobs still pass when Remote only is enabled.

## `departments` (type: `array`):

Keep jobs whose department or team contains any of these values (case-insensitive).

## `remoteOnly` (type: `boolean`):

When enabled, only jobs marked remote (or whose location text looks remote) are kept.

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

Adds a plain-text `descriptionText` field. Greenhouse and Workable fetch richer payloads; SmartRecruiters requires one extra request per job. Leave off unless you need descriptions.

## `maxJobs` (type: `integer`):

Safety cap on dataset items pushed this run. `0` means no cap when a Monitor ID is set. Stateless runs (empty Monitor ID) cap at 25 unless you set a positive value, so Store auto-tests stay small and cheap.

## `webhookUrl` (type: `string`):

Optional URL that receives a JSON POST with new/removed counts and a truncated list of changes. Ignored when empty.

## `slackWebhookUrl` (type: `string`):

Optional Slack incoming webhook. The Actor posts a short summary of NEW and REMOVED jobs. Ignored when empty.

## Actor input object example

```json
{
  "companies": [
    {
      "identifier": "https://boards.greenhouse.io/airtable",
      "name": "Airtable",
      "ats": "greenhouse"
    },
    {
      "identifier": "https://jobs.lever.co/palantir",
      "name": "Palantir"
    }
  ],
  "snapshotStoreName": "ats-new-jobs-monitor-snapshots",
  "incremental": true,
  "outputMode": "changesOnly",
  "firstRunMode": "emitAllAsNew",
  "remoteOnly": false,
  "includeDescription": false,
  "maxJobs": 0
}
```

# Actor output Schema

## `jobChanges` (type: `string`):

Dataset of NEW, REMOVED, and optionally EXISTING job postings

## `runSummary` (type: `string`):

Counts, errors, and detected boards for this run

# 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": [
        {
            "identifier": "https://boards.greenhouse.io/airtable",
            "name": "Airtable",
            "ats": "greenhouse"
        }
    ],
    "snapshotStoreName": "ats-new-jobs-monitor-snapshots",
    "outputMode": "changesOnly",
    "firstRunMode": "emitAllAsNew"
};

// Run the Actor and wait for it to finish
const run = await client.actor("looped_warp/ats-new-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": [{
            "identifier": "https://boards.greenhouse.io/airtable",
            "name": "Airtable",
            "ats": "greenhouse",
        }],
    "snapshotStoreName": "ats-new-jobs-monitor-snapshots",
    "outputMode": "changesOnly",
    "firstRunMode": "emitAllAsNew",
}

# Run the Actor and wait for it to finish
run = client.actor("looped_warp/ats-new-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": [
    {
      "identifier": "https://boards.greenhouse.io/airtable",
      "name": "Airtable",
      "ats": "greenhouse"
    }
  ],
  "snapshotStoreName": "ats-new-jobs-monitor-snapshots",
  "outputMode": "changesOnly",
  "firstRunMode": "emitAllAsNew"
}' |
apify call looped_warp/ats-new-jobs-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,looped_warp/ats-new-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/B5Au0qMr0b5fg4IGk/builds/IRRecpQXUril8AMea/openapi.json
