# ATS Jobs Scraper: Greenhouse, Lever, Ashby & Workday (`rjmacarthy/ats-jobs-scraper`) Actor

Scrape job listings from company job boards on Greenhouse, Lever, Ashby and Workday into one normalized schema. Paste board URLs or careers pages, filter by keyword, location, remote or department, and use incremental mode to get only new jobs on a schedule. Pay per job. No browser or proxy.

- **URL**: https://apify.com/rjmacarthy/ats-jobs-scraper.md
- **Developed by:** [rj](https://apify.com/rjmacarthy) (community)
- **Categories:** Jobs, Automation, Developer tools
- **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 scrapeds

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 Scraper: Greenhouse, Lever, Ashby & Workday Job Listings

**Scrape job listings directly from company job boards on Greenhouse, Lever, Ashby and Workday, and get them back in one clean, normalized format.** Paste job board URLs or company careers pages and get every open role with title, department, location, remote flag, employment type, salary (where published), posting date and apply link.

Turn on **Only new jobs** and schedule the Actor to get a **daily feed of newly posted jobs** from the companies you track. Each run returns only jobs you haven't received before, so you only pay for what's new.

- ✅ **4 ATS platforms, 1 schema**: Greenhouse, Lever, Ashby and Workday in the same output format
- ✅ **Incremental "new jobs only" mode** for scheduled monitoring, with no duplicates and no re-billing
- ✅ **Paste any careers page**: the Actor finds the Greenhouse / Lever / Ashby / Workday board behind it
- ✅ **Live data from the source**: read straight from each company's public job board API at run time, not from a cached database
- ✅ **Cheap and fast**: $1.50 per 1,000 jobs, no browser, no proxy, about 30 seconds for 4,000 jobs
- ✅ **Filters**: keywords, excluded keywords, locations, remote only, departments, posted within N days, max jobs per company

### What does ATS Jobs Scraper do?

Most tech companies, startups and many enterprises publish their open positions through an **applicant tracking system (ATS)**. The four big ones are **Greenhouse, Lever, Ashby and Workday**. Each runs a public job board with its own URL format and data structure.

ATS Jobs Scraper reads those public job boards directly and returns:

- every open job on each board you give it (or only the new ones since the last run),
- with **the same field names for every ATS**, so you can merge Stripe (Greenhouse), Spotify (Lever), OpenAI (Ashby) and NVIDIA (Workday) into one table, spreadsheet or database without extra mapping,
- with stable, unique job IDs, so you can deduplicate and track jobs over time.

It's built for **job boards and aggregators, recruiters and sourcers, sales teams using hiring signals, market and talent researchers, and anyone building job alerts**.

### Why use this instead of other job scrapers?

| | ATS Jobs Scraper | Job databases / aggregated job APIs | Single-ATS scrapers |
|---|---|---|---|
| Data freshness | **Live from the company's job board at run time** | Indexed with a delay (often hours) | Live |
| Coverage | **The exact companies you choose** | Whatever the database indexed | One ATS only |
| Normalized schema across ATSs | **Yes** | Yes | No, you merge them yourself |
| "Only new jobs" mode for schedules | **Yes, the first run returns everything and later runs return only new jobs** | Time-window based | Rare |
| Accepts plain careers page URLs | **Yes (auto-detects the ATS)** | n/a | Usually no |
| Charged for failed or empty companies | **No** | Varies | Varies |
| Price | **$1.50 per 1,000 jobs** | Typically higher per job plus start fees | Varies |

### Supported ATS platforms and input formats

Put one company per line in **Companies**. You can mix formats freely.

| ATS | Job board URL example | Short form |
|---|---|---|
| **Greenhouse** | `https://boards.greenhouse.io/stripe` or `https://job-boards.greenhouse.io/stripe` | `greenhouse:stripe` |
| **Lever** | `https://jobs.lever.co/spotify` (EU-hosted `jobs.eu.lever.co` boards are supported) | `lever:spotify` |
| **Ashby** | `https://jobs.ashbyhq.com/ramp` | `ashby:ramp` |
| **Workday** | `https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite` | `workday:nvidia.wd5/NVIDIAExternalCareerSite` |
| **Any careers page** | `https://www.figma.com/careers` (the ATS is detected automatically) | n/a |
| **Bare slug** | `stripe` (tried on Greenhouse, then Ashby, then Lever) | n/a |

**Tip:** add ` | Company Name` to any line to set the company name in the output, e.g. `https://jobs.ashbyhq.com/openai | OpenAI`. Greenhouse provides the company name itself. For the other ATSs the name is derived from the board slug, so `openai` becomes "Openai" unless you set it.

**Careers page auto-detection** loads the page (and up to 3 "careers/jobs" links on the same site) and looks for links to, embeds of, or redirects to a Greenhouse, Lever, Ashby or Workday board. If none is found, the company is skipped with a clear warning in the log and the run summary, and you are not charged.

### How to use it

1. Open the Actor and paste your companies into **Companies**.
2. Optionally set filters (keywords, locations, remote only, …).
3. Click **Start**. Results appear in the **Output** tab and can be downloaded as JSON, CSV, Excel, XML or HTML.
4. For monitoring, turn on **Only new jobs** and create a **Schedule** (see below).

#### Input example

```json
{
    "companies": [
        "https://boards.greenhouse.io/figma",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp | Ramp",
        "https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite | NVIDIA",
        "https://www.notion.com/careers"
    ],
    "keywords": ["engineer", "developer"],
    "excludeKeywords": ["intern"],
    "locations": ["United States", "Remote"],
    "remoteOnly": false,
    "postedWithinDays": 14,
    "maxJobsPerCompany": 0,
    "includeDescription": false,
    "onlyNewJobs": true,
    "stateKey": "us-engineering-watchlist"
}
```

#### Input options

| Field | What it does |
|---|---|
| `companies` (required) | Job board URLs, careers pages, `ats:slug` or bare slugs. One per line. |
| `keywords` | Keep jobs whose **title** contains any keyword (case-insensitive, word start: `engineer` matches "Engineering"). |
| `keywordsMatchIn` | `title` (default) or `titleAndDescription`. |
| `excludeKeywords` | Drop jobs whose title contains any of these. |
| `locations` | Keep jobs whose location contains any of these texts (`London`, `Germany`, `Remote`, …). |
| `remoteOnly` | Keep only remote jobs. |
| `departments` | Keep jobs whose department or team contains any of these texts. |
| `postedWithinDays` | Keep jobs first published in the last N days. |
| `maxJobsPerCompany` | Cap per company, newest first (0 = no cap). |
| `includeDescription` | Add `descriptionHtml` and `descriptionText`. |
| `onlyNewJobs` | Incremental mode: output only jobs not delivered before for this `stateKey`. If you also set `maxJobsPerCompany`, each run delivers up to that many undelivered jobs and the rest arrive in later runs. To start with only genuinely new postings, do the first run without the limit. |
| `stateKey` | Name of the incremental memory. Use one per schedule or watchlist. |
| `resetState` | Forget seen jobs for this `stateKey` (run once after changing filters). |
| `proxyConfiguration` | Optional. Not needed for the public ATS APIs. |
| `maxConcurrency` | Companies scraped in parallel (default 5). |

### Output example

Every job has the same structure regardless of the ATS:

```json
{
    "id": "ashby:ramp:331dbcfd-3faf-4357-a7d8-3acd854b4b42",
    "ats": "ashby",
    "company": "Ramp",
    "title": "Physical Security Lead",
    "department": "Security",
    "team": "Security",
    "location": "New York, NY (HQ)",
    "locations": ["New York, NY (HQ)"],
    "isRemote": true,
    "workplaceType": "hybrid",
    "employmentType": "FULL_TIME",
    "salary": {
        "min": 134400,
        "max": 215000,
        "currency": "USD",
        "interval": "year",
        "text": "$134.4K - $215K",
        "source": "ats"
    },
    "postedAt": "2026-10-14T22:22:02.947Z",
    "updatedAt": null,
    "jobUrl": "https://jobs.ashbyhq.com/ramp/331dbcfd-3faf-4357-a7d8-3acd854b4b42",
    "applyUrl": "https://jobs.ashbyhq.com/ramp/331dbcfd-3faf-4357-a7d8-3acd854b4b42/application",
    "atsJobId": "331dbcfd-3faf-4357-a7d8-3acd854b4b42",
    "sourceBoard": "https://jobs.ashbyhq.com/ramp",
    "scrapedAt": "2026-10-15T08:11:59.989Z"
}
```

| Field | Description |
|---|---|
| `id` | Stable unique ID `{ats}:{board}:{atsJobId}`. Use it to deduplicate. |
| `ats` | `greenhouse`, `lever`, `ashby` or `workday` |
| `company`, `title`, `department`, `team` | As published (department/team are `null` where the ATS has none) |
| `location`, `locations` | Display location and the list of all locations |
| `isRemote`, `workplaceType` | Remote flag and `remote` / `hybrid` / `onsite` / `null` |
| `employmentType` | `FULL_TIME`, `PART_TIME`, `CONTRACT`, `TEMPORARY`, `INTERNSHIP` or `null` |
| `salary` | `{ min, max, currency, interval, text, source }` or `null`. `source: "ats"` = structured pay data from the ATS (Ashby, Lever). `source: "description"` = parsed from the pay-transparency text in the description (best effort). |
| `postedAt`, `updatedAt` | ISO 8601 dates (`updatedAt` only on Greenhouse) |
| `jobUrl`, `applyUrl` | Job page and application link |
| `descriptionHtml`, `descriptionText` | Only with **Include job descriptions** |
| `atsJobId`, `sourceBoard`, `scrapedAt` | ATS-native job ID, the board URL, and when it was scraped |

A per-company run report (jobs listed, matched, already seen, saved, errors) is saved as `SUMMARY` in the run's key-value store and printed at the end of the log.

### Pricing: $1.50 per 1,000 jobs

This Actor uses **pay-per-event** pricing with one event:

| Event | Price | When it's charged |
|---|---|---|
| `job` | **$0.0015** per job ($1.50 per 1,000) | Once for each job saved to the dataset |

Apify's standard tiny Actor-start fee may apply per run. Platform usage (compute, memory, storage) is **included**; you don't pay for it separately.

You are **not** charged for:

- jobs removed by your filters,
- jobs already delivered in a previous run (with **Only new jobs**),
- companies that fail, can't be found or have no matching jobs.

**Examples:**

- One-off export of 5 companies with 2,000 open jobs: **$3.00**.
- Daily monitoring of 100 companies that post ~40 new jobs a day in total: about **$0.06 per day** (~$1.80 a month). The first run costs more because it returns the full backlog.

**Spending limit:** set **Maximum cost per run** when you start the Actor. When the limit is reached, the Actor stops cleanly, keeps everything saved so far, and explains it in the status message. Jobs that didn't fit are *not* marked as seen, so the next incremental run delivers them.

**Free plan:** there are no special limits for free-plan users. The features and price are the same on every Apify plan, and you can use your monthly free platform credit for this Actor.

### Monitoring new jobs on a schedule (recommended)

1. Set **Companies** to your watchlist and turn on **Only new jobs**.
2. Give it a **State key** such as `fintech-watchlist`. Each schedule or watchlist should have its own key.
3. Run it once. The first run returns **all** currently open matching jobs and remembers them.
4. In Apify Console, go to **Schedules → Create new**, choose this Actor (or a saved task with your input), and pick a frequency, e.g. daily at 07:00.
5. Every following run returns **only jobs that appeared since the previous run**, so the dataset of each run is your "new jobs today" list.

How "seen" works: a job counts as seen only after it was **delivered (and charged)**. Jobs removed by filters, jobs cut by your spending limit, and companies that failed are not marked as seen, so nothing is lost. If you **change your filters**, run once with **Reset state** (or use a new state key) so jobs that previously didn't match can be delivered.

### Integrations: Zapier, Make, n8n, Slack, Google Sheets, webhooks and API

- **Webhooks**: add a webhook on *Run succeeded* to push new jobs to your backend, or to Slack via a workflow.
- **Zapier / Make / n8n**: use the Apify app to "Run Actor" on a schedule and "Get dataset items" to send new jobs to Google Sheets, Airtable, Notion, HubSpot, email or Slack.
- **Google Sheets / Excel**: export the dataset directly or connect with the Google Sheets integration.
- **AI agents and MCP**: the Actor can be called from the Apify MCP server and AI agent frameworks. Input and output are simple JSON.
- **API**: run it and fetch results in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/rjmacarthy~ats-jobs-scraper/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"companies":["https://boards.greenhouse.io/stripe","https://jobs.ashbyhq.com/ramp"],"onlyNewJobs":true,"stateKey":"api-feed"}'
```

### FAQ

**How do I find a company's job board URL?**
Open the company's careers page and click a job. The URL usually contains `greenhouse.io`, `lever.co`, `ashbyhq.com` or `myworkdayjobs.com`. Or paste the careers page itself and let the Actor detect it.

**The careers page was skipped with "No Greenhouse, Lever, Ashby or Workday job board found". Why?**
The company uses another ATS or its own career site, or the page loads its jobs with JavaScript from an unrecognizable source. Paste the job board URL directly if you can find it.

**Why did my department filter return 0 jobs for a company?**
Department names are whatever the company uses. Some use cost-center names like `8512 Terminal - Eng`. The filter is a case-insensitive substring match, so try shorter terms (`eng`) or use keywords on the title instead. Workday doesn't publish departments, so Workday jobs never match a department filter.

**Do I get duplicates across runs?**
Not with **Only new jobs**. Without it, every run returns all currently open matching jobs; use the stable `id` field to deduplicate.

**Can two schedules share one state key?**
Give each schedule its own key. Two runs using the same key at the same time could both deliver the same new job.

**Are salaries always available?**
No. Only where the employer publishes them. Ashby and Lever expose structured pay ranges for some jobs (`source: "ats"`). Pay-transparency ranges written in the job description (common for US postings) are parsed from the text (`source: "description"`). This always happens for Greenhouse, Lever and Ashby, and for Workday only when **Include job descriptions** is on.

**Do I need proxies?**
No. The Actor uses the ATSs' public JSON endpoints. A proxy option exists only for unusual blocking.

**Is this legal?**
The Actor only reads publicly available job postings that companies publish to attract applicants, using the same public endpoints their own career pages use. It does not log in or collect personal data. You are responsible for how you use the data. Check the terms that apply to your use case.

### Limits and known behavior

- **Workday**: Workday exposes at most **2,000 postings per career site** (newest first). Very large employers may have more. Without **Include job descriptions**, multi-location Workday jobs show the location as e.g. "2 Locations", and the posting date has day precision ("Posted 30+ Days Ago" has no exact date). With descriptions on, the Actor loads each job's detail page to get all locations, the exact date and the employment type. That costs one extra request per job but no extra charge. Workday has no public departments. `applyUrl` is the job URL plus `/apply`.
- **Company name** is published by Greenhouse. For Lever, Ashby and Workday it is derived from the board slug unless you add ` | Company Name`.
- **Greenhouse** public boards have no team or structured employment type. `employmentType` is set only when the title clearly says so (e.g. "Intern").
- **Auto-detection** is best effort and only covers the four supported ATSs.
- Other ATSs (Workable, SmartRecruiters, iCIMS, SuccessFactors, …) are not supported yet.
- Large boards with **Include job descriptions** produce big datasets (descriptions are often 5–15 KB each).

### Support

Found a board that doesn't work, or need another ATS? Open an issue on the Actor's **Issues** tab with the URL, and it will be looked at quickly.

# Actor input Schema

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

One company per line. Accepted formats:<br>• <b>Job board URL</b>: <code>https://boards.greenhouse.io/stripe</code>, <code>https://jobs.lever.co/spotify</code>, <code>https://jobs.ashbyhq.com/ramp</code>, <code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite</code><br>• <b>ATS + slug</b>: <code>greenhouse:stripe</code>, <code>lever:spotify</code>, <code>ashby:ramp</code>, <code>workday:nvidia.wd5/NVIDIAExternalCareerSite</code><br>• <b>Company careers page</b>: <code>https://www.figma.com/careers</code> (the ATS is auto-detected; skipped with a warning if none is found)<br>• <b>Bare slug</b>: <code>stripe</code> (tried on Greenhouse, Ashby, then Lever)<br>Optionally add <code> | Company Name</code> at the end to set the company name in the output.

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

Keep only jobs whose title contains at least one of these keywords (case-insensitive, matches word starts: <code>engineer</code> matches 'Engineering'). Leave empty to keep all jobs.

## `keywordsMatchIn` (type: `string`):

Where to look for the keywords. Matching descriptions is broader but noisier. For Workday it requires one extra request per candidate job.

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

Drop jobs whose title contains any of these keywords, e.g. <code>Intern</code>, <code>Senior</code>, <code>Manager</code>.

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

Keep only jobs whose location contains one of these texts (case-insensitive substring), e.g. <code>London</code>, <code>United States</code>, <code>Remote</code>, <code>Germany</code>.

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

Keep only jobs marked remote by the ATS or with 'Remote' in the location.

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

Keep only jobs whose department or team contains one of these texts, e.g. <code>Engineering</code>, <code>Sales</code>. Workday boards do not publish departments, so Workday jobs never match this filter.

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

Keep only jobs first published within this many days. Leave empty for any date.

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

Cap the number of jobs saved per company (newest first). 0 = no limit.

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

Add <code>descriptionHtml</code> and <code>descriptionText</code> to every job. Makes the dataset much larger. For Workday it adds one request per job.

## `onlyNewJobs` (type: `boolean`):

Remember which jobs were already delivered and output only jobs that are new since the previous run with the same state key. The first run outputs all matching jobs and remembers them. You pay only for new jobs. Ideal for scheduled runs.

## `stateKey` (type: `string`):

Name of the memory used by incremental mode. Use a different key for each schedule or watchlist so they don't interfere (letters, numbers, - \_ . only).

## `resetState` (type: `boolean`):

Forget previously seen jobs for this state key before running, so all matching jobs are output again. Use it after changing filters. Turn it off again afterwards.

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

Not needed for the public ATS APIs. Use a proxy only if you see blocking (HTTP 403/429) on Workday or careers pages.

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

How many companies are scraped in parallel.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/figma",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear"
  ],
  "keywordsMatchIn": "title",
  "remoteOnly": false,
  "maxJobsPerCompany": 0,
  "includeDescription": false,
  "onlyNewJobs": false,
  "stateKey": "default",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One item per job listing: title, company, ATS, location, remote flag, employment type, salary, posting date and apply URL.

## `jobsFull` (type: `string`):

Every field for each job, including descriptions when includeDescription is on.

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

Per-company status and counts (listed, matched, saved), including any failed or skipped companies.

# 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": [
        "https://boards.greenhouse.io/figma",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/linear"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("rjmacarthy/ats-jobs-scraper").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": [
        "https://boards.greenhouse.io/figma",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/linear",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("rjmacarthy/ats-jobs-scraper").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": [
    "https://boards.greenhouse.io/figma",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call rjmacarthy/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rjmacarthy/ats-jobs-scraper"
        }
    }
}
```

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/sbq9RfzKIsUMuFxUx/builds/42ohhVnhIdstG5d4H/openapi.json
