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

Scrape job postings from company careers pages on Greenhouse, Lever, Ashby and Workday in one run. Give links, board slugs or company names; the job board is detected for you. One clean schema: title, location, remote, department, salary, dates, apply link, full description.

- **URL**: https://apify.com/quarra/ats-jobs-scraper.md
- **Developed by:** [Quarra Labs](https://apify.com/quarra) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 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/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

Get every open job from company careers pages hosted on **Greenhouse, Lever, Ashby and Workday**, in one run and one
clean schema. Paste careers page links, job links, board slugs or simply company names: the Actor detects the job
board system for you, reads each board in full and delivers each job with its title, locations, country, remote or
on-site, department, employment type, salary range, dates, apply link and full description. Export as JSON, CSV or
Excel, or read it through the Apify API.

**$2 per 1,000 jobs**, plus a small start fee per run. Jobs removed by your filters, duplicates and failed companies
are not charged.

### Why this one

- **Four systems, one schema.** The same fields for a Greenhouse, Lever, Ashby or Workday job: no per-source mapping
  on your side. Salaries are normalized to min, max, currency and period, from the job board's own pay data when it
  has some, or read in the description (`salarySource` says which).
- **Complete Workday boards.** Workday lists at most 2,000 jobs per search, and many scrapers stop there. Large sites
  are read part by part, as the site groups its jobs, until every job is delivered (NVIDIA: 2,600+ jobs in one run).
- **Company names that are proved, never guessed.** `Stripe` is used only when a board with exactly that company
  name is found. Two systems with the same name, or none: a free error record says so, nothing is picked for you.
- **Only new jobs.** Schedule a watch on your list of companies: each run delivers only the jobs it has not delivered
  before. Jobs already delivered are never charged again.
- **Hundreds of companies per run**, read in parallel. One company that fails does not stop the others.
- **Never a silent empty run.** Unknown company, closed job, unreadable answer: each gets a free error record with
  what to do, and the run summary lists every company with its count.

### Quick start

1. Paste your companies in **Companies**, one per line:
   `https://boards.greenhouse.io/airbnb`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/openai`,
   `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`, `Stripe`.
2. Add filters if you want (job title, location, remote, department, employment type, posted after, salary only).
3. Click **Start** and read the jobs in the run's **Output** tab.

Like any Apify Actor, it can also be run and read through the Apify API, scheduled, and connected to webhooks, Make,
Zapier, n8n or Apify's MCP server for AI agents.

### Inputs

| Field | What it does |
|---|---|
| **Companies** | Careers page links, job links, board slugs with their system (`greenhouse:airbnb`, `lever:palantir`, `ashby:openai`), company names, or the careers page of a company's own website (`stripe.com/jobs/search?gh_jid=…`). A name or a bare word is always a company NAME, used only when a board with exactly that name is found. Workday sites need their link (`…myworkdayjobs.com/<site>` or `…myworkdaysite.com/recruiting/<company>/<site>`), with the search words of the address (`?q=engineer`) if you use them. |
| **Max jobs per company** | Empty = every open job. |
| **Job title contains / does not contain** | Keep or leave out jobs by words of their title. |
| **Location contains** | Words of a location, a country name or a two-letter country code (`London`, `Germany`, `Remote`, `US`). Whole words: `IN` finds India, never Berlin; `US` never Australia. |
| **Workplace** | Any, remote, hybrid, on-site, remote or hybrid. |
| **Department contains** | Words of the department or team (not on Workday sites, which do not give it: a run with this filter on a Workday link says so, free). |
| **Employment type** | Full-time, part-time, contract, internship, temporary. Greenhouse gives no type of contract: it is read from the title when the title says it (Intern, Contract, Part-time); a job whose type stays unknown is left out by this filter, and the run says how many. |
| **Posted after** | A date (`2026-10-01`) or relative (`7 days`). |
| **Only jobs with a salary** | Keep jobs that state a pay range. |
| **Include the job description** | Plain text and HTML (on by default). |
| **Only new jobs** + **Watch name** | Deliver only the jobs this watch has not delivered before. |
| **Max results** | Total number of jobs of the run, counted exactly. |

A job that does not state what a filter checks (no salary, no department) is left out by that filter, and never
charged.

### Output

One record per job:

```json
{
  "type": "job",
  "status": "ok",
  "ats": "greenhouse",
  "company": "Discord",
  "companyBoard": "discord",
  "jobId": "8806482002",
  "title": "Commercial Policy Lead",
  "location": "San Francisco Bay Area",
  "locations": ["San Francisco Bay Area"],
  "country": null,
  "workplaceType": null,
  "department": "Policy",
  "team": null,
  "employmentType": null,
  "employmentTypeRaw": null,
  "publishedAt": "2026-09-15T16:52:11Z",
  "updatedAt": "2026-10-15T20:08:17Z",
  "salaryMin": 192000,
  "salaryMax": 216000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salaryText": "$192,000 to $216,000",
  "salarySource": "description",
  "url": "https://job-boards.greenhouse.io/discord/jobs/8806482002",
  "applyUrl": "https://job-boards.greenhouse.io/discord/jobs/8806482002",
  "requisitionId": "R-107406",
  "descriptionText": "Discord has a highly engaged community of millions of daily active users…",
  "descriptionHtml": "<div class=\"content-intro\"><p>Discord has a highly engaged community…",
  "input": "https://boards.greenhouse.io/discord",
  "position": 1,
  "scrapedAt": "2026-10-18T02:21:43Z"
}
```

- `country` is a two-letter code (ISO 3166) when the job board states it, or when the location names a known city (a list of
  cities of 15,000+ inhabitants, GeoNames, CC BY 4.0), a country, a US state or a Canadian province. `null` otherwise: never a
  guess (`Berlin, DE` is Germany; `Springfield, MO` is the US).
- `salaryRanges` lists every pay range when a job gives several (one per country or level); `salaryMin` / `salaryMax` span them
  all. Signing bonuses, stipends and budgets are not read as pay.
- `workplaceType` and `employmentType` are normalized (`remote` / `hybrid` / `onsite`; `fullTime`, `partTime`,
  `contract`, `internship`, `temporary`); `employmentTypeRaw` keeps the board's own words.
- `company` on Workday is the hiring entity of the job (Workday gives no brand name); `companyBoard` is stable.
- With **Only new jobs**, each job also has `"isNew": true`.

Error records (free) have `type: "error"`, an `errorCode`, a clear `error` message and `retryable`. The run summary
(OUTPUT record) gives, per company, the system, the board, the company name, the jobs delivered and a note.

### Pricing

Pay per event:

| Event | Price |
|---|---|
| Job | $2 per 1,000 (Apify Store discounts from Silver: $1.20 per 1,000) |
| Run start | $0.005 per GB of memory (1 GB minimum; $0.005 at the default memory) |

Never charged: jobs removed by your filters, a job given more than once (by its board and by its link: delivered once), jobs
already delivered to your watch, error records. Set **Maximum cost per run** in the run options and the run stops
before going over it.

### Limits

- Only the four systems: Greenhouse, Lever, Ashby and Workday. A company's own website is read for one page only, to find
  the system it embeds; if it does not say, give the job board link or the name.
- Company names are looked up on Greenhouse, Ashby and Lever. Workday sites cannot be found from a name: give the link
  as shown in your browser.
- Workday gives the publication day, not the hour, and no department. Salaries on Workday come from the description
  only.
- A search across every company of a system (all Greenhouse jobs that mention "Rust") is not offered: give the
  companies you follow.

### FAQ

**Do I need an account on these job boards?** No. The Actor reads the public careers pages any visitor sees.

**How do I find a company's careers page?** Paste the Jobs page of the company's own website as it is: the Actor reads that one page
and looks for the job board it embeds or links to (Greenhouse, Lever, Ashby or Workday). When the page does not say (many company sites load
it with a script), the run says so in a free error row: click a job and paste the address of the job board it opens
(`boards.greenhouse.io`, `jobs.lever.co`, `jobs.ashbyhq.com`, `myworkdayjobs.com`). A link with `gh_jid=` or `ashby_jid=` delivers only that job.

**How does "Only new jobs" work?** The watch remembers every job it delivered, by its identifier on its board. Use the
same **Watch name** in each scheduled run (without a name, the memory belongs to that exact list of companies and filters). The first run delivers every matching job; the next ones only the jobs
published since. One run of a watch at a time. Each run reads first the companies the watch read least recently.

**What if the run is restarted by the platform (migration or reboot)?** It resumes where it was: jobs already in the
dataset are not delivered or charged again.

**What if a job board changes?** The run says so in a free error record, instead of reporting "no jobs", and nothing
is charged for that company.

**Is it legal?** It collects public job postings published by companies to be read by anyone. Check the rules that
apply to your use of the data.

**Does the Actor send anything to Quarra?** Only when a run fails or runs out of time: the Actor, its build, the error type, the line of our code where it happened, and the memory and time the run was given, so we can fix it fast. Never your input, your results, your account or the run ID.

# Actor input Schema

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

One per line: a careers page link (boards.greenhouse.io/airbnb, jobs.lever.co/palantir, jobs.ashbyhq.com/openai, nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite), a job link, a board slug with its system (greenhouse:airbnb, lever:palantir, ashby:openai), or a company name (Stripe). A name is only used when a board with exactly that company name is found on Greenhouse, Ashby or Lever; Workday sites need their link.

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

Leave empty to get every open job of each company. Large Workday sites (more than 2,000 open jobs) are read in full, part by part.

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

Keep a job when its title contains any of these words (engineer, data, sales). Whole words only, not case sensitive: 'engineer' also finds Engineers and Engineering, 'IT' does not find Security.

## `excludeTitleKeywords` (type: `array`):

Leave out a job when its title contains any of these words (intern, senior). Whole words only: 'intern' finds Internship but not International.

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

Keep a job when one of its locations contains any of these words (London, New York, Remote), or when it is in one of these countries: a country name (Germany) or a two-letter country code (US, DE, GB). A two-letter code must be the exact country code, so 'IN' finds India but not Berlin, and 'US' does not find Australia.

## `workplace` (type: `string`):

Remote, hybrid or on-site, as the job board states it, or as the location says it (Remote - US).

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

Keep a job when its department or team contains any of these words (Engineering, Sales). Whole words only: 'IT' does not find Security.

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

Keep only these employment types. Leave empty for all.

## `postedAfter` (type: `string`):

Only jobs first published after this date (2026-10-01, or relative: 7 days).

## `salaryOnly` (type: `boolean`):

Keep only jobs that state a pay range (given by the job board, or written in the description).

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

Full description as plain text and as HTML. Turn off for lighter records.

## `onlyNew` (type: `boolean`):

Deliver only the jobs this watch has not delivered before. The first run delivers every matching job; the next runs with the same watch name deliver only the jobs published since. Made for scheduled runs: jobs already delivered are never charged again.

## `watchName` (type: `string`):

Name of the watch that remembers the jobs already delivered (used with 'Only new jobs'). Optional: without a name, the memory belongs to this exact list of companies and filters, so a different list is a different watch. Set a name to keep the same memory when you add or remove companies or change filters. Two different names never share a memory.

## `maxItems` (type: `integer`):

Total number of jobs delivered by the run, all companies together, counted exactly.

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

Connection. Keep the default (automatic). Costs are included in the price.

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

Set by the Actor.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/airbnb",
    "https://jobs.ashbyhq.com/linear",
    "https://jobs.lever.co/palantir",
    "Stripe"
  ],
  "workplace": "any",
  "salaryOnly": false,
  "includeDescription": true,
  "onlyNew": false,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "ats": "auto"
}
```

# Actor output Schema

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

Every job with all its fields, and free error records.

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

A table with one row per job.

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

Jobs per company, notes, why the run stopped, and 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": [
        "https://boards.greenhouse.io/airbnb",
        "https://jobs.ashbyhq.com/linear",
        "https://jobs.lever.co/palantir",
        "Stripe"
    ],
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("quarra/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/airbnb",
        "https://jobs.ashbyhq.com/linear",
        "https://jobs.lever.co/palantir",
        "Stripe",
    ],
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("quarra/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/airbnb",
    "https://jobs.ashbyhq.com/linear",
    "https://jobs.lever.co/palantir",
    "Stripe"
  ],
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call quarra/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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