# ATS Jobs Monitor – Greenhouse, Lever, Workable, Personio (`mikestart/ats-jobs-monitor`) Actor

Track open jobs at any company on Greenhouse, Lever, Workable or Personio via official APIs. One schema, filters, salary, and a monitor mode for new jobs only. This is not personalized advice. Material created also with the use of AI tools.

- **URL**: https://apify.com/mikestart/ats-jobs-monitor.md
- **Developed by:** [Mike](https://apify.com/mikestart) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Security:** Limited permissions
- **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 rows

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 Jobs Monitor – Greenhouse, Lever, Workable & Personio job boards

Get every open position of the companies you follow, straight from the **official public job-board APIs** of their
applicant tracking system (ATS), in **one clean schema** – and schedule it in **monitor mode** to receive **only the jobs
posted since the last run**.

- **4 ATS, one schema**: Greenhouse, Lever (global + EU), Workable, Personio (XML feed). No browser, no login, no proxies.
- **Monitor mode**: new jobs only (optionally also changed and closed jobs), with `firstSeenAt`, `changedFields` and `closedAt`.
- **Flexible input**: `greenhouse:gitlab`, a job-board URL, a careers-page URL (the embedded board is detected) or a bare company name.
- **Filters**: title keywords, exclusions, locations, countries, remote only, employment type, posted within N days.
- **Salary** when the company publishes it (Lever salary ranges, or a range written in the description).
- **Privacy by design**: recruiter names, personal e-mails and phone numbers are removed from descriptions; ATS custom fields (which can contain people) are never copied.
- **Pay only for results**: per company checked and per job row; unknown boards and errors are free.

### Use cases

- **Recruiters and agencies** – a daily feed of new roles at target accounts.
- **Sales intelligence** – "who is hiring for what" signals (e.g. companies opening data-engineering roles in Germany).
- **Niche job boards** – keep listings in sync with the employers' own boards.
- **Job seekers** – follow 20–200 companies and get notified only when something new appears.
- **Labour-market research** – snapshots of open roles by department, country and work model.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array | – | Required. `ats:slug`, job-board URL, careers-page URL or bare name (see below). Max 1,000 per run. |
| `mode` | `all` | `monitor` | `all` | `monitor` outputs only changes since the previous run. |
| `monitorReport` | array | `["new"]` | Which changes to output in monitor mode: `new`, `changed`, `closed`. |
| `firstRunBehavior` | `emit-all` | `baseline-only` | `emit-all` | For a company seen for the first time: output all jobs as new, or only save a baseline. |
| `monitorStateKey` | string | `ats-monitor-state` | Named key-value store holding the monitor state. One per watch-list. |
| `keywords` / `excludeKeywords` | array | – | Title (and department/team) contains any / none of these words. |
| `locations` | array | – | Location text contains any of these strings. |
| `countries` | array | – | ISO-3166 alpha-2 codes (`IT`, `DE`, `US`…). |
| `includeRemoteInLocationFilter` | boolean | `true` | Remote jobs pass location/country filters. |
| `remoteOnly` | boolean | `false` | Only remote jobs. |
| `employmentTypes` | array | – | `full-time`, `part-time`, `contract`, `internship`, `temporary`. |
| `postedWithinDays` | integer | – | Only jobs published in the last N days. |
| `includeDescription` | `none` | `text` | `html` | `text` | Description format (personal contact data always removed). |
| `maxJobsPerCompany` | integer | `1000` | Safety cap. |
| `personioLanguage` | string | – | Language of Personio feeds, e.g. `en`. |
| `trackDescriptionChanges` | boolean | `false` | In monitor mode, also treat description edits as changes. |
| `probeBareNames` | boolean | `true` | Try a bare name on Greenhouse → Lever → Workable → Personio. |
| `maxConcurrency` | integer | `5` | Companies processed in parallel (requests per ATS host are paced anyway). |

#### How to reference a company

| You give | Example |
|---|---|
| Prefixed slug (fastest) | `greenhouse:gitlab`, `lever:spotify`, `workable:huggingface`, `personio:ottonova` |
| Job-board URL | `https://job-boards.greenhouse.io/gitlab`, `https://jobs.lever.co/spotify`, `https://apply.workable.com/huggingface/`, `https://ottonova.jobs.personio.de` |
| Careers page | `https://example.com/careers` – the page is loaded once (if its robots.txt allows) and the embedded board is detected |
| Bare name | `gitlab` – probed on the 4 supported ATS, one request each |

#### Example: daily "new jobs" feed

```json
{
  "companies": ["greenhouse:gitlab", "lever:spotify", "workable:huggingface", "personio:ottonova"],
  "mode": "monitor",
  "monitorReport": ["new"],
  "firstRunBehavior": "baseline-only",
  "keywords": ["engineer", "data"],
  "countries": ["DE", "IT", "GB"],
  "includeDescription": "none"
}
```

Create a **Schedule** in Apify Console (e.g. every day at 08:00) with this input and connect an integration
(e-mail, Slack, webhook, Google Sheets) to the dataset: each run contains only the jobs that appeared since the previous run.
Use a different `monitorStateKey` for each independent watch-list.

### Output

One dataset row per job (or per change in monitor mode). Example (real data, description shortened):

```json
{
  "jobId": "lever:spotify:2a55d6c9-c564-443a-be3e-a3851db5a93b",
  "ats": "lever",
  "companySlug": "spotify",
  "companyName": "Spotify",
  "title": "Account Executive - Commercial, Spotify Technology",
  "department": "Operations and Business Support",
  "team": "Platform",
  "employmentType": "full-time",
  "workplaceType": "hybrid",
  "isRemote": false,
  "locations": ["New York, NY"],
  "country": "US",
  "salary": { "min": 87069, "max": 124385, "currency": "USD", "period": "year", "source": "text", "raw": "$87,069-$124,385" },
  "postedAt": "2026-10-23T15:00:40.768Z",
  "jobUrl": "https://jobs.lever.co/spotify/2a55d6c9-c564-443a-be3e-a3851db5a93b",
  "applyUrl": "https://jobs.lever.co/spotify/2a55d6c9-c564-443a-be3e-a3851db5a93b/apply",
  "description": "Spotify Technology is our business-to-business software team, …",
  "changeType": null,
  "changedFields": [],
  "firstSeenAt": null,
  "closedAt": null,
  "scrapedAt": "2026-10-28T15:35:12.120Z"
}
```

| Field | Notes |
|---|---|
| `jobId` | Stable id `ats:slug:nativeId` – use it to de-duplicate. |
| `companyName` | From the ATS when available (Greenhouse, Workable, Personio), otherwise derived from the slug. |
| `employmentType` | Normalised: `full-time`, `part-time`, `contract`, `internship`, `temporary` or `null`. |
| `workplaceType` / `isRemote` | `remote`, `hybrid`, `onsite` when the ATS provides it; `isRemote` also uses the location text. |
| `country` | ISO code from the ATS or derived from the location; `null` for multi-country or unknown locations. |
| `salary` | `source: "ats"` for structured ranges, `"text"` when parsed from the description; `null` if not published. |
| `changeType` | Monitor mode only: `new`, `changed` (`changedFields` lists what changed) or `closed` (`closedAt`). |

A **`SUMMARY`** record in the default key-value store lists every input entry with its ATS, status
(`ok`, `board-not-found`, `ats-not-detected`, `excluded-ats`, `error`, `duplicate`), job and row counts, plus run totals.

### Supported and excluded ATS

| ATS | Source used | Status |
|---|---|---|
| Greenhouse | Job Board API `boards-api.greenhouse.io/v1/boards/{token}/jobs` (documented as public, no auth for GET) | ✅ |
| Lever | Postings API `api.lever.co/v0/postings/{company}` and EU instance (documented as public; robots crawl-delay 1 s respected) | ✅ |
| Workable | Public jobs endpoint `www.workable.com/api/accounts/{subdomain}` (documented for career-page widgets) | ✅ |
| Personio | Career-site XML feed `{company}.jobs.personio.de/xml` (documented, enabled by the company) | ✅ |
| Ashby | robots.txt of the API host not verifiable and `/api/` disallowed on the jobs host | ❌ recognised, not fetched |
| Recruitee | Careers Site API moving to company-issued tokens (Feb 2027) | ❌ recognised, not fetched |
| SmartRecruiters | robots.txt disallows all crawlers | ❌ recognised, not fetched |
| Workday | no documented public job-board API | ❌ recognised, not fetched |

Excluded boards appear in the `SUMMARY` with the reason and are **not charged**.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start | $0.005 per run |
| Company checked (board found and read successfully) | $0.002 per company |
| Job row | $0.0015 per row ($1.50 per 1,000) |

Monitor mode charges only the rows it outputs: following 50 companies daily with ~10 new jobs/day costs about
$0.12/day (≈ $3.60/month). Unknown, excluded or failing boards cost nothing. The run stops cleanly when your
maximum cost per run is reached.

### Limits and notes

- Only jobs that the company publishes on its public job board are returned (no internal or unlisted jobs).
- Salary is extracted only when the company publishes it; the first range in the description is used.
- Requests are paced per host (Lever: 1 request/second as asked by its robots.txt) and retried with back-off on 429/5xx.
- If a board fails during a monitor run, its saved state is left untouched, so jobs are never falsely reported as closed or new.
- Descriptions are scrubbed of personal contact data with heuristics: generic mailboxes such as `jobs@company.com` are kept.

### Legal & data protection

The Actor reads only endpoints that the ATS vendors document for public use, identifies itself with an honest
User-Agent, respects robots.txt and rate limits, and does not log in or bypass any protection. Job postings are
public business information; the Actor does **not** collect recruiter or contact-person data. You are responsible
for using the data in compliance with the terms of the job boards and applicable law (e.g. GDPR) in your use case.
Some ATS hosts signal `ai-train=no`: do not use the output to train AI models.

### Support

Open an issue on the Actor's Issues tab with the input you used (company list) and the run ID.

### Disclaimer

This is not personalized advice. Material created also with the use of AI tools.

# Actor input Schema

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

One entry per company. Accepted: a prefixed slug (<code>greenhouse:gitlab</code>, <code>lever:spotify</code>, <code>workable:huggingface</code>, <code>personio:ottonova</code>), a job-board URL (<code>https://job-boards.greenhouse.io/gitlab</code>, <code>https://jobs.lever.co/spotify</code>, <code>https://apply.workable.com/huggingface</code>, <code>https://ottonova.jobs.personio.de</code>), a careers-page URL (the embedded board is detected) or a bare name (probed on the 4 supported ATS). Ashby, Recruitee, SmartRecruiters and Workday boards are recognised but not fetched (see README).

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

<b>all</b>: every open job each run. <b>monitor</b>: the Actor remembers what it saw (in a named key-value store) and outputs only jobs that are new since the previous run – schedule it daily for a "new jobs" feed.

## `monitorReport` (type: `array`):

Which changes monitor mode outputs. Default: only new jobs.

## `firstRunBehavior` (type: `string`):

What to do for a company the monitor has never seen.

## `monitorStateKey` (type: `string`):

Name of the key-value store that keeps the monitor state. Use different names for independent watch-lists.

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

Keep jobs whose title (or department/team) contains any of these words (case-insensitive). Empty = all.

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

Drop jobs whose title contains any of these words, e.g. "senior", "intern".

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

Keep jobs whose location text contains any of these strings, e.g. "Berlin", "Milan", "London".

## `countries` (type: `array`):

Keep jobs in these countries (2-letter codes, e.g. "IT", "DE", "US"). Country is derived from the location text when the ATS does not provide it.

## `includeRemoteInLocationFilter` (type: `boolean`):

When location/country filters are set, also keep jobs marked remote.

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

Keep only jobs marked remote by the ATS (or with "remote" in the location).

## `employmentTypes` (type: `array`):

Keep only these employment types (jobs without a type are kept).

## `postedWithinDays` (type: `integer`):

Keep only jobs published in the last N days (when the ATS exposes a date).

## `includeDescription` (type: `string`):

Personal contact data (recruiter names, personal e-mails, phone numbers) is always removed from descriptions.

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

Safety cap per company and run.

## `personioLanguage` (type: `string`):

Optional language code for Personio feeds (e.g. "en", "de"), if the company publishes several.

## `trackDescriptionChanges` (type: `boolean`):

By default only title, department, location, type, workplace and salary changes are tracked.

## `probeBareNames` (type: `boolean`):

For a bare name like "gitlab", try Greenhouse → Lever → Workable → Personio (one request each, stops at the first hit).

## `maxConcurrency` (type: `integer`):

Companies processed in parallel. Requests to the same ATS host are additionally paced.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:gitlab",
    "https://jobs.lever.co/spotify",
    "workable:huggingface",
    "personio:ottonova"
  ],
  "mode": "all",
  "monitorReport": [
    "new"
  ],
  "firstRunBehavior": "emit-all",
  "monitorStateKey": "ats-monitor-state",
  "includeRemoteInLocationFilter": true,
  "remoteOnly": false,
  "includeDescription": "text",
  "maxJobsPerCompany": 1000,
  "trackDescriptionChanges": false,
  "probeBareNames": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Job postings normalized into one schema (title, company, location, salary, URLs, change info in monitor mode).

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

Run summary: companies checked, jobs found, rows output and per-company status/errors.

# 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": [
        "greenhouse:gitlab",
        "https://jobs.lever.co/spotify",
        "workable:huggingface",
        "personio:ottonova"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mikestart/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": [
        "greenhouse:gitlab",
        "https://jobs.lever.co/spotify",
        "workable:huggingface",
        "personio:ottonova",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("mikestart/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": [
    "greenhouse:gitlab",
    "https://jobs.lever.co/spotify",
    "workable:huggingface",
    "personio:ottonova"
  ]
}' |
apify call mikestart/ats-jobs-monitor --silent --output-dataset

```

## MCP server setup

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