# Greenhouse, Lever, Ashby & Workday Job Board Scraper (`mdcurt/ats-job-board-scraper`) Actor

Scrape every open position from company career sites on Greenhouse, Lever, Ashby, Workday or SmartRecruiters via their public job-board APIs: title, location, department, salary, posted date, description snippet, apply link. Keyword + location filters, incremental mode, first record in ~1 s.

- **URL**: https://apify.com/mdcurt/ats-job-board-scraper.md
- **Developed by:** [Matthew Curtis](https://apify.com/mdcurt) (community)
- **Categories:** Jobs, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.96 / 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 & Workday Job Board Scraper

*Also SmartRecruiters — five ATS platforms, one Actor.*

Scrape every open position from any company career site hosted on **Greenhouse**, **Lever**, **Ashby**, **Workday** or **SmartRecruiters** — five applicant-tracking systems behind tens of thousands of career pages — through their public job-board APIs. No browser, no login, no HTML parsing: a whole Greenhouse, Lever or Ashby board arrives in one request, so the first job record lands in about a second and a 1,000-job board costs a few seconds of compute.

Paste career-site URLs (or a single job URL for an instant lookup), optionally filter by keywords and location, and get one clean record per job: title, company, location(s), country, remote / hybrid / onsite, department, team, employment type, salary range when the company publishes one, requisition id, posted and updated dates, a description snippet and the apply link. Ids are stable across runs, so scheduled runs with `knownIds` only pay for new postings.

### What it does

| ATS | Career-site URL shape | Transport | Jobs per request |
|---|---|---|---|
| Greenhouse | `boards.greenhouse.io/<company>`, `job-boards.greenhouse.io/<company>`, `…/embed/job_board?for=<company>` | Job Board API (`boards-api.greenhouse.io`) | whole board (content + pay ranges) |
| Lever | `jobs.lever.co/<company>`, `jobs.eu.lever.co/<company>` | Postings API (`api.lever.co`) | whole board |
| Ashby | `jobs.ashbyhq.com/<company>` | Posting API (`api.ashbyhq.com`) with compensation | whole board |
| Workday | `<tenant>.wd<n>.myworkdayjobs.com/[<locale>/]<site>` | the career site's own JSON (`/wday/cxs/…/jobs`) | 20 per page (+1 per job with details) |
| SmartRecruiters | `jobs.smartrecruiters.com/<Company>`, `careers.smartrecruiters.com/<Company>` | Posting API (`api.smartrecruiters.com`) | 100 per page (+1 per job with details) |

A single job URL on any of the five (`…/airbnb/jobs/8306554`, `jobs.lever.co/palantir/<uuid>`, `jobs.ashbyhq.com/openai/<uuid>`, a Workday `/job/<location>/<title>_<REQ>` link, `jobs.smartrecruiters.com/<Company>/<id>-…`) returns that one job. The short forms `greenhouse:airbnb`, `lever:palantir`, `ashby:openai`, `smartrecruiters:BoschGroup` work too.

Custom-domain career pages (`careers.example.com`) are out of scope unless they are one of the hosted URLs above — the Actor cannot know which ATS sits behind an arbitrary domain. Open the company's "View all jobs" link and paste the hosted URL instead.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `boards` | string\[] | `["https://boards.greenhouse.io/airbnb"]` | Career-site URLs, single job URLs or `ats:company` short forms. Several boards share `maxItems`. |
| `query` | string | — | Keywords: every word must appear in the title, department, team, location or employment type (case-insensitive). Workday and SmartRecruiters are also searched server-side first, so large boards stay cheap. |
| `location` | string | — | Keep only jobs whose location text, country code or workplace type contains this (`London`, `US`, `remote`). |
| `maxItems` | integer | 500 | Total records to return across all boards (1–10,000). |
| `fetchDetails` | boolean | false | Workday and SmartRecruiters list rows carry no description; turn this on to open each of their jobs (one extra request per job) for the description snippet, employment type and country. Greenhouse, Lever and Ashby lists already contain everything, so no extra requests are made for them. |
| `knownIds` | string\[] | `[]` | `id` values you already have: skipped without a request, and a page made only of known ids stops pagination. |
| `proxyConfiguration` | object | Apify Proxy | Datacenter proxies work; the APIs are public. |

### Output

One record per job. Field names follow the convention shared by all mdcurt Actors (`id`, `url`, `title`, `location`, `postedAt`, `description` ≤ 300 chars, `kind`, `source`, `scrapedAt`).

```json
{
  "kind": "listing",
  "id": "ashby:openai:240d459b-696d-43eb-8497-fab3e56ecd9b",
  "ats": "ashby",
  "board": "openai",
  "company": null,
  "url": "https://jobs.ashbyhq.com/openai/240d459b-696d-43eb-8497-fab3e56ecd9b",
  "applyUrl": "https://jobs.ashbyhq.com/openai/240d459b-696d-43eb-8497-fab3e56ecd9b/application",
  "title": "Research Engineer",
  "location": "San Francisco",
  "locations": ["San Francisco"],
  "country": "United States",
  "remote": null,
  "workplaceType": null,
  "department": "Research",
  "team": "Research",
  "employmentType": "FullTime",
  "salary": "$250K – $445K • Offers Equity",
  "salaryMin": 250000,
  "salaryMax": 445000,
  "currency": "USD",
  "requisitionId": null,
  "postedAt": "2025-04-05T00:03:20.653Z",
  "updatedAt": null,
  "description": "By applying to this role, you will be considered for Research Engineer roles across all teams at OpenAI…",
  "source": { "ats": "ashby", "board": "openai" },
  "scrapedAt": "2026-11-02T00:24:49.000Z"
}
```

- `id` is `<ats>:<board>:<native id>` (Greenhouse job id, Lever/Ashby UUID, Workday requisition id such as `JR2028280`, SmartRecruiters posting id) — stable across runs and safe to feed back as `knownIds`.
- `salary` / `salaryMin` / `salaryMax` / `currency` are filled only when the company publishes a range (Ashby compensation, Greenhouse pay-transparency ranges, Lever salary ranges). Workday and SmartRecruiters do not expose structured pay.
- `postedAt` is the board's publish date; Workday only says "Posted N Days Ago", which is converted to a date at day precision (`null` for "30+ Days Ago").
- `workplaceType` is `remote` / `hybrid` / `onsite` when the board states it (or the title/location says so), otherwise `null`.
- `kind` is `listing` for list-call records and `detail` for Workday/SmartRecruiters jobs opened with `fetchDetails`.

The key-value store holds a `SUMMARY` record: boards by ATS, jobs seen and filtered out, pages stopped by known ids, duplicates, failed requests, p95 latency, time to first record and compute seconds.

### Pricing

Pay per event: **$0.005 per run start + $0.0012 per job record** (tier discounts for paid plans). A one-job lookup costs $0.0062; a full 800-job Ashby board costs under $1. That is 40 % under the $0.002 median of Greenhouse/Lever/Ashby/Workday scrapers on the Store because a whole board is one request — there is no browser and no per-job fetch to pay for.

### Monitoring new postings cheaply (incremental mode)

Schedule the Actor and pass the `id` values of your last dataset as `knownIds`: known jobs are skipped without a request, and on Workday / SmartRecruiters pagination stops at the first page made only of known ids, so a daily run pays for new postings only. Pair it with [`mdcurt/dataset-diff-monitor`](https://apify.com/mdcurt/dataset-diff-monitor) to get an alert when a company opens, changes or closes a role.

### Speed (live tests, datacenter egress)

- One-job lookup: first record in 0.9 s, run done in ~4 s.
- Lever board, 301 jobs, keyword filter: 60 records in 8.5 s (the Lever API returns full descriptions, ~6 MB for a 300-job board).
- Ashby + Greenhouse, 949 jobs, location filter: 150 records, first in 1.2 s, run done in 5 s (~5,000 records/minute).
- Workday + SmartRecruiters with details: 80 detail records in 27 s (~180 records/minute, 85 requests).
- Seven boards, 3,000 records with details: see `tests.md`.

### Limitations

- Only the five hosted ATS URL shapes above; custom-domain career pages, Workable (rate-limited from datacenter IPs), Taleo, iCIMS, SuccessFactors and BambooHR are not supported in this version.
- Description is a ≤ 300-character plain-text snippet, never the full posting (link to `url` for the rest). E-mail addresses and phone numbers that appear in posting text are redacted.
- Workday tenants that require a locale or a non-standard site path work when you paste the exact career-site URL; the Actor keeps the locale you give it.
- Keyword matching is per word against title/department/team/location/employment type; it is not a full-text search of descriptions (Workday and SmartRecruiters are narrowed server-side with the same words first).
- Ashby has no single-job endpoint: a single Ashby job URL fetches the board once and returns that job.

### Changelog

- 1.0.0 — first release: Greenhouse, Lever, Ashby, Workday, SmartRecruiters; keyword + location filters; details for Workday/SmartRecruiters; incremental `knownIds`; salary fields.

# Actor input Schema

## `boards` (type: `array`):

Company career pages hosted on Greenhouse (boards.greenhouse.io/<company>), Lever (jobs.lever.co/<company>), Ashby (jobs.ashbyhq.com/<company>), Workday (<tenant>.wd<n>.myworkdayjobs.com/<site>) or SmartRecruiters (jobs.smartrecruiters.com/<Company>). A single job URL on any of them returns just that job in one request. Short form also works: greenhouse:airbnb, lever:palantir, ashby:openai. Several boards share maxItems. Returns one record per open position within seconds.

## `query` (type: `string`):

Optional. Every word must appear in the job title, department, team, location or employment type (case-insensitive), e.g. "senior engineer". Workday boards are searched server-side (title and description). Leave empty for every open position.

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

Optional. Keep only jobs whose location text, country code or workplace type contains this, e.g. "London", "US", "remote". Matched case-insensitively against every location the posting lists.

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

Maximum number of job records to return in total across all boards (1–10000). Greenhouse, Lever and Ashby boards arrive in one request; Workday pages by 20 and SmartRecruiters by 100.

## `fetchDetails` (type: `boolean`):

Off (default): one record per job from the board listing — Greenhouse, Lever and Ashby already include the description snippet, salary and dates there. On: one extra request per job to add the description snippet, employment type and country for Workday and SmartRecruiters jobs (kind = detail).

## `knownIds` (type: `array`):

Job ids you already have (the `id` field, e.g. greenhouse:airbnb:8306554). They are skipped, and a page made entirely of known ids stops pagination, so a scheduled run only pays for new postings. Feed it the `id` values from your last dataset.

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

Apify Proxy settings. The five job-board APIs are public and work from datacenter proxies; no residential group is needed.

## Actor input object example

```json
{
  "boards": [
    "https://boards.greenhouse.io/airbnb",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/openai"
  ],
  "maxItems": 500,
  "fetchDetails": false,
  "knownIds": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `records` (type: `string`):

One record per job posting: id, url, title, company, ats, location(s), country, remote/workplaceType, department, team, employmentType, salary, requisitionId, postedAt, updatedAt, description (≤300 chars), applyUrl, kind (listing|detail) and source.

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

SUMMARY record: counts, duplicates and known ids skipped, blocked pages, inferred rate limit, p95 latency, time to first record and compute seconds.

# 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 = {
    "boards": [
        "https://boards.greenhouse.io/airbnb",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/openai"
    ],
    "query": "",
    "location": "",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("mdcurt/ats-job-board-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 = {
    "boards": [
        "https://boards.greenhouse.io/airbnb",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/openai",
    ],
    "query": "",
    "location": "",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("mdcurt/ats-job-board-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 '{
  "boards": [
    "https://boards.greenhouse.io/airbnb",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/openai"
  ],
  "query": "",
  "location": "",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call mdcurt/ats-job-board-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mdcurt/ats-job-board-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/NmabM2PTShxoQUhoW/builds/EeqSEX9mmrESSvwLN/openapi.json
