# ATS Jobs Monitor (`piotrv1001/ats-jobs-monitor`) Actor

The ATS Jobs Monitor watches company job boards on a schedule and returns only what changed — roles opened since the last check, and roles closed — with canonical apply URLs, true first-published dates, structured locations, remote and employment type, department and salary.

- **URL**: https://apify.com/piotrv1001/ats-jobs-monitor.md
- **Developed by:** [FalconScrape](https://apify.com/piotrv1001) (community)
- **Categories:** Jobs, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 new jobs

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/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

### 🚀 ATS Jobs Monitor

Watch any company's job board on a schedule and get back **only what changed** — the roles that opened since the last check, and, if you want them, the roles that closed.

Every other company-jobs scraper is one-shot: run it today, run it tomorrow, and tomorrow you get the same 400 roles back plus a handful of new ones — and you pay for all 400 again. This monitor remembers every job it has already reported to you and hands you only the difference.

It reads the employer's own applicant-tracking system rather than a job board, so each new role arrives with its **canonical apply link**, its **true first-published date**, and the **remote / employment-type / salary fields the employer actually published** — not a guess inferred from the text.

Point it at a list of companies, put it on a daily schedule, and you have a hiring feed instead of a job dump.

### ✨ Features

- **Only what changed, ever.** Each check is compared against everything the monitor has reported before, keyed on the employer system's own job ID. A role you've already been given is never delivered twice and never charged twice.
- **Closed roles too — a signal nothing else in this category gives you.** When a job comes off a board, it was filled, pulled or expired. Switch it on and you get a `closed` row for it, which is what keeps a job index or a hiring-signal product from going stale. Off by default.
- **A closure is never guessed at.** A role is only reported closed after **two consecutive checks** miss it. A board that answers oddly for one run, or a company whose board is too large to read in full, is left alone rather than reported as a wave of layoffs.
- **Companies in, boards found for you.** Give it homepage URLs. The monitor locates each company's job board once and then re-checks that same board on every run, so results stay consistent from day to day instead of drifting between sources.
- **Eight employer systems read at the source:** Greenhouse, Lever, Ashby, Workday, Oracle HCM, SmartRecruiters, Workable and Radancy. You can also paste a board URL directly.
- **Filters that genuinely filter.** Keyword, location, workplace type and employment type are applied to the fields the employer publishes. A role whose employer doesn't publish a field is kept, not silently dropped — "this company doesn't say" is not the same answer as "no".
- **Nothing is silently dropped.** If a check stops at your new-job limit, the rest stay unreported and are picked up next run. Every run writes a `RUN_SUMMARY` saying what it found per company, what it already knew, which companies it couldn't resolve, and why any closure check was skipped.
- **Memory that survives.** The seen-list is stored permanently in your own account, so it works across months of scheduled runs.
- **No proxy, no account, no cookies.** Only publicly published job data.

### 🛠️ How It Works

1. Enter the companies you want to watch — homepage URLs, or board URLs if you already have them.
2. Optionally narrow with keywords, a location, workplace type or employment type.
3. Run it once. The first check has no history, so it reports every open role it finds and becomes your baseline.
4. **Put it on a schedule.** Every check after that returns only what's new.

#### ⏰ Setting up the schedule

In the Apify Console, open the actor → **Schedules** → **Create new schedule**, pick a frequency and attach this actor with your input. Nothing else is needed — the monitor picks up its own memory on every run.

Daily is the usual choice. Hiring boards don't change by the hour, and a slower schedule costs you fewer checks for the same set of roles.

#### 🔑 Monitor ID

By default the memory is tied to the exact companies and filters you entered — so adding a company or changing a keyword starts a fresh monitor, and the next check re-reports everything as new.

If you expect to edit the watch over time, set a **Monitor ID** such as `competitor-hiring`. Runs sharing an ID share one memory, so you can change the list without being sent — and charged for — your whole feed again.

#### 🔔 Getting notified when something changes

Open the actor → **Integrations** and connect Slack, email, a webhook, Zapier or Make to the `ACTOR.RUN.SUCCEEDED` event.

One thing to set up carefully, because it is easy to get wrong: **Apify only has run-state events** — there is no "only if the dataset has rows" trigger. A check that finds nothing new succeeds with an empty dataset *by design*, so a plain "notify me on success" hook will ping you every day whether or not there is anything to see, which is exactly the noise this actor exists to remove.

Filter on the run's own numbers instead. Every run writes a `RUN_SUMMARY` record to its key-value store:

```json
{ "newJobs": 12, "closedJobs": 3, "jobsOnBoards": 486, "alreadyReportedBefore": 474 }
```

- **Webhook / Zapier / Make** — read `newJobs` (and `closedJobs`) and drop the event when both are `0`.
- **Slack / email integration** — send the dataset and add a filter step for a non-empty result.

Set that up once and a quiet week is genuinely silent.

### 📋 Output Fields

Every row carries `changeType`, which is either `new` or `closed`.

#### New role

| Field | Description |
| --- | --- |
| `changeType` | `new` |
| `jobId` | The employer system's own job ID — the key the monitor remembers |
| `title` | Job title |
| `companyName` / `companyDomain` | Hiring company, and its domain as a join key |
| `source` | Which employer system the role was read from |
| `atsBoardToken` | The company's board identifier on that system |
| `jobUrl` | Canonical job posting URL |
| `applyUrl` | **Canonical application link** — the real one, not a redirect |
| `location` | Location as published |
| `locationCity` / `locationRegion` / `locationCountry` | Structured location |
| `remote` | `yes`, `no`, `hybrid` or `unknown`, from the employer's own field |
| `employmentType` | Full-time, Part-time, Contract, Internship, ... |
| `department` / `team` | Org placement, where the employer publishes it |
| `seniority` | Seniority read from the title |
| `requisitionId` | The employer's internal requisition number |
| `postedAt` | **True first-published date**, not "2 weeks ago" |
| `updatedAt` | When the employer last edited the posting |
| `salary` | Salary as published |
| `salaryMin` / `salaryMax` / `salaryCurrency` / `salaryPeriod` | Parsed salary |
| `descriptionText` / `descriptionHtml` | Full job description |
| `firstSeenAt` / `lastSeenAt` | When this monitor first and last saw the role |
| `careersUrl` | The board the role was read from |
| `scrapedAt` | When the role was read |
| `skills` / `requirements` / `responsibilities` / `benefits` | Populated when AI enrichment is switched on |

#### Closed role

| Field | Description |
| --- | --- |
| `changeType` | `closed` |
| `jobId` / `title` / `jobUrl` | The role that came off the board |
| `companyName` / `companyDomain` / `source` | Which company and system |
| `firstSeenAt` | When the monitor first reported the role |
| `lastSeenAt` | The last check that still found it on the board |
| `closedAt` | When the monitor confirmed it was gone |

Fields the employer does not publish are `null` or empty — never invented.

### 📊 Sample Output Data

A newly opened role:

```json
{
    "changeType": "new",
    "companyDomain": "figma.com",
    "companyName": "Figma",
    "source": "greenhouse",
    "atsBoardToken": "figma",
    "jobId": "gh-5364702004",
    "title": "Account Executive, Emerging Enterprise (Berlin, Germany)",
    "location": "Berlin, DE",
    "locationCity": "Berlin",
    "locationCountry": "Germany",
    "remote": "unknown",
    "department": "Sales",
    "requisitionId": "1334",
    "postedAt": "2024-11-01T06:05:10-04:00",
    "updatedAt": "2026-07-22T05:37:08-04:00",
    "jobUrl": "https://boards.greenhouse.io/figma/jobs/5364702004",
    "applyUrl": "https://boards.greenhouse.io/figma/jobs/5364702004?gh_jid=5364702004",
    "firstSeenAt": "2026-08-09T06:30:42.000Z",
    "lastSeenAt": "2026-08-09T06:30:42.000Z",
    "closedAt": null
}
```

A role that has come off the board:

```json
{
    "changeType": "closed",
    "companyDomain": "notion.so",
    "companyName": "Notion",
    "source": "ashby",
    "jobId": "ashby-4f2c1a90-1b7e-4a55-9a3e-2c6d8b0f7e11",
    "title": "Staff Software Engineer, Platform",
    "jobUrl": "https://jobs.ashbyhq.com/notion/4f2c1a90-1b7e-4a55-9a3e-2c6d8b0f7e11",
    "firstSeenAt": "2026-08-06T06:43:40.000Z",
    "lastSeenAt": "2026-08-08T06:43:40.000Z",
    "closedAt": "2026-08-09T06:43:43.368Z"
}
```

Each run also writes a `RUN_SUMMARY` to the key-value store, with a per-company breakdown:

```json
{
    "firstRun": false,
    "newJobs": 12,
    "closedJobs": 3,
    "jobsOnBoards": 486,
    "alreadyReportedBefore": 474,
    "companiesWatched": 5,
    "companiesResolved": 5,
    "companiesUnresolved": 0,
    "remembered": 495,
    "truncated": false,
    "truncationReason": null
}
```

### 💰 Pricing

| Event | Free plan | Gold plan |
| --- | --- | --- |
| Monitor check (per run) | $0.008 | $0.003 |
| New job found | $0.004 | $0.0015 |
| Job closed *(optional)* | $0.002 | $0.001 |

**You pay for a check and for changes, not for the board.** Reading a company's full board is what makes it possible to tell a genuinely new role from one you already have — and to tell a closed role from a board that simply failed to answer — so that work is covered by the flat per-check fee, however many roles the board holds.

A daily monitor over five companies turning up about 15 new roles a day costs roughly **$2.00/month** on the Free plan and **$0.80/month** on Gold. A quiet day costs the check fee alone — under a cent.

Closed-job reporting is off unless you switch it on, and adds nothing when it is off.

### ❓ FAQ

**What happens on the very first run?**
It has no history, so every open role it finds is new and gets reported — that's your baseline, and it is charged. If you're watching large employers, keep **Maximum new jobs per run** low to spread that first intake over several runs; nothing is lost, the next check picks up where the last one stopped.

**I added a company and got everything again. Why?**
By default the memory is tied to the exact watch. Set a **Monitor ID** and reuse it, and you can edit the list and the filters freely without losing history.

**One of my companies came back unresolved.**
Some employers — mostly large enterprises whose careers site is built entirely in the browser — publish no board that can be found from their homepage. The monitor names those companies in the log and in `RUN_SUMMARY` with every route it tried, rather than quietly returning nothing for them. If you know the board URL, paste it in directly and it will be used as-is.

**Why didn't it report any closed jobs for one of my companies?**
Closure detection is deliberately skipped whenever the monitor cannot prove it read the whole board: the read failed, the board returned nothing, the run stopped early at its new-job limit, or the board is larger than **Maximum jobs read per company**. The reason is recorded per company in `RUN_SUMMARY`. Raising that limit fixes the last case.

**Will it ever send me the same role twice?**
Only if a run is interrupted between delivering a role and saving its memory — the monitor deliberately errs toward a visible duplicate rather than silently dropping a role you paid to be told about.

**Does zero results mean it's broken?**
No. Zero changes is the normal outcome on a quiet day and the run succeeds. A run only fails when no company could be resolved at all, or when every board came back empty — which is what a genuine breakage looks like from here.

**I want every open role, not just what changed.**
Use the [Company Career Page Scraper](https://apify.com/piotrv1001/company-career-page-scraper), which returns a company's full board on every run.

# Actor input Schema

## `startUrls` (type: `array`):

Company homepage URLs. The monitor finds each company's job board once and then re-checks it on every run. You can also paste a board URL directly (Greenhouse, Lever, Ashby, Workday, Oracle HCM, SmartRecruiters, Workable, Radancy).

## `maxNewJobsPerRun` (type: `integer`):

Stop after this many newly found roles. Anything beyond it is not lost — it is reported by the next run — so this caps what a single check can cost. The first run of a new monitor reports every open role it finds, so keep this in mind when watching large employers.

## `reportClosedJobs` (type: `boolean`):

Report roles that have come off a board since the last check — filled, pulled or expired. A closure is only reported after two consecutive checks miss it, so a board that answers oddly for one run is never mistaken for a wave of closures. Adds a charge per closed role.

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

Only report jobs whose title, department or team contains any of these. Leave empty to watch every role.

## `location` (type: `string`):

Only report jobs whose location matches this text (e.g. "United States", "Berlin"). Leave empty for all locations.

## `workplaceType` (type: `array`):

Only report remote, hybrid or on-site roles. Read from the field the employer's own system publishes, so this filters rather than merely appears to. Jobs whose source does not report the field are always kept.

## `employmentType` (type: `array`):

Only report these employment types (e.g. Full-time, Part-time, Contract, Internship). Jobs whose source does not report the field are always kept.

## `enrichWithAI` (type: `boolean`):

Additionally extract structured skills, requirements, responsibilities and benefits from each new job's description. Off by default: the fields the employer publishes are already structured, and this makes each check slower.

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

Optional. Runs sharing an ID share one memory of what has already been reported. Leave empty and the memory is tied to the exact companies and filters — which means editing any of them starts a fresh monitor that reports everything again. Set an ID if you want to add a company or change a filter later without being sent, and charged for, roles you already have.

## `maxJobsPerCompany` (type: `integer`):

How deep into each board a check reads. This is not a limit on results — it is how much of the board the monitor can see. A company with more open roles than this is reported as truncated and is left out of closed-job detection, because a partial view cannot tell a closed role from one further down the list. Raise it for very large employers.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.figma.com"
    },
    {
      "url": "https://www.notion.so"
    },
    {
      "url": "https://www.gitlab.com"
    }
  ],
  "maxNewJobsPerRun": 200,
  "reportClosedJobs": false,
  "keywords": [],
  "location": "",
  "workplaceType": [],
  "employmentType": [],
  "enrichWithAI": false,
  "maxJobsPerCompany": 500
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `runSummary` (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 = {
    "startUrls": [
        {
            "url": "https://www.figma.com"
        },
        {
            "url": "https://www.notion.so"
        },
        {
            "url": "https://www.gitlab.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("piotrv1001/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 = { "startUrls": [
        { "url": "https://www.figma.com" },
        { "url": "https://www.notion.so" },
        { "url": "https://www.gitlab.com" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("piotrv1001/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 '{
  "startUrls": [
    {
      "url": "https://www.figma.com"
    },
    {
      "url": "https://www.notion.so"
    },
    {
      "url": "https://www.gitlab.com"
    }
  ]
}' |
apify call piotrv1001/ats-jobs-monitor --silent --output-dataset

```

## MCP server setup

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