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

Every open job from any company on Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR, Jobvite or Oracle in one schema. Presets, company hiring summaries, salary, seniority, new & closed job monitoring.

- **URL**: https://apify.com/forward_opera18/ats-jobs-scraper.md
- **Developed by:** [Izzy Sobrizzy](https://apify.com/forward_opera18) (community)
- **Categories:** Jobs, Lead generation, 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 jobs

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 & 8 More

Get **every open job from any company** that hires through **Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR, Jobvite or Oracle Recruiting Cloud**. All twelve come back in **one clean format** with title, department, location, remote/hybrid/on-site, employment type, **seniority**, **salary ranges** (also converted to yearly), post date, apply link and the full description.

**You don't need to know which ATS a company uses.** Paste a careers URL, a website (`figma.com`) or just a company name (`Stripe`), and the Actor finds the board for you. Or skip the typing and pick a **preset list**: top tech, AI, fintech, remote-first, developer tools, European tech or large Workday enterprises.

Need the big picture rather than every posting? Switch **Output** to **One row per company** for a **hiring-signal summary**: open roles, jobs posted in the last 7 and 30 days, top departments and locations, remote share, seniority mix and median salary per company.

Run it once for a snapshot. Or schedule it with **"Only new jobs"** to get a feed of new openings, plus **closed jobs** if you want them, ready for Slack, email, Google Sheets, webhooks, Zapier or Make.

- ✅ **12 ATSs, 1 schema.** The same 42 fields with the same names and types for every job. Unknown values are `null`, never missing.
- ✅ **Presets.** 7 ready-made lists of 140+ verified boards (top tech, AI, fintech, remote-first, devtools, Europe, Workday enterprises).
- ✅ **Hiring signals.** One row per company with open roles, new this week, departments, locations and seniority mix, billed as one result per company.
- ✅ **Live data.** Every run reads the company's own job board at that moment, not a stale database copy.
- ✅ **Public career-site feeds only.** No browser, proxy, login or CAPTCHA, so it's fast, cheap and hard to break.
- ✅ **Finds the board for you.** It accepts board URLs, `ats:board`, company domains, careers pages and company names.
- ✅ **Enriched.** It adds normalised seniority (intern to executive), workplace type, employment type, ISO country codes and yearly salary.
- ✅ **Monitoring.** It can return only new jobs since the last run, and optionally closed jobs too (closed-job rows are free).
- ✅ **Clear per-company report.** A wrong name never kills the run. `RUN_SUMMARY` says exactly what happened to each company.
- ✅ **No personal data.** Recruiter and hiring-manager names are never exported.

### Who uses it

- **Job boards and aggregators:** fill a niche board with fresh postings straight from the source, and drop closed ones automatically.
- **Sales and recruiting teams (hiring signals):** see which accounts are hiring for which teams, where and how senior, and get alerts when a target company opens a role.
- **Job seekers:** watch 50 dream companies and get only new roles that match your title, location, seniority and salary.
- **Analysts and investors:** track hiring velocity, salary bands, remote policy and seniority mix across companies.
- **AI agents, RAG and LLM pipelines:** clean plain-text descriptions and stable IDs. It works as an MCP tool through Apify's MCP server.

### How to use it

1. Add companies to **Companies**, one per line. Mix and match any of these:

| You paste | What happens |
|---|---|
| `https://boards.greenhouse.io/airbnb` | Greenhouse board (also `job-boards.greenhouse.io`, EU boards, embed URLs) |
| `https://jobs.lever.co/spotify` | Lever board (also `jobs.eu.lever.co`) |
| `https://jobs.ashbyhq.com/openai` | Ashby board |
| `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | Workday careers site (also `wdN.myworkdaysite.com/recruiting/...`) |
| `https://careers.smartrecruiters.com/ServiceNow` | SmartRecruiters company |
| `https://apply.workable.com/blueground` | Workable account (also `blueground.workable.com`) |
| `https://bunq.recruitee.com` | Recruitee careers site |
| `https://acme.jobs.personio.de` | Personio careers page (`.de` or `.com`) |
| `https://acme.teamtailor.com` | Teamtailor career site |
| `https://alkira.bamboohr.com/careers` | BambooHR careers page |
| `https://jobs.jobvite.com/egnyte/jobs` | Jobvite career site |
| `https://xxxx.fa.us2.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1/jobs` | Oracle Recruiting Cloud careers site (paste any URL that contains `/sites/<site>`) |
| `greenhouse:figma`, `ashby:notion`, `bamboohr:alkira`, `jobvite:nutanix` | ATS + board name |
| `figma.com` or `https://www.notion.com/careers` | **Company website**: the Actor checks the page, the homepage, `/careers`, `/jobs` and `careers.<domain>` for an embedded board, then tries the domain name on each ATS |
| `stripe` or `Monzo Bank` | **Company or board name**: the Actor checks the ten probe-able ATSs (Workday and Oracle need a URL) |

Or pick one or more **Preset company lists** (they're merged with your own entries, duplicates removed).

2. (Optional) Add filters: keywords, excluded words, locations, remote/hybrid, departments, seniority, minimum yearly salary, posted within N days.
3. (Optional) Set **Output** to **One row per company** for hiring summaries instead of individual jobs.
4. Click **Start**. Results appear in the **Output** tab. Export them as JSON, CSV, Excel or HTML, or fetch them through the API.

**Tip:** to find a company's board yourself, open any job on its careers page. The link usually contains `greenhouse.io/<name>`, `lever.co/<name>`, `ashbyhq.com/<name>`, `myworkdayjobs.com/<Site>` and so on.

### Presets

| Preset | Companies | Examples |
|---|---|---|
| `top-tech` | 30 | Airbnb, Stripe, Figma, Discord, Reddit, Cloudflare, Datadog, Spotify, Palantir, Notion |
| `ai` | 26 | Anthropic, OpenAI, Cohere, Scale AI, Hugging Face, Perplexity, Databricks, ElevenLabs, Harvey |
| `fintech` | 16 | Stripe, Brex, Chime, Affirm, Monzo, Mercury, Ramp, Plaid, Nubank, Adyen |
| `remote-first` | 20 | Zapier, PostHog, 1Password, GitLab, Supabase, Grafana Labs, Vercel, Toptal |
| `devtools` | 21 | Sentry, Snowflake, Temporal, ClickHouse, Vercel, Netlify, Cockroach Labs, Linear |
| `europe` | 16 | Monzo, GoCardless, Adyen, N26, SumUp, Synthesia, Miro, bunq, Blueground |
| `enterprise-workday` | 12 | NVIDIA, Salesforce, Adobe, Intel, Cisco, Mastercard, PayPal, Capital One, 3M, HP |

Every preset board was checked against the live ATS in October 2026. Companies sometimes move to a new ATS; if one does, `RUN_SUMMARY` flags it and the rest of the run carries on.

### Company hiring summary (one row per company)

With **Output = One row per company** (`"outputMode": "companies"`), the Actor still reads every job, applies your filters, and then saves **one row per company** instead of one per job. A real row (engineering roles at Alkira, lists shortened):

```json
{
  "recordType": "company",
  "companyName": "Alkira",
  "ats": "bamboohr",
  "openJobs": 14,
  "jobsPostedLast7Days": 1,
  "jobsPostedLast30Days": 3,
  "newSinceLastRun": 0,
  "hiringActivity": "low",
  "remoteShare": 0.71,
  "topDepartments": [{ "name": "Engineering - R&D", "count": 12 }, { "name": "Sales Engineering", "count": 2 }],
  "topLocations": [{ "name": "Remote (United States)", "count": 5 }, { "name": "San Jose, California, United States", "count": 4 }],
  "topCountries": [{ "name": "US", "count": 10 }, { "name": "IN", "count": 4 }],
  "seniorityMix": { "ENTRY": 6, "SENIOR": 5, "MID": 1, "LEAD": 2 },
  "workplaceMix": { "REMOTE": 10, "HYBRID": 4 },
  "jobsWithSalary": 4,
  "newestPostedAt": "2026-10-15T00:00:00.000Z",
  "newestTitles": ["Sr. Solutions Engineer, Pre-Sales (East Coast)", "Principal Software Engineer, Forwarding Plane"],
  "boardUrl": "https://alkira.bamboohr.com/careers"
}
```

- Filters decide what is counted. For example, `keywords: ["engineer"]` gives open engineering roles per company.
- With `onlyNewJobs`, `newSinceLastRun` counts jobs not seen before (and `closedSinceLastRun` with `reportClosedJobs`). Every company still gets a row.
- `hiringActivity` is a rough label from jobs posted in the last 30 days (or open roles when most jobs have no date): 0 = none, 1–4 low, 5–19 moderate, 20–49 high, 50+ very high.
- Each company row is billed as **one result** ($0.0015). Summarising 100 companies costs about $0.16 including the run fee.
- In normal **Jobs** mode you get the same summaries for free as the `COMPANY_SUMMARY` record in the run's key-value store.

### Input example

```json
{
  "companies": ["https://boards.greenhouse.io/airbnb", "lever:spotify", "figma.com", "Monzo Bank",
                "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite", "bamboohr:alkira"],
  "presets": ["ai"],
  "outputMode": "jobs",
  "keywords": ["engineer", "data scientist"],
  "excludeKeywords": ["intern"],
  "locations": ["United States", "Remote"],
  "seniority": ["SENIOR", "LEAD"],
  "minYearlySalary": 150000,
  "postedWithinDays": 14,
  "onlyNewJobs": true,
  "reportClosedJobs": true,
  "descriptionFormat": "text"
}
```

| Field | Default | What it does |
|---|---|---|
| `companies` | none | Up to 1,000 per run, in any format above. Duplicates are removed. Required unless you pick a preset. |
| `presets` | none | Any of `top-tech`, `ai`, `fintech`, `remote-first`, `devtools`, `europe`, `enterprise-workday`. Merged with `companies`. |
| `outputMode` | `jobs` | `jobs` (one row per posting) or `companies` (one hiring-summary row per company). |
| `keywords` | none | Keep jobs whose **title** contains any of these. Matching is whole-word (`AI` matches "AI Engineer", not "Maintenance") and works with `C++` and `.NET`. |
| `excludeKeywords` | none | Drop jobs whose title contains any of these. |
| `keywordsInDescription` | `false` | Also match keywords in the description, e.g. every job mentioning "Kubernetes". |
| `locations` | none | Keep jobs whose location, other locations, city, region, country or country code contains any of these. |
| `workplace` | `any` | `remote`, `hybrid`, `onsite` or `remoteOrHybrid`. |
| `departments` | none | Keep jobs whose department or team contains any of these. |
| `postedWithinDays` | `0` (off) | Only jobs published in the last N days. |
| `onlyWithSalary` | `false` | Only jobs with a structured salary range. |
| `minYearlySalary` | `0` (off) | Only jobs whose pay reaches this yearly amount, in the job's own currency (hourly and monthly pay are converted). |
| `seniority` | all | Any of `INTERN`, `ENTRY`, `MID`, `SENIOR`, `LEAD`, `MANAGER`, `DIRECTOR`, `EXECUTIVE`. |
| `includeUnknownSeniority` | `true` | Keep titles with no clear level (e.g. "Software Engineer") when filtering by seniority. |
| `onlyNewJobs` | `false` | Monitoring: return only jobs not returned by earlier runs with the same `monitorKey`. |
| `monitorKey` | `default` | A separate memory for each watch list. |
| `reportClosedJobs` | `false` | With monitoring on, also output remembered jobs that disappeared from their board (`isClosed: true`). **Free.** |
| `descriptionFormat` | `text` | `text`, `html`, `both` or `none`. `none` gives the smallest output and is much faster for SmartRecruiters, Workday, BambooHR, Jobvite and Oracle. |
| `maxJobsPerCompany` / `maxJobs` | `0` (no limit) | Caps per company and per run. In `companies` mode, `maxJobs` caps the number of company rows and `maxJobsPerCompany` is ignored. |
| `maxConcurrency` | `5` | Companies processed in parallel. Requests to each ATS stay rate-limited regardless. |

### Output example

Every job record has exactly the same fields. This is a real record (description shortened):

```json
{
  "id": "ashby:openai:c414f15f-60c4-40b5-a896-d2ad6c7f4415",
  "recordType": "job",
  "ats": "ashby",
  "companySlug": "openai",
  "companyName": "OpenAI",
  "jobId": "c414f15f-60c4-40b5-a896-d2ad6c7f4415",
  "requisitionId": null,
  "title": "Senior Technical Program Manager - Security",
  "department": "Security",
  "team": "Security",
  "location": "US - Remote",
  "allLocations": ["US - Remote", "New York City", "Seattle", "Washington, DC", "San Francisco"],
  "city": null,
  "region": null,
  "country": "United States",
  "countryCode": "US",
  "workplaceType": "REMOTE",
  "isRemote": true,
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "FullTime",
  "experienceLevel": null,
  "seniority": "SENIOR",
  "salaryMin": 165400,
  "salaryMax": 285000,
  "salaryCurrency": "USD",
  "salaryInterval": "YEAR",
  "salaryMinYearly": 165400,
  "salaryMaxYearly": 285000,
  "salaryRanges": [{ "min": 165400, "max": 285000, "currency": "USD", "interval": "YEAR", "label": null }],
  "salarySummary": "$165.4K – $285K • Offers Equity • Multiple Ranges",
  "postedAt": "2026-06-25T17:30:54.205Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/openai/c414f15f-60c4-40b5-a896-d2ad6c7f4415",
  "applyUrl": "https://jobs.ashbyhq.com/openai/c414f15f-60c4-40b5-a896-d2ad6c7f4415/application",
  "descriptionText": "About the Team\n\nOpenAI builds powerful AI systems like ChatGPT…",
  "descriptionHtml": null,
  "language": null,
  "isNew": null,
  "firstSeenAt": null,
  "isClosed": null,
  "closedAt": null,
  "scrapedAt": "2026-10-21T03:44:01.582Z"
}
```

Normalised values:

- `workplaceType`: `REMOTE`, `HYBRID`, `ONSITE` or `null`
- `employmentType`: `FULL_TIME`, `PART_TIME`, `CONTRACT`, `TEMPORARY`, `INTERN`, `OTHER` or `null`. The original value is kept in `employmentTypeRaw`.
- `seniority`: `INTERN`, `ENTRY`, `MID`, `SENIOR`, `LEAD`, `MANAGER`, `DIRECTOR`, `EXECUTIVE` or `null`. It's inferred from the title and the ATS's experience field. "Product Manager"-style individual-contributor titles are not counted as people managers.
- `salaryInterval`: `HOUR`, `DAY`, `WEEK`, `MONTH`, `YEAR` or `null`. `salaryMinYearly` and `salaryMaxYearly` convert the range to a year (2,080 hours, 260 days, 52 weeks or 12 months).
- `id` = `ats:board:jobId`. It's stable across runs, so you can use it to deduplicate and join.

The **Overview**, **Jobs with salary**, **New & closed jobs** and **Company hiring summary** views in the Output tab show the most useful columns. Every row has `recordType`: `job` or `company`. Each run also saves **`RUN_SUMMARY`** in its key-value store. Per company, it lists the ATS and board that were used, jobs found, matched, new, closed and saved, duplicates skipped, and any error or warning in plain English.

### What each ATS provides

Field coverage on real boards (Oct 2026: about 11,000 jobs from 54 boards for the first four ATSs, 485 jobs from 17 boards for the next five, and 180 jobs from 7 boards for BambooHR, Jobvite and Oracle). What you get depends on what each company publishes.

| Field | Greenhouse | Lever | Ashby | SmartRecruiters | Workday | Workable | Recruitee | Personio | Teamtailor | BambooHR | Jobvite | Oracle |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Title, location, URL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Post date | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ~40%¹ | ✅ |
| Full description | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Company name | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Department | ✅ | ~50–77% | ✅ | ~65–95% | – | ~90% | ~90% | ✅ | ✅ | ✅ | ✅ | ✅ |
| Remote / hybrid / on-site | ~30–40% | ✅ | ~70–80% | ✅ | from location | ✅ | ✅ | from office | ✅ | ✅ | from location | when set |
| Employment type | ~25% | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | ✅ | ~40%¹ | ✅ |
| Seniority (normalised) | ~60% | ~25% | ~30% | ~90% | ~90% | ✅ | ✅ | ✅ | ~30% | ✅ | ~45% | ~85% |
| Structured salary | ~55% | ~30% | ~35–64% | – | – | – | ~30% | – | – | text, ~15% | when published | – |
| Country code | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | ✅ | ✅ | ~40%¹ | ✅ |

¹ Jobvite sites that publish structured job data (schema.org) give date, type, country and salary. Sites that don't still give title, department, location and description.

### Pricing

**Pay per result: $1.50 per 1,000 jobs** ($0.0015 per job saved), plus **$0.005 per run**. There's no monthly fee, and you don't pay for compute.

- **Filters run before you're charged.** You only pay for jobs that match.
- **In monitoring mode you only pay for new jobs.** Closed-job rows are free.
- A company that returns nothing costs nothing.

| Scenario | Jobs saved | Cost |
|---|---|---|
| One-off: every job at 20 companies | ~3,000 | **≈ $4.51** |
| One-off: senior remote engineering roles at 100 companies | ~400 | **≈ $0.61** |
| Daily monitor of 50 companies, ~40 new jobs/day | ~1,200 / month | **≈ $1.95 / month** (incl. 30 run fees) |
| Daily monitor of 10 dream companies, ~3 new matching jobs/day | ~90 / month | **≈ $0.29 / month** |

**Company summaries** (`outputMode: companies`) are billed as one result per company: a weekly summary of 100 companies costs about **$0.16**.

Set **Maximum cost per run** in the run options to cap spending. The Actor stops cleanly at the limit and records why in `RUN_SUMMARY`.

### How it compares

| | This Actor | Typical single-ATS scraper | Typical job-database API |
|---|---|---|---|
| ATSs in one run | **12**, same schema | 1 | many |
| Ready-made company lists | ✅ 7 presets | – | – |
| One-row-per-company hiring summary | ✅ | – | sometimes |
| Data freshness | **Live from the company's board at run time** | live | depends on the crawl schedule |
| Input | URL, `ats:board`, **domain, company name** | board slug | search filters |
| New-job monitoring | ✅ built in | rarely | sometimes |
| Closed-job detection | ✅ free | rarely | sometimes |
| Seniority + yearly salary | ✅ | rarely | often (AI-derived) |
| Price | $1.50 / 1,000 jobs | varies | varies |

### Use cases and recipes

- **New-jobs alert for target accounts:** add your accounts, turn on `onlyNewJobs` and schedule daily. Then add a Slack or email integration on "Run succeeded", or a webhook that reads the dataset.
- **Niche job board:** schedule `onlyNewJobs` + `reportClosedJobs` hourly or daily. Upsert by `id`, and unpublish rows where `isClosed` is `true`.
- **Salary benchmarking:** filter with `onlyWithSalary`, then compare `salaryMinYearly` and `salaryMaxYearly` by `seniority`, `countryCode` and `department`.
- **Hiring-velocity tracking:** schedule `outputMode: companies` weekly (for example with the `ai` or `fintech` preset) and chart `openJobs` and `jobsPostedLast7Days` per company over time.
- **Sales prospecting:** run your account list in `companies` mode with `keywords` for the roles you sell to (e.g. "data engineer") and sort by `jobsPostedLast30Days`.

### FAQ

**Is this legal?**
The Actor reads only **public job-posting feeds** that companies publish so candidates can see their jobs:

- documented public APIs from Greenhouse ([Job Board API](https://developers.greenhouse.io/job-board.html)), Lever ([Postings API](https://github.com/lever/postings-api)), Ashby ([Job Postings API](https://developers.ashbyhq.com/docs/public-job-posting-api)) and SmartRecruiters ([Posting API](https://developers.smartrecruiters.com/docs/posting-api))
- Personio's [XML job feed](https://developer.personio.de/docs/retrieving-open-job-positions)
- Recruitee's careers-site API, Workable's job widget feed and Teamtailor's RSS feed
- for Workday, BambooHR and Oracle Recruiting Cloud, the same JSON endpoints their public careers sites use
- for Jobvite, the public career-site pages (including the schema.org job data they publish for search engines)

It collects job postings only, not personal data, and rate-limits itself. You're responsible for how you use the data (for example, follow each company's terms if you republish descriptions).

**Why is a company "not found"?**
The board name is wrong, or the company uses an ATS this Actor doesn't cover (e.g. iCIMS, SAP SuccessFactors or Taleo, which don't offer a reliable public feed). Open any job on its careers page and paste that board URL. `RUN_SUMMARY` lists each company's status.

**How reliable is finding a board from a domain or company name?**
For a domain, the Actor first looks for a board linked from the company's own pages, which is reliable when it finds one. Otherwise it tries the domain or company name as a board name on each ATS. A name match can occasionally pick a different company with the same name, so those rows get a warning in `RUN_SUMMARY`. Paste the board URL when you need certainty. Careers pages that load jobs only with JavaScript may not reveal their board.

**Why did a SmartRecruiters company return 0 jobs?**
SmartRecruiters returns an empty list, not an error, for unknown company IDs, and the IDs are case-sensitive (`BoschGroup`, not `bosch`).

**Why are SmartRecruiters, Workday, BambooHR, Jobvite and Oracle slower?**
They need one extra request per job for the description (BambooHR and Jobvite also for the post date). Filters are applied *before* those requests whenever possible. Choose `descriptionFormat: none` for the fastest runs. With title keywords, Workday boards are pre-searched on Workday's side.

**How do "Only new jobs" and closed jobs work?**
The Actor remembers the jobs it returned in a named key-value store (`ats-jobs-scraper-monitor`, one record per `monitorKey`), for 180 days after each job was last seen. The first run returns everything that matches, and later runs return only new jobs. With `reportClosedJobs`, a remembered job that is no longer on its board is output once with `isClosed: true` and `closedAt`. If it comes back, it counts as open again.

**Does it handle big companies?**
Yes. All pages are fetched in parallel where the ATS allows it. Boards with 2,000+ jobs (e.g. NVIDIA on Workday, ServiceNow on SmartRecruiters, OpenAI on Ashby) load in seconds to a minute.

**Can I use it from the API, Zapier, Make, n8n or an AI agent?**
Yes. Start it through the Apify API or schedule it, and connect the dataset to Zapier, Make, n8n, Google Sheets, Slack or webhooks. AI agents can call it as a tool through the Apify MCP server.

**Something broke. What now?**
Open an issue in the **Issues** tab and include the run link. Temporary errors (rate limits, 5xx, timeouts) are retried automatically with backoff, and one failing company never stops the others. Feature requests (another ATS, another field) are welcome too.

# Actor input Schema

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

One company per line (or leave empty and pick a preset below). Paste any of these:<br>• <b>A job-board URL</b> from Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR, Jobvite, Workday or Oracle Recruiting Cloud, e.g. <code>https://boards.greenhouse.io/airbnb</code>, <code>https://jobs.lever.co/spotify</code>, <code>https://alkira.bamboohr.com/careers</code>, <code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite</code><br>• <code>ats:board</code>, e.g. <code>greenhouse:figma</code>, <code>ashby:notion</code>, <code>smartrecruiters:ServiceNow</code>, <code>recruitee:bunq</code>, <code>bamboohr:alkira</code>, <code>jobvite:nutanix</code><br>• <b>A company website or domain</b>, e.g. <code>figma.com</code> or <code>https://www.notion.com/careers</code>: the Actor finds the careers board for you<br>• <b>A company name or board name</b>, e.g. <code>stripe</code> or <code>Monzo Bank</code>: the Actor checks every supported ATS (Workday and Oracle need a URL)

## `presets` (type: `array`):

Add ready-made lists of verified careers boards (merged with "Companies", duplicates removed): Top tech (30), AI (26), Fintech (16), Remote-first (20), Developer tools (21), European tech (16), and Enterprise on Workday (12 large employers such as NVIDIA, Salesforce and Cisco). Tip: combine with Output "One row per company" to watch hiring across a whole sector for a few cents.

## `outputMode` (type: `string`):

<b>Jobs</b>: one row per job posting (a free per-company hiring summary is also saved as COMPANY_SUMMARY in the key-value store).<br><b>One row per company</b>: a hiring-signal summary instead of jobs: open roles, jobs posted in the last 7 and 30 days, top departments and locations, remote share, seniority mix, median salary and newest titles. Filters decide what is counted. Each company row is billed as one result, so summarising 100 companies costs the same as 100 jobs.

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

Keep jobs whose title contains at least one of these words or phrases (whole-word match, case-insensitive). Leave empty to keep all jobs.

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

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

## `keywordsInDescription` (type: `boolean`):

When on, a keyword can match the title OR the full description (e.g. find every job that mentions "Kubernetes").

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

Keep jobs whose location contains any of these texts, e.g. "London", "Germany", "US", "New York". Matches the location, all secondary locations, city, region and country fields.

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

Filter by remote / hybrid / on-site. Jobs whose workplace type is unknown are only kept with "Any".

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

Keep jobs whose department or team contains any of these texts, e.g. "Engineering", "Sales".

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

Only keep jobs first published in the last N days. 0 = no date filter. Jobs without a publish date are dropped when this is set.

## `onlyWithSalary` (type: `boolean`):

Keep only jobs where the ATS publishes a machine-readable salary range (common in the US on Greenhouse, Ashby and Lever, and on some Recruitee boards).

## `minYearlySalary` (type: `integer`):

Keep jobs whose published pay reaches at least this yearly amount (in the job's own currency; hourly/monthly pay is converted to yearly). Jobs without a salary are dropped. 0 = off.

## `seniority` (type: `array`):

Keep only these levels. Seniority is inferred from the job title (e.g. "Senior", "Staff", "Head of", "Intern") and the ATS's experience field.

## `includeUnknownSeniority` (type: `boolean`):

Many titles (e.g. "Software Engineer") carry no level. On = keep them when filtering by seniority; off = drop them.

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

Remember which jobs were already returned and output only new ones. Schedule the Actor (e.g. daily) and connect a webhook, Slack, email or Google Sheets integration to get a feed of new openings. The first run returns all matching jobs.

## `monitorKey` (type: `string`):

Use a different name for each separate watch list so their memories don't mix. Letters, digits, - and \_ only.

## `reportClosedJobs` (type: `boolean`):

With monitoring on, also output jobs from earlier runs that have disappeared from the board (isClosed = true). Closed-job rows are not charged.

## `descriptionFormat` (type: `string`):

Plain text is best for spreadsheets and AI; HTML keeps formatting. "None" makes the output much smaller and SmartRecruiters, Workday, BambooHR, Jobvite and Oracle runs much faster (they need one extra request per job for descriptions).

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

Stop after this many matching jobs per company. 0 = no limit.

## `maxJobs` (type: `integer`):

Stop the whole run after this many jobs. 0 = no limit. You can also cap spending with the run's maximum cost setting.

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

Higher is faster for long company lists. Requests to each ATS are rate-limited regardless of this value.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/airbnb",
    "lever:spotify",
    "ashby:openai",
    "figma.com",
    "https://apply.workable.com/blueground",
    "https://alkira.bamboohr.com/careers",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "outputMode": "jobs",
  "keywords": [
    "engineer",
    "data scientist"
  ],
  "keywordsInDescription": false,
  "workplace": "any",
  "postedWithinDays": 0,
  "onlyWithSalary": false,
  "minYearlySalary": 0,
  "includeUnknownSeniority": true,
  "onlyNewJobs": false,
  "monitorKey": "default",
  "reportClosedJobs": false,
  "descriptionFormat": "text",
  "maxJobsPerCompany": 0,
  "maxJobs": 0,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

All saved job postings with title, company, department, location, workplace type, employment type, salary range, post date, job/apply URLs and description.

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

One row per company (output mode "companies"): open roles, jobs posted in the last 7 / 30 days, top departments and locations, remote share, seniority mix and median salary.

## `runSummary` (type: `string`):

Per-company status (ok / empty / not_found / error), jobs found, matched and saved, plus any error message and why the run stopped early.

# 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",
        "lever:spotify",
        "https://jobs.ashbyhq.com/openai"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("forward_opera18/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",
        "lever:spotify",
        "https://jobs.ashbyhq.com/openai",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("forward_opera18/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",
    "lever:spotify",
    "https://jobs.ashbyhq.com/openai"
  ]
}' |
apify call forward_opera18/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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