# Greenhouse, Lever, Ashby & Workable Jobs Scraper & Monitor (`mmdincer/ats-jobs-monitor`) Actor

Job board scraper for Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor: one schema from a company name, filters, posted salaries, and monitoring of new, updated and removed jobs. Public job feeds, no login. Not affiliated with Greenhouse, Lever, Ashby, Workable, Personio or Teamtailor.

- **URL**: https://apify.com/mmdincer/ats-jobs-monitor.md
- **Developed by:** [Muhammet Mustafa Dincer](https://apify.com/mmdincer) (community)
- **Categories:** Jobs, 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.05 / 1,000 job records

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?

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

![Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor jobs scraper and job change monitor: one schema for company job boards, with new, updated and removed job alerts](https://api.apify.com/v2/key-value-stores/Iraub1Fw8gpUk3az3/records/ats-banner-v1-1.png?signature=JsimTe6NANZQLMdm6acq)

### What is Greenhouse, Lever, Ashby, Workable, Personio & Teamtailor Jobs Scraper & Monitor?

**Greenhouse, Lever, Ashby, Workable, Personio & Teamtailor Jobs Scraper & Monitor** is an Apify Actor that reads the public job boards of companies and returns every open job in **one clean format**, no matter which hiring system the company uses. Type a company name such as `airbnb`, click **Start**, and the Actor finds the company's board on Greenhouse, Lever, Ashby, Workable, Personio or Teamtailor, downloads all open jobs and returns one row per job: title, department, team, locations, remote or hybrid, employment type, publish date, job and apply links, and the pay range when the employer posts one.

It is also a **job change monitor**. In **Changes** mode it compares each company with your previous run and returns only the **new, updated and removed jobs**. Put it on a schedule and you have a hiring tracker for competitors, target accounts, or a niche job board. It needs no login, no browser and no API key, because it reads the public job board APIs and feeds that these systems publish for company career pages.

### Quick answer: when should you (or an AI agent) pick this Actor?

**Pick this Actor when** you know the company (or a list of companies) and want its open jobs as clean data, or you want to be told when a company opens or closes jobs. Typical goals: recruiting and sourcing, sales prospecting from hiring signals, salary benchmarking, competitor hiring tracking, feeding a job board or a spreadsheet, or giving an AI agent a reliable "what is company X hiring for" tool.

**Pick something else when** you want to search jobs by keyword across the whole internet without naming companies, you need LinkedIn, Indeed or Glassdoor listings, or the company uses another hiring system (for example Workday, SmartRecruiters or BambooHR). This Actor only reads boards on Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor, and it does not return applicant data, recruiter contacts or emails.

**Defaults that work:** send `{"companies": ["airbnb", "palantir", "ashby"]}` to get the jobs of three companies, one on each of the first three systems. Add `workable:huggingface`, `personio:helpling` and `teamtailor:dustin` to read the other three. For alerts send `{"companies": ["airbnb"], "mode": "changes"}` and run it again later. The first Changes run saves a baseline and returns no job rows. Keep `monitorName` the same between runs, because runs with the same name compare against each other.

### At a glance

| | |
|---|---|
| **Does** | Reads company job boards on Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor, finds the right system from a company name, and returns every open job in one schema. A second mode returns only new, updated and removed jobs since the previous run |
| **Input** | `companies` (required, up to 300 names, domains or job board URLs). Optional: `mode`, `ats`, `titleKeywords`, `excludeKeywords`, `departmentKeywords`, `locationKeywords`, `remoteOnly`, `postedWithinDays`, `includeDescription`, `maxJobsPerCompany`, `changeTypes`, `monitorName` |
| **Output** | One row per job: `company`, `ats`, `title`, `department`, `team`, `location`, `locations`, `isRemote`, `workplaceType`, `employmentType`, `postedAt`, `url`, `applyUrl`, `salaryMin`, `salaryMax`, `salaryCurrency`, `salarySummary`. One summary row per company with job, remote and pay counts and jobs per department |
| **Change types** | `NEW_JOB`, `UPDATED_JOB` (title, location, department or pay changed), `REMOVED_JOB` (closed or filled) |
| **Needs** | No login, no API key, no browser. Apify Proxy is optional and normally not needed |
| **Speed** | A board with 700 jobs is read in a few seconds. A test run over six companies on six systems took under 7 seconds |
| **Pricing** | Pay per event. Free plan: $0.001 per run and $0.0015 per job in Jobs mode. In Changes mode $0.001 per company checked and $0.003 per detected change |
| **Does not** | Search across companies by keyword, read other hiring systems (Workday, SmartRecruiters, BambooHR and others), return applicants, recruiter contacts or emails, or guess pay when the employer does not publish it |

### Key terms used in this README

- **ATS (applicant tracking system):** the software a company uses to publish jobs and collect applications. This Actor supports Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor.
- **Job board:** the public list of a company's open jobs in its ATS, for example `jobs.lever.co/palantir`. The part after the host (`palantir`) is the **board name** (also called slug or board token).
- **Job row:** a dataset row with `recordType: "job"`. One open job, flat and self-contained.
- **Company row:** a dataset row with `recordType: "company"`. One per company and run, with the system it was found on, the outcome and counts.
- **Baseline:** the saved list of a company's jobs from the previous Changes run. The first run of a company only saves a baseline.
- **Detected change:** one new, updated or removed job found by comparing the board with the baseline. Each is one `change` row and one billable event.
- **Monitor:** a named baseline set with `monitorName`. Use one monitor per purpose, for example `competitors` and `target-accounts`.

### Which hiring systems are supported?

| System | Board address looks like | Prefix | Pay | Posted date | Notes |
|---|---|---|---|---|---|
| Greenhouse | `boards.greenhouse.io/<name>`, `job-boards.greenhouse.io/<name>` | `greenhouse:` | From the pay block in the job text | Yes, plus `updatedAt` | Departments and offices |
| Lever | `jobs.lever.co/<name>`, `jobs.eu.lever.co/<name>` | `lever:` | Structured | Yes | US and EU regions tried automatically |
| Ashby | `jobs.ashbyhq.com/<name>` | `ashby:` | Structured | Yes | Remote and workplace type |
| Workable | `apply.workable.com/<name>`, `<name>.workable.com` | `workable:` | Not published in the feed | Yes (date only) | Several sites per job |
| Personio | `<name>.jobs.personio.de`, `<name>.jobs.personio.com` | `personio:` | Structured when the company fills it in | Yes | Both hosts tried; no remote flag |
| Teamtailor | `<name>.teamtailor.com` | `teamtailor:` | Not published in the feed | Yes | Workplace type remote, hybrid or onsite; read 100 jobs per request |

Not supported on purpose: SmartRecruiters and BambooHR (their terms or `robots.txt` forbid automated access), Recruitee (an access token per company becomes mandatory in 2027).

### What can you use it for?

#### Recruiting and talent sourcing

Pull every open engineering, sales or design job of a list of companies into a sheet, filtered by title, department, location or remote. Use `titleKeywords` and `locationKeywords` to keep only the roles you recruit for, and `postedWithinDays` to see only fresh postings.

#### Sales prospecting from hiring signals

Companies that hire for a role often need the tools around it. A new batch of data engineer jobs can point to a data platform project, a wave of sales hires to a growth push. Monitor your target accounts in Changes mode and trigger outreach when `NEW_JOB` rows match your signal keywords. The company summary row also shows jobs per department, so you can see where a company is investing.

#### Competitor hiring tracking

Watch which teams a competitor grows and shrinks. `NEW_JOB` rows show new roles, `REMOVED_JOB` rows show roles that were closed or filled, and `UPDATED_JOB` rows show moved roles (for example a new location).

#### Salary benchmarking

Many boards publish pay ranges, and in some US states and EU countries employers have to. Rows carry `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryInterval` when the board states a range, so you can compare ranges by role and company. Rows without a published range keep these fields empty; nothing is estimated.

#### Job boards and AI agents

Feed a niche job board or a newsletter with fresh jobs from the companies you choose, or give an AI agent a dependable tool for "what is this company hiring for". Because every row has the same fields on all six systems, the agent does not need to know which ATS a company uses.

### How does it work?

1. For every entry in `companies` the Actor builds the likely board names (for example `Open AI, Inc.` becomes `openai` and `open-ai`). A job board URL or a prefix such as `lever:palantir` skips the guessing.
2. It asks the public job board API or feed of Greenhouse, Lever (including its EU region), Ashby, Workable, Personio (`.de` and `.com` hosts) and Teamtailor, and reads the board with the most open jobs. If the same name exists on more than one system, the company row says so. Companies without a board get a company row that says so and are not charged.
3. All jobs are converted to the same fields. Department, team, locations, remote status, employment type and pay come from each system's own data.
4. Your filters are applied and the newest jobs come first.
5. In **Jobs** mode the rows are written. In **Changes** mode the list is compared with the saved baseline, the changes are written, and the new baseline is saved. If a run stops early (for example because of the spending limit), the old baseline is kept, so nothing is lost.

### How to scrape the jobs of a company (step by step)

1. Open the Actor and enter one or more companies in **Companies**, one per line: a name (`airbnb`), a domain (`airbnb.com`) or a board URL (`https://jobs.lever.co/palantir`).
2. Keep **Mode** on `Jobs`. Optionally add filters, for example `engineer` under **Job title contains** or `remote` under **Location contains**.
3. Click **Start**. Open the **Output** tab to see the Jobs, Changes and Company summaries views, or download the dataset as JSON, CSV or Excel.
4. If a company is reported as not found, open its board in your browser (company career pages usually link to `jobs.lever.co/<name>`, `boards.greenhouse.io/<name>`, `jobs.ashbyhq.com/<name>`, `apply.workable.com/<name>`, `<name>.jobs.personio.de` or `<name>.teamtailor.com`) and use that name or URL.

### How to monitor job changes (step by step)

1. Set **Mode** to `Changes` and choose a **Monitor name**, for example `competitors`.
2. Run it once. Every company gets a baseline and you get only the company rows.
3. Save the input as a task and add a schedule (for example daily at 07:00).
4. From the second run on, you get one row per new, updated or removed job. Use **Change types to report** if you only want `NEW_JOB`.
5. If you change the keyword, location or remote filters, the Actor saves a new baseline instead of reporting every job as new. Keep filters stable for a monitor.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `companies` | list of text | `airbnb`, `palantir`, `ashby` | Names, domains or job board URLs. Prefix with `greenhouse:`, `lever:`, `ashby:`, `workable:`, `personio:` or `teamtailor:` to pick the system. Up to 300 per run |
| `ats` | choice | `auto` | `auto` asks all six systems and reads the board with the most open jobs. Pick one system to skip the search or to avoid mix-ups when two companies share a name |
| `mode` | choice | `jobs` | `jobs` lists all current jobs. `changes` returns only changes since the last run |
| `maxJobsPerCompany` | number | 1000 | Stops after this many jobs per company (newest first). Max 20,000 |
| `titleKeywords` | list of text | none | Keep jobs whose title contains any of these words |
| `excludeKeywords` | list of text | none | Drop jobs whose title contains any of these words |
| `departmentKeywords` | list of text | none | Keep jobs whose department or team contains any of these words |
| `locationKeywords` | list of text | none | Keep jobs with a location that contains any of these words |
| `remoteOnly` | yes/no | no | Keep only remote jobs |
| `postedWithinDays` | number | 0 | Jobs mode only: keep only jobs first published in the last N days (0 = no limit) |
| `includeDescription` | yes/no | no | Add the full description as plain text to each job row |
| `changeTypes` | list | all three | Changes mode: which changes become rows. Changes you leave out are not charged and are not reported later |
| `monitorName` | text | `default` | Changes mode: name of the saved baseline |
| `requestsPerSecond` | number | 2 | Request rate per system |
| `proxyConfiguration` | proxy | off | Optional; normally not needed |

### Output

Every run writes one dataset with three kinds of rows. Filter on `recordType`.

**Job row** (Jobs mode). Real row from a test run on an Ashby board (rows from the other five systems have the same fields):

```json
{
  "recordType": "job",
  "company": "ashby",
  "companyName": null,
  "ats": "ashby",
  "jobId": "d46e79d4-43fd-477f-aa06-25b604715223",
  "title": "Strategy & Operations, Customer Success",
  "department": "Sales",
  "team": "Revenue Operations",
  "location": "Remote - US",
  "locations": ["Remote - US", "Remote - Canada"],
  "isRemote": true,
  "workplaceType": "remote",
  "employmentType": "Full-time",
  "postedAt": "2026-11-02T18:22:27.542Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ashby/d46e79d4-43fd-477f-aa06-25b604715223",
  "applyUrl": "https://jobs.ashbyhq.com/ashby/d46e79d4-43fd-477f-aa06-25b604715223/application",
  "salaryMin": 168500,
  "salaryMax": 221875,
  "salaryCurrency": "USD",
  "salaryInterval": "year",
  "salarySummary": "$168,500 - $221,875",
  "checkedAt": "2026-11-03T11:09:42.824Z"
}
```

![Sample output of the Actor: job rows from six companies on Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor and one summary row per company](https://api.apify.com/v2/key-value-stores/Iraub1Fw8gpUk3az3/records/ats-sample-output-v1-1.png?signature=KzW4JagfsfgtbGQhAhdY)

**Change row** (Changes mode). This example is illustrative and shows the shape of an `UPDATED_JOB` row; the other change types have the same fields with empty `previous*` values:

```json
{
  "recordType": "change",
  "changeType": "UPDATED_JOB",
  "company": "palantir",
  "ats": "lever",
  "jobId": "ca222ecd-b659-4e3e-aee5-d85655c7bbc8",
  "title": "Software Engineer - Applied AI, Defense Applications",
  "location": "New York, NY",
  "changedFields": ["location"],
  "previousLocation": "Washington, D.C.",
  "previousTitle": "Software Engineer - Applied AI, Defense Applications",
  "url": "https://jobs.lever.co/palantir/ca222ecd-b659-4e3e-aee5-d85655c7bbc8",
  "previousCheckedAt": "2026-11-02T07:00:03.120Z",
  "checkedAt": "2026-11-03T07:00:02.874Z"
}
```

**Company row** (every run). One per company. It includes `status`, `ats`, `jobsOnBoard` (all open jobs), `jobsMatched` (after your filters), `jobsReported`, `remoteJobs`, `jobsWithSalary`, `departmentCounts` (jobs per department) and `locationCounts`. Real values from a test run on an Ashby board: 70 jobs on the board, 70 remote, 70 with a published pay range, 28 in Customer Success, 23 in Engineering and 14 in Sales. In Changes mode it also has `changesFound`, `changesReported` and `changeCounts`. Companies that could not be read have a `status` such as `not_found` and a `message` that tells why.

Field notes:

- `postedAt` is the first publish date (all six systems provide it; Workable gives a date without a time, Personio the creation time of the posting). `updatedAt` exists for Greenhouse only.
- `isRemote` is `true` when the board marks the job as remote or the location says remote, `false` for on-site or hybrid, and `null` when the board gives no hint.
- `employmentType` is normalized to `Full-time`, `Part-time`, `Internship`, `Contract` or `Temporary` where possible. Many Greenhouse boards do not set it, and Teamtailor feeds do not carry it. Personio adds `Working student` and `Full-time or part-time`.
- Pay comes from structured fields on Lever, Ashby and Personio (when the company filled in the salary of the posting). Workable and Teamtailor feeds contain no pay, so those rows keep the pay fields empty. Greenhouse does not return pay as a field, so the Actor reads the "pay range" block that many Greenhouse job texts contain; for Greenhouse `salaryInterval` is set to `year` only when the amounts are clearly annual.
- Location: Workable and Teamtailor list every site of a job; `location` is the first and `locations` has all of them. Personio gives the office and any additional offices. Teamtailor sets `workplaceType` to `remote`, `hybrid` or `onsite` from the job's remote status.
- `description` is only present when `includeDescription` is on.

### How much does it cost to scrape and monitor job boards?

You pay per event. The Free plan prices are below; paid Apify plans get lower prices per event.

| Event | When | Free plan price |
|---|---|---|
| Actor start | once per run | $0.001 |
| Job record | each job returned in Jobs mode | $0.0015 |
| Company check | each company checked in Changes mode | $0.001 |
| Job change | each new, updated or removed job in Changes mode | $0.003 |

Company rows, failed companies and companies without a board are free.

**Examples**

- 1,000 jobs in Jobs mode: $0.001 + 1,000 × $0.0015 = **$1.50**.
- All jobs of a company with 213 open jobs: about $0.32.
- Daily monitor of 100 companies with about 20 changes a day: 100 × $0.001 + 20 × $0.003 + $0.001 = **$0.16 per run**, about $5 per month. A day without changes costs only the company checks.

Set a **maximum cost per run** in the run options to cap spending; the Actor stops cleanly at the limit and tells you in the run summary. The platform's own compute cost for the Actor is small: in a test run, 588 jobs from three companies used about $0.0013 of compute.

### Greenhouse, Lever, Ashby, Workable, Personio & Teamtailor Jobs Scraper & Monitor compared with other approaches

| | This Actor | A scraper for one hiring system | Your own script |
|---|---|---|---|
| Systems covered | Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor in one run | One system per Actor | Whatever you write |
| Needs the board name | No: tries the company name, domain or URL on all six | Yes | Yes |
| Same fields on every system | Yes | Only within that system | You map the fields |
| Change tracking | Built in: new, updated and removed jobs, billed per change | Usually a full list each time, you diff it | You build and store the diff |
| Summary per company | Jobs, remote, pay counts and jobs per department | Not usually | You build it |
| Login or browser | No | Usually no | No |
| Maintenance | Handled by the Actor | Handled by the Actor | Yours |

The price per job is on the same level as other job board Actors in the Apify Store, most of which charge between $0.001 and $0.004 per job. The reason to choose this Actor is the combination of company-name lookup, one schema and change tracking, not the lowest possible price per row.

### Tips for best results

- **Use the board name from the board URL.** If `acme` is not found, open the company's careers page, click a job and read the host: `jobs.lever.co/acme-labs` means use `acme-labs`.
- **Pick the system when you know it.** `lever:palantir` skips the search and avoids picking the wrong board when two companies share a name.
- **Start small.** Use `maxJobsPerCompany` of 5 for a first test, then raise it.
- **Keep Changes filters stable.** A new filter means a new baseline.
- **Add keywords for signals.** Filter `titleKeywords` to the roles that matter to you instead of reading everything.
- **Leave descriptions off** unless you need them; rows get much larger with them.

### Limits and good to know

- Only Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor boards. Companies on other systems return `not_found`.
- **Systems that are deliberately not included:** SmartRecruiters (its public API forbids automated access in its `robots.txt`), BambooHR (its terms forbid robots and spiders and it has no documented public job feed) and Recruitee (a per-company access token becomes mandatory in 2027). They can be added later if their terms change.
- Workable and Teamtailor give the first publish date only; neither exposes pay. Personio feeds have no remote flag, so `isRemote` is `true` only when the office name says remote.
- If a name exists on more than one system (for example a small namesake company), the board with the most open jobs is read and the company row names the other boards. Choose the system with `ats` or a prefix to read another. A company that moved to a new system keeps its old, empty board for a while; the larger board wins.
- Only jobs that the company publishes on its public board are returned. Internal or unlisted jobs are not visible.
- Pay is returned only if the employer publishes it on the posting. Some Greenhouse boards write pay in free text only; those rows have no pay fields.
- Removed jobs are detected by absence from the board. If a board is cut off by `maxJobsPerCompany`, removed jobs are not reported for that run.
- `postedWithinDays` works in Jobs mode only. A moving date window would make old jobs look removed in Changes mode.
- Dates are shown in UTC (ISO 8601).

### Use it from AI agents, the API and code

**Apify MCP server (ChatGPT, Claude, Cursor and other MCP clients).** Connect `https://mcp.apify.com`, then follow these steps:

1. `search-actors` with a short query such as `Greenhouse Lever Ashby Workable jobs`.
2. `fetch-actor-details` for `mmdincer/ats-jobs-monitor` to read the input schema.
3. `call-actor` with the input below and a spending cap in `callOptions`.
4. `get-dataset-items` with the returned dataset ID. `call-actor` returns the dataset ID, not the rows.

```json
{
  "actor": "mmdincer/ats-jobs-monitor",
  "input": {
    "companies": ["airbnb", "palantir"],
    "mode": "jobs",
    "titleKeywords": ["engineer"],
    "maxJobsPerCompany": 50
  },
  "callOptions": { "maxTotalChargeUsd": 0.5 }
}
```

For monitoring, use `"mode": "changes"` with a fixed `monitorName`. The first call only saves a baseline, so tell the user that changes appear from the second call. This Actor uses limited permissions, so it is not blocked from the MCP server.

To give an MCP client this Actor as its own tool (no search step), connect `https://mcp.apify.com?tools=mmdincer/ats-jobs-monitor`.

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('mmdincer/ats-jobs-monitor').call(
    { companies: ['airbnb', 'palantir', 'ashby'], mode: 'jobs', remoteOnly: true },
    { maxTotalChargeUsd: 0.5 },
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const jobs = items.filter((row) => row.recordType === 'job');
```

**Python**

```python
import os
from decimal import Decimal
from apify_client import ApifyClient

client = ApifyClient(token=os.environ['APIFY_TOKEN'])
run = client.actor('mmdincer/ats-jobs-monitor').call(
    run_input={'companies': ['airbnb', 'palantir', 'ashby'], 'mode': 'jobs', 'remoteOnly': True},
    max_total_charge_usd=Decimal('0.5'),
)
items = client.dataset(run['defaultDatasetId']).list_items().items
jobs = [row for row in items if row['recordType'] == 'job']
```

**HTTP (one request, returns the rows)**

```bash
curl -X POST "https://api.apify.com/v2/acts/mmdincer~ats-jobs-monitor/run-sync-get-dataset-items?maxTotalChargeUsd=0.5" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies": ["airbnb"], "mode": "jobs", "maxJobsPerCompany": 20}'
```

The synchronous endpoint waits for the run to finish, so use it for a few companies. For many companies, start the run asynchronously and read the dataset when it ends.

### Automation recipes

**Slack or email alert through a webhook.** In the Apify Console open **Integrations** (or **Webhooks**) and add a webhook on **Run succeeded**. In your handler fetch the run's dataset (the webhook payload contains the run, including `defaultDatasetId`) and keep the rows where `recordType` is `change`. Send one message per row, for example: `NEW_JOB: {company} is hiring {title} ({location}) {url}`.

**Zapier.** Create a Zap with Apify as the trigger for a finished Actor or task run, add an Apify step that fetches the run's dataset items, filter for `recordType` equal to `change` and `changeType` equal to `NEW_JOB`, then add a Slack, Gmail or Google Sheets step. See the [Apify Zapier integration](https://docs.apify.com/integrations/zapier).

**Make.** Add the Apify module that runs an Actor (or watches for a finished run), then the module that gets dataset items, filter on `recordType` equal to `change` and route the bundles to your app. See the [Apify Make integration](https://docs.apify.com/integrations/make).

**Google Sheets or a database.** Fetch the dataset as JSON or CSV with the API (`/v2/datasets/{id}/items?format=csv`) after each run and append the rows. The `postedAt`, `department` and `salaryMin` columns make filtering and pivot tables easy.

**Scheduling.** Save your input as a task (for example `competitors`, daily at 07:00) and add a schedule in the Apify Console. Keep the same `monitorName` so every run compares with the previous one.

### Is it legal to scrape company job boards?

The Actor reads only the job postings that companies publish on purpose for everyone: it calls the public job board APIs and feeds of Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor that these systems offer so that companies can show their jobs on their own career pages (for Workable, Personio and Teamtailor the vendors document these feeds in their help centers). It does not log in, does not use an account or API key, does not read applicant data and does not return recruiter or candidate personal data. Job postings can contain the name of a hiring manager or recruiter in the text; descriptions are off by default. You are responsible for using the results in line with the terms of the companies and systems involved and with the laws that apply to you, including data protection rules if you combine the data with personal data.

**Trademarks.** Not affiliated with Greenhouse, Lever, Ashby, Workable, Personio or Teamtailor. This Actor is an independent tool and is not endorsed or sponsored by Greenhouse Software, Lever (Employ Inc.), Ashby, Workable, Personio, Teamtailor or any company whose jobs it returns. Trademarks belong to their respective owners. The names and marks are used only to say which systems the Actor reads; the icon shows the marks for the same purpose.

### FAQ

#### How do I get all open jobs of a company on Greenhouse, Lever, Ashby, Workable, Personio or Teamtailor?

Enter the company name, domain or job board URL in **Companies** and run the Actor in `jobs` mode. The Actor finds the board on one of the six systems (the one with the most open jobs) and returns every open job. Add `maxJobsPerCompany` to limit the size.

#### How do I find the board name of a company?

Open the company's careers page and click a job. If the address contains `jobs.lever.co/<name>`, `boards.greenhouse.io/<name>`, `job-boards.greenhouse.io/<name>`, `jobs.ashbyhq.com/<name>`, `apply.workable.com/<name>`, `<name>.workable.com`, `<name>.jobs.personio.de` (or `.com`) or `<name>.teamtailor.com`, then `<name>` is the board name. You can paste the whole URL into **Companies**.

#### Which hiring systems does it support?

Greenhouse, Lever (including the EU region), Ashby, Workable, Personio and Teamtailor. Other systems such as Workday, SmartRecruiters, BambooHR or Recruitee are not supported and return `not_found`. SmartRecruiters and BambooHR are left out on purpose because their terms do not allow automated access to their job data.

#### Can it tell me when a company posts a new job?

Yes. Use `changes` mode with a `monitorName`, run it once for the baseline and schedule it. From the second run, `NEW_JOB` rows list the new jobs, `REMOVED_JOB` rows the closed ones and `UPDATED_JOB` rows jobs whose title, location, department or pay changed.

#### Why did my first Changes run return no jobs?

The first run of a company saves a baseline and writes only the company row. Run it again later to get the changes. If you change filters, a new baseline is saved.

#### Does it return salaries?

Yes, when the employer publishes a pay range on the posting. Lever, Ashby and Personio provide structured pay data. Workable and Teamtailor feeds have no pay. For Greenhouse the Actor reads the pay range block in the job text when present. Jobs without a published range have empty pay fields. Nothing is estimated.

#### Does it return recruiter or hiring manager emails?

No. The Actor returns job data only: no emails, phone numbers, applicants or recruiter profiles.

#### How many companies and jobs can I read per run?

Up to 300 companies per run and up to 20,000 jobs per company. Large boards such as 900 jobs are read in a few seconds; Teamtailor boards are read 100 jobs per request.

#### How much does it cost to monitor 100 companies every day?

About $0.16 per run on the Free plan (100 company checks plus around 20 changes), roughly $5 per month. Days without changes cost only the company checks.

#### What happens if a company has no public board?

The company gets a row with `status: "not_found"` and a message that lists the names that were tried. It is not charged. If every company in the run fails, the run fails so that schedules show the problem.

#### Does it work with a company's own careers site?

If the careers site is powered by one of the six supported systems, yes: use the board name or the company domain. A custom careers page that is not backed by one of these systems cannot be read.

#### Does it work for companies in Europe?

Yes. Personio and Teamtailor are popular with companies in Germany and the Nordics, and Lever has an EU region that the Actor tries automatically. Job titles and locations come back in the language the company wrote them in.

#### Can I use it without writing code?

Yes. Use the Console form, then download the dataset as CSV or Excel, or connect it to Google Sheets, Slack, Zapier or Make with the recipes above.

#### What if I need another hiring system or more fields?

Open an issue on the Actor page with the company example and the data you need.

# Changelog

This Actor's version history is a separate document: https://apify.com/mmdincer/ats-jobs-monitor/changelog.md

# Actor input Schema

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

Companies to read, one per line. Use the company name (<code>airbnb</code>), a job board URL (<code>https://boards.greenhouse.io/airbnb</code>, <code>https://jobs.lever.co/palantir</code>, <code>https://jobs.ashbyhq.com/ashby</code>, <code>https://apply.workable.com/huggingface</code>, <code>https://helpling.jobs.personio.de</code>, <code>https://dustin.teamtailor.com</code>) or a company domain (<code>airbnb.com</code>). The Actor looks for the company's public board on all six systems and reads the one with the most open jobs. If the board name differs from the company name, use the name from the board URL, or add the system in front: <code>lever:palantir</code>, <code>workable:huggingface</code>, <code>personio:helpling</code>, <code>teamtailor:dustin</code>. Up to 300 companies per run.

## `ats` (type: `string`):

<b>Auto</b> looks for each company on Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor and reads the board with the most open jobs; the company row tells you when the same name also exists on another system. Choose one system to skip the search and avoid mix-ups when two different companies share a name. A prefix such as <code>lever:palantir</code> in the Companies list overrides this setting.

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

<b>Jobs</b> returns every current job that matches your filters. <b>Changes</b> compares each company with the previous run and returns only new, updated and removed jobs (the first run saves a baseline and returns no job rows).

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

Stops reading a company after this many jobs (newest first, after your filters). Keep it small for a quick test; the default covers almost every board. In Changes mode a cut-off board cannot report removed jobs.

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

Keep only jobs whose title contains at least one of these words or phrases, for example <code>engineer</code> or <code>product manager</code>. Not case sensitive. Leave empty for all jobs.

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

Drop jobs whose title contains any of these words, for example <code>intern</code> or <code>director</code>. Not case sensitive.

## `departmentKeywords` (type: `array`):

Keep only jobs whose department or team contains one of these words, for example <code>engineering</code> or <code>sales</code>.

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

Keep only jobs with at least one location that contains one of these words, for example <code>london</code>, <code>germany</code> or <code>remote</code>. Jobs with several locations match on any of them.

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

Keep only jobs the board marks as remote (workplace type remote, or a location that says remote).

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

Jobs mode only: keep only jobs first published in the last N days. 0 means no limit. Jobs without a publish date are dropped when this is set. Ignored in Changes mode.

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

Add the full job description as plain text to every job row. Rows get much larger; leave it off if you only need the job list.

## `changeTypes` (type: `array`):

Which changes become rows (Changes mode only). Leave all selected to get everything. Changes you leave out are not reported and are not charged, and they are not reported later either.

## `monitorName` (type: `string`):

Changes mode only: the name under which the baseline is saved (letters, digits, - and \_). Use different names to watch the same company with different filters or for different people. Keep the same name between runs.

## `requestsPerSecond` (type: `number`):

Maximum request rate per job board host. The default is polite and fast enough; lower it only if you see rate-limit warnings. Lever is always limited to one request per second, as its robots.txt asks.

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

Optional. The public job board APIs work from the Apify platform without a proxy. Turn on Apify Proxy only if a request is refused. Proxy traffic is billed by Apify separately.

## Actor input object example

```json
{
  "companies": [
    "airbnb",
    "palantir",
    "ashby"
  ],
  "ats": "auto",
  "mode": "jobs",
  "maxJobsPerCompany": 5,
  "titleKeywords": [
    "engineer",
    "designer"
  ],
  "excludeKeywords": [
    "intern"
  ],
  "departmentKeywords": [
    "engineering"
  ],
  "locationKeywords": [
    "london",
    "remote"
  ],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": false,
  "changeTypes": [
    "NEW_JOB",
    "UPDATED_JOB",
    "REMOVED_JOB"
  ],
  "monitorName": "default",
  "requestsPerSecond": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

One row per job (Jobs mode). The view also lists one summary row per company; keep rows with recordType job.

## `changes` (type: `string`):

One row per new, updated or removed job (Changes mode). The view also lists one summary row per company; keep rows with recordType change.

## `companies` (type: `string`):

One row per company with the system it was found on, the outcome, job counts, remote and pay counts, and the jobs per department. Companies without a public board show the reason here.

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

Companies checked and failed, jobs and changes delivered, warnings and the reason the run stopped.

# 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": [
        "airbnb",
        "palantir",
        "ashby"
    ],
    "ats": "auto",
    "mode": "jobs",
    "maxJobsPerCompany": 5,
    "remoteOnly": false,
    "includeDescription": false,
    "changeTypes": [
        "NEW_JOB",
        "UPDATED_JOB",
        "REMOVED_JOB"
    ],
    "monitorName": "default",
    "requestsPerSecond": 2,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("mmdincer/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": [
        "airbnb",
        "palantir",
        "ashby",
    ],
    "ats": "auto",
    "mode": "jobs",
    "maxJobsPerCompany": 5,
    "remoteOnly": False,
    "includeDescription": False,
    "changeTypes": [
        "NEW_JOB",
        "UPDATED_JOB",
        "REMOVED_JOB",
    ],
    "monitorName": "default",
    "requestsPerSecond": 2,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("mmdincer/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": [
    "airbnb",
    "palantir",
    "ashby"
  ],
  "ats": "auto",
  "mode": "jobs",
  "maxJobsPerCompany": 5,
  "remoteOnly": false,
  "includeDescription": false,
  "changeTypes": [
    "NEW_JOB",
    "UPDATED_JOB",
    "REMOVED_JOB"
  ],
  "monitorName": "default",
  "requestsPerSecond": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call mmdincer/ats-jobs-monitor --silent --output-dataset

```

## MCP server setup

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