# ATS Job Scraper: Greenhouse, Lever, Workday, Oracle +10 More (`steady-scrape/ats-job-feed`) Actor

Open jobs from Greenhouse, Lever, Ashby, Workday, Oracle HCM, BambooHR, Workable, Recruitee, SmartRecruiters, Personio, Rippling, Teamtailor, Breezy & Gem boards as unified JSON: title, salary, location, remote, apply link. New/removed job alerts. No login. $1.50 per 1,000 jobs.

- **URL**: https://apify.com/steady-scrape/ats-job-feed.md
- **Developed by:** [SteadyScrape](https://apify.com/steady-scrape) (community)
- **Categories:** Jobs, Lead generation
- **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 results

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 Job Feed & Scraper: Greenhouse, Lever, Ashby, Workday, Oracle, Workable, BambooHR & More

**Get every open job posting from any company's Greenhouse, Lever, Ashby, Workday, Oracle Cloud HCM, BambooHR, Workable, Recruitee, SmartRecruiters, Personio, Rippling, Teamtailor, Breezy HR or Gem careers page as one clean, unified JSON feed.** You give it a list of company job boards (URLs, slugs, or company names from the built-in list), and it returns one record per job with title, department, salary range, remote/hybrid/onsite, parsed city/region/country, employment type, posted date, job and apply links, and the full description. Schedule it daily with **Only new jobs** and you get a change feed of new (and removed) postings.

- **Official public job-board APIs.** It reads the JSON endpoints each ATS publishes for career pages. No login, no cookies, no proxies, no browser.
- **14 ATS platforms, 1 schema.** Mix Greenhouse, Lever, Ashby, Workday, Oracle Cloud HCM, BambooHR, Workable, Recruitee, SmartRecruiters, Personio, Rippling, Teamtailor, Breezy HR and Gem boards in a single run and get identical fields for all of them.
- **Paste any board URL.** The platform is auto-detected. A bare company slug like `figma` works too, and so does a company name or domain from the [built-in company list](#built-in-company-list) (for example `Bosch Group` or `stripe.com`).
- **Job change detection.** Output only jobs you haven't received before, plus optional "removed" records when a job is taken down.
- **Salary data.** Structured where the ATS exposes it (Ashby compensation, Greenhouse pay transparency, Lever `salaryRange`, Recruitee, SmartRecruiters, Personio, Rippling and Breezy salary fields), otherwise parsed from the description text. `salary.source` tells you which.
- **Pay per result.** $1.50 per 1,000 jobs. Removed-job records and the run summary are free.
- **Robust.** Retries with backoff, one broken board never fails the run, and it saves its results before the run timeout instead of losing them. A per-company status report is saved to `OUTPUT`.

> **Note:** This Actor fetches the boards you list. It does not search every company on the internet for a keyword. To find jobs at many companies, list their boards (you can pass hundreds in one run) and filter by keyword, location, department or date.

### What can you use it for?

- **Job alerts.** "Tell me when Stripe, Ramp or NVIDIA posts a new engineering role in Canada." Run on a schedule with *Only new jobs* and send results to email, Slack or a webhook.
- **Recruiter and sales leads from hiring signals.** See which companies are hiring for which roles, where and at what pay. A company hiring its first data engineer or a new sales team is a buying signal for many B2B products.
- **Labor-market and salary research.** Collect structured salary ranges, locations, remote share and departments across many employers.
- **Job boards and aggregators.** Feed a niche job board (for example "remote AI jobs" or "climate tech jobs") with fresh postings every day, with apply links that go straight to the employer's ATS.
- **AI agents and LLM workflows.** Agents can call this Actor through the Apify API or Apify MCP server to answer "what roles is company X hiring for?" with structured, current data.

### Quick start

1. Click **Try for free** (or **Start**).
2. In **Company job boards**, paste the careers page URLs of the companies you want, for example:
   ```
   https://job-boards.greenhouse.io/stripe
   https://jobs.lever.co/spotify
   https://jobs.ashbyhq.com/ramp
   https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
   ```
3. Optionally add filters (title keywords, locations, remote only, posted within N days) and a **Max jobs per company** cap.
4. Click **Start**. Jobs appear in the **Output** tab, where you can download them as JSON, CSV, Excel or HTML, or read them through the API.

#### Example input

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/zoox",
    "https://jobs.ashbyhq.com/ramp",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "keywords": ["engineer"],
  "excludeKeywords": ["intern"],
  "locations": ["US", "Canada"],
  "postedWithinDays": 14,
  "maxJobsPerCompany": 200,
  "onlyNewSinceLastRun": true,
  "emitRemovedJobs": true
}
```

#### Run it from the API

```bash
curl -X POST "https://api.apify.com/v2/acts/steady-scrape~ats-job-feed/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies": ["greenhouse:stripe", "ashby:ramp"], "keywords": ["engineer"], "maxJobsPerCompany": 50}'
```

### How to find a company's job board (slug)

Open the company's careers page, click any job, and look at the address bar. The URL tells you the ATS and the slug. You can paste the board URL as is, or a job URL from it, or use the short `platform:slug` form.

| ATS | What the URL looks like | Slug | Short form |
|---|---|---|---|
| **Greenhouse** | `job-boards.greenhouse.io/stripe` or `boards.greenhouse.io/stripe`, or an embedded board `boards.greenhouse.io/embed/job_board?for=discord` | `stripe` | `greenhouse:stripe` |
| **Lever** | `jobs.lever.co/spotify` (EU boards: `jobs.eu.lever.co/<company>`) | `spotify` | `lever:spotify` / `lever-eu:<company>` |
| **Ashby** | `jobs.ashbyhq.com/ramp` | `ramp` | `ashby:ramp` |
| **Workday** | `nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite` | tenant `nvidia`, data center `wd5`, site `NVIDIAExternalCareerSite` | `workday:nvidia.wd5/NVIDIAExternalCareerSite` |
| **Workday (alt. domain)** | `wd3.myworkdaysite.com/recruiting/<tenant>/<site>` | | paste the URL |
| **BambooHR** | `acme.bamboohr.com/careers` | `acme` | `bamboohr:acme` |
| **Workable** | `apply.workable.com/huggingface/` | `huggingface` | `workable:huggingface` |
| **Recruitee** | `acme.recruitee.com` | `acme` | `recruitee:acme` |
| **SmartRecruiters** | `jobs.smartrecruiters.com/Wise` or `careers.smartrecruiters.com/Wise` | company ID `Wise` | `smartrecruiters:Wise` |
| **Personio** | `acme.jobs.personio.de` (or `.com`) | `acme` | `personio:acme` |
| **Oracle Cloud HCM** | `jpmc.fa.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1001/jobs` | host `jpmc.fa.oraclecloud.com`, site `CX_1001` (or the site name in the URL) | `oraclehcm:jpmc.fa/CX_1001` |
| **Rippling** | `ats.rippling.com/rippling/jobs` | `rippling` | `rippling:rippling` |
| **Teamtailor** | `career.teamtailor.com/jobs` (regional hosts like `acme.na.teamtailor.com` work too) | `career` | `teamtailor:career`, or `teamtailor:careers.acme.com` for a custom domain |
| **Breezy HR** | `acme.breezy.hr` | `acme` | `breezy:acme` |
| **Gem** | `jobs.gem.com/accel` | `accel` | `gem:accel` |

Tips:

- If a company's careers page is on its own domain (e.g. `company.com/careers`), click a job: the apply page usually reveals the ATS URL.
- A **bare slug** (e.g. `figma`) is first looked up in the built-in company list, then tried on Greenhouse, Ashby and Lever. Workday always needs the full URL or the `workday:tenant.wdN/site` form, because the site name differs per company. For the other platforms, use the URL or the `platform:slug` form. Oracle Cloud HCM needs the full Candidate Experience URL (or `oraclehcm:host/site`), because each company has its own host and site. Teamtailor boards on a company's own domain need `teamtailor:<that domain>`.
- The language part of Workday URLs (`/en-US/`) is optional and ignored.
- If a slug is wrong, that board shows `Board not found (404)` in the run summary and the other boards still run.

#### Built-in company list

You can also enter a **company name or domain** (`Bosch Group`, `ServiceNow`, `stripe.com`, `https://wise.com`). The Actor looks it up in a small bundled table of about 580 company job boards: about 380 on the first 9 platforms (Greenhouse through Personio) and 40 each on Rippling, Teamtailor, Breezy HR, Gem and Oracle Cloud HCM. Every board in the table was checked live and had open jobs when the table was built (October 2026). Matching ignores case, punctuation, `www.`, the domain ending and suffixes like Inc or GmbH.

- The list is small on purpose. It is not a directory of every company. If a name is not in it, the run summary lists it under `unresolvedInputs` with a clear message, and you can paste the company's careers board URL instead.
- Names that matched are listed under `resolvedByLookup` in the run summary, and those rows have `resolvedVia: "lookup:<name>"`, so you can check what each name was mapped to.
- An explicit URL or `platform:slug` always wins over the list.
- Companies sometimes switch ATS. If a looked-up board stops working, paste the current careers URL.

The table was compiled from these open datasets (see [Data sources and credits](#data-sources-and-credits)).

### Pricing

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

- **$1.50 per 1,000 jobs delivered** ($0.0015 per job result).
- A tiny Actor-start fee of $0.00005 per run (at up to 1 GB memory).
- **Free:** removed-job records, error reports and the run summary.

You pay only for job records written to the dataset. Filters are applied before charging, so jobs you filter out cost nothing.

| What you fetch | Cost |
|---|---|
| 100 jobs | about $0.15 |
| 1,000 jobs | about $1.50 |
| 5,000 jobs (a full first pull of several large boards) | about $7.50 |
| Daily *Only new jobs* feed that finds 200 new jobs per day | about $0.30 per day |
| Daily *Only new jobs* feed with no new jobs that day | about $0.00005 (start fee only) |

Platform usage (compute) is included in the price. You can also set a **maximum total charge** for a run in the run options. The Actor stops cleanly at that limit and does not deliver jobs it can't bill, so you never pay more than you set.

### Only new jobs and removed jobs

Turn on **Only new jobs since last run** (`onlyNewSinceLastRun`) for scheduled runs:

- The Actor remembers which job IDs it has already delivered for each board, in a named key-value store (`stateStoreName`, default `ats-job-feed-state`).
- Each run outputs only jobs it has not delivered before, marked `changeType: "new"`. You pay only for those.
- **First run:** by default every current job is output once (`firstRunEmitsAll: true`). Set it to `false` to record a silent baseline and get only jobs posted after that.
- **Removed jobs:** with `emitRemovedJobs: true`, jobs that disappeared from a board since the last run are output as free records with `changeType: "removed"`, `firstSeenAt` and `removedDetectedAt`. This needs a complete listing of the board, so for Workday it only works when no cap or keyword pre-filter limited the listing. On SmartRecruiters, a job missing from the listing is re-checked directly before it is reported as removed, because its paged listing can skip a job while the board is changing.
- **Max jobs per company** in this mode caps the *new* jobs per run. Jobs held back by the cap are delivered on later runs.
- Use different `stateStoreName` values for independent feeds (for example one per customer or per alert).

### Stops early instead of timing out

If a run is about to hit its timeout (for example you passed many large Workday, BambooHR or SmartRecruiters boards with a short timeout), the Actor stops starting new requests shortly before the deadline (about 20% of the timeout, at most 2 minutes), saves every job collected so far and writes the run summary. The run still **succeeds**, and the status message says how many boards were completed. The summary lists `incompleteBoards` and `notStartedBoards`. The *Only new* state is updated only for boards that finished, so nothing is lost or skipped next time. To get everything, raise the timeout or reduce the boards or `maxJobsPerCompany`.

### Input

| Field | Description |
|---|---|
| `companies` (required) | Board URLs, job URLs, `platform:slug`, or company names/domains from the [built-in list](#built-in-company-list). See [how to find a board](#how-to-find-a-companys-job-board-slug). |
| `keywords` / `excludeKeywords` | Keep jobs whose title contains any of / none of these words (case-insensitive). |
| `locations` | City, region, country name or ISO country code (`"US"`, `"Toronto"`, `"Germany"`). |
| `remoteOnly` | Only remote jobs. |
| `departments` | Substring match on department or team. |
| `postedWithinDays` | Only jobs posted in the last N days. Jobs with no date are kept. |
| `includeDescription` | Include the HTML and plain-text description (default on). |
| `maxJobsPerCompany` | Cap per board (in *Only new* mode: cap on new jobs per board). |
| `onlyNewSinceLastRun` | Output only jobs not delivered in earlier runs. |
| `firstRunEmitsAll` | With *Only new*: on a board's first run, output everything (default) or just record a baseline. |
| `emitRemovedJobs` | With *Only new*: also output free `changeType: "removed"` records. |
| `stateStoreName` | Name of the state store. Use different names for independent feeds. |
| `maxConcurrency`, `maxRetries` | Politeness and robustness settings. |

### Output

One dataset item per job, with the same fields on every platform. Example:

```json
{
  "jobId": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "sourceJobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "platform": "ashby",
  "boardSlug": "ramp",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "company": "Ramp",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "locations": [
    {"raw": "New York, NY (HQ)", "city": "New York City", "region": "New York", "country": "United States", "countryCode": "US", "isRemote": false},
    {"raw": "Remote (Canada)", "city": null, "region": null, "country": "Canada", "countryCode": "CA", "isRemote": true}
  ],
  "locationText": "New York, NY (HQ)",
  "isRemote": true,
  "workplaceType": "HYBRID",
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "FullTime",
  "salary": {"min": 211400, "max": 290600, "currency": "USD", "interval": "YEAR", "source": "structured", "raw": "$211.4K - $290.6K"},
  "postedAt": "2026-04-07T17:12:35.753000Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "requisitionId": null,
  "descriptionHtml": "<h1><strong>About Ramp</strong></h1>...",
  "descriptionText": "About Ramp\n\nRamp is building ...",
  "changeType": "new",
  "scrapedAt": "2026-10-25T21:10:02Z"
}
```

#### Output fields

| Field | Type | Description |
|---|---|---|
| `jobId` | string | Stable unique ID: `platform:board:sourceJobId`. Use it to deduplicate. |
| `sourceJobId` | string | The job's ID in the ATS. |
| `platform` | string | `greenhouse`, `lever`, `ashby`, `workday`, `bamboohr`, `workable`, `recruitee`, `smartrecruiters`, `personio`, `oraclehcm`, `rippling`, `teamtailor`, `breezy` or `gem`. |
| `boardSlug` | string | Board identifier (e.g. `stripe`, `nvidia.wd5/NVIDIAExternalCareerSite`, `jpmc.fa/CX_1001`). |
| `boardUrl` | string | Public careers board URL. |
| `company` | string | Company name as reported by the ATS (or the slug). |
| `title` | string | Job title. |
| `department` / `team` | string | Department and team, where the ATS provides them. |
| `locations` | array | Parsed locations: `raw`, `city`, `region`, `country`, `countryCode` (ISO 3166-1 alpha-2), `isRemote`. |
| `locationText` | string | Primary location as written in the ATS. |
| `isRemote` | boolean | True if the job is remote or any of its locations is remote. |
| `workplaceType` | string | `REMOTE`, `HYBRID`, `ONSITE` or null. |
| `employmentType` | string | `FULL_TIME`, `PART_TIME`, `CONTRACT`, `INTERN`, `TEMPORARY`, `OTHER` or null. |
| `employmentTypeRaw` | string | Employment type exactly as the ATS reports it. |
| `salary` | object | `min`, `max`, `currency`, `interval` (`YEAR`, `MONTH`, `WEEK`, `DAY`, `HOUR`), `source` (`structured` or `text`), `raw`. Null if no pay is published. |
| `postedAt` / `updatedAt` | string | ISO 8601 UTC timestamps, where available. |
| `jobUrl` / `applyUrl` | string | Job page and application page. |
| `requisitionId` | string | Employer's requisition number, where available. |
| `descriptionHtml` / `descriptionText` | string | Full description as HTML and plain text (if `includeDescription` is on). |
| `changeType` | string | `new` or `removed` in *Only new* mode, otherwise null. |
| `firstSeenAt` / `removedDetectedAt` | string | Only on `removed` records. |
| `scrapedAt` | string | When the record was fetched (ISO 8601 UTC). |

The **run summary** is saved to the key-value store record `OUTPUT`: totals plus per-board status, counts, duration and errors (e.g. `Board not found (404)`), plus `unresolvedInputs` (entries that were not a board URL, slug or known company) and `resolvedByLookup` (company names matched in the built-in list).

#### What each platform provides

| Field | Greenhouse | Lever | Ashby | Workday |
|---|---|---|---|---|
| Salary | pay-transparency ranges, else text | `salaryRange`, else text | compensation API, else text | text only |
| Department / team | ✓ | ✓ (team; dept if set) | ✓ | – |
| Employment type | only via custom metadata | ✓ | ✓ | ✓ |
| Workplace type | from location text | ✓ | ✓ | from location text |
| Posted date | first published | created | published | start date / "Posted N days ago" |
| Updated date | ✓ | – | – | – |

| Field | BambooHR | Workable | Recruitee | SmartRecruiters | Personio |
|---|---|---|---|---|---|
| Salary | compensation text on the job page | text only | ✓ salary fields | compensation fields (needs descriptions on), else text | salary fields where published, else text |
| Department / team | department | ✓ (team = function) | department | department or function | ✓ (team = category) |
| Employment type | ✓ | ✓ | ✓ | ✓ | ✓ |
| Workplace type | ✓ (onsite / remote / hybrid) | remote flag only | ✓ | remote / hybrid flags | from location text |
| Posted date | ✓ | published | published | released | created |
| Updated date | – | – | ✓ | – | – |

| Field | Oracle Cloud HCM | Rippling | Teamtailor | Breezy HR | Gem |
|---|---|---|---|---|---|
| Salary | text only | pay range fields (needs descriptions on), else text | text only | ✓ salary field (structured on the job page) | text only |
| Department / team | business unit or job family / job function | ✓ | ✓ (team = role) | department | ✓ |
| Employment type | ✓ (schedule) | ✓ (needs descriptions on) | – | ✓ | ✓ |
| Workplace type | ✓ where the employer sets it | ✓ | ✓ (remote status) | remote flag | ✓ |
| Posted date | ✓ | created (needs descriptions on) | published | published | first published |
| Updated date | – | – | – | – | ✓ |

Requests per board: Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor and Gem return a whole board in one or a few requests. BambooHR always needs one request per job (the posted date and pay are only on the job page). SmartRecruiters, Oracle Cloud HCM, Rippling and Breezy HR need one request per job only when descriptions are on; Workday always does. Oracle lists 200 jobs per request and returns at most the newest 10,000 jobs per site. `maxJobsPerCompany` limits these per-job requests.

### Performance

Tested on the Apify platform on Oct 25, 2026 at 256 MB, with the first four platforms:

- 670 jobs from 8 boards (2 per platform, capped at 100 jobs per company) in 47 s.
- A second run in *Only new* mode took 6 s. It delivered the 205 jobs that the cap had held back and nothing it had already delivered.
- The default memory of 256 MB is enough. Peak memory was about 147 MB.

Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor and Gem return a whole board in a few requests, so they are fast. Workday, BambooHR and (with descriptions on) Oracle, Rippling, Breezy and SmartRecruiters need one request per job for details, so large boards on them take longer.

### How it compares

Verified on Apify Store, Oct 25, 2026. Prices shown are the Free-tier price per job.

| Actor | Platforms | Price per 1,000 jobs | Change detection |
|---|---|---|---|
| **This Actor** | Greenhouse, Lever, Ashby, **Workday**, **Oracle Cloud HCM**, BambooHR, Workable, Recruitee, SmartRecruiters, Personio, Rippling, Teamtailor, Breezy HR, Gem | **$1.50** | New + removed jobs |
| bovi/greenhouse-lever-ashby-job-scraper | Greenhouse, Lever, Ashby, Recruitee, SmartRecruiters, Personio (no Workday) | $1.50 | n/a |
| fantastic-jobs/greenhouse-jobs-api / workday-jobs-api | One ATS per Actor | $2.00 | n/a |
| memo23/career-site-ats-jobs-api | 116 ATS platforms | $4.00 + $0.03 per run | n/a |

Source: Apify Store API (`api.apify.com/v2/store`, field `currentPricingInfo`), fetched Oct 25, 2026 (competitor rows; this Actor's platform list updated Oct 26, 2026). Competitor prices can change at any time. "n/a" means not checked.

### FAQ

**Is it legal to scrape job postings?** (Not legal advice.) This Actor reads only public job postings that companies publish so that they get distributed, through the public JSON endpoints that power their own careers pages. It uses no login, collects no personal data about candidates or employees, and doesn't bypass any access control. You are responsible for how you use the data, e.g. respecting each site's terms when you republish it.

**Will I get rate-limited or blocked?** Unlikely at normal volumes. The Actor sends at most 5 parallel requests by default (`maxConcurrency`), and retries 429/5xx errors with exponential backoff (`maxRetries`). No proxies are needed. If a board fails anyway, it is reported in `OUTPUT` and the other boards still finish.

**How do I run it on a schedule?** In Apify Console, open the Actor, save your input as a **task**, then add a **Schedule** (e.g. daily at 08:00). Turn on *Only new jobs* so each run returns only fresh postings.

**Can I send the jobs to Google Sheets, Slack, Zapier, Make or my own app?** Yes. Use Apify **integrations** (Google Sheets, Slack, email, Zapier, Make, n8n and others) on the Actor or task, or add a **webhook** that calls your URL when a run succeeds. You can also read the dataset through the Apify API in JSON, CSV or Excel format.

**Can AI agents use it?** Yes. It can be called through the Apify API and the Apify MCP server. The input and output fields are fully described so agents can call it correctly.

**Why are some fields null?** The ATS doesn't publish them (see the platform table). Nothing is invented: salary is parsed from text only when an explicit currency range appears, and `salary.source` says so.

**Can it search all jobs on Greenhouse or Workday by keyword?** No. These ATSs have no public cross-company search. You list the company boards you care about (or company names from the built-in list), and the Actor fetches and filters them.

**Workday boards with more than 2,000 jobs?** Workday's public list endpoint appears to cap at 2,000 (NVIDIA reports exactly `total: 2000`). Use `keywords` or `postedWithinDays` to narrow very large boards.

**What happens if I set a low timeout or a max charge?** The Actor stops cleanly and saves what it has (see [Stops early instead of timing out](#stops-early-instead-of-timing-out) and [Pricing](#pricing)).

### More tools from SteadyScrape

- [Career Site Jobs Search](https://apify.com/steady-scrape/career-jobs-search): search open jobs by keyword across tens of thousands of company career sites, with remote, country, company and date filters.
- [Remote Jobs API](https://apify.com/steady-scrape/remote-jobs-api): remote-only jobs from employer career sites, filtered by where you can work from (countries, regions, time zones).

### Feedback and support

Found a bug, need another ATS, want a company added to the built-in list, or want a new field? Please open an issue in the **Issues** tab of this Actor.

### Data sources and credits

The built-in company list was compiled from these open datasets, and every board was then re-checked live:

- [job-board-aggregator](https://github.com/Riley-Dorrington/job-board-aggregator) by Riley Dorrington (MIT License).
- [job-board-directory](https://kalebconfer-sys.github.io/job-board-directory/) by kalebconfer-sys (CC0 1.0, public domain).
- Hiring data from **LastRound AI**, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Source: [LastRound AI Salary Transparency Study 2026](https://lastroundai.com/blog/salary-transparency-study-2026) ([dataset](https://github.com/fyrosofttech/lastroundai-hiring-data)). Changes: the company/board entries were filtered to boards that were live with open jobs, normalized to `platform:slug` form and merged with the other sources.
- SmartRecruiters company IDs were added by hand and checked live.
- [ats-career-page-urls](https://huggingface.co/datasets/latmay/ats-career-page-urls) by latmay, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) (Rippling, Teamtailor, Breezy HR, Gem and Oracle Cloud HCM boards). Changes: reduced to board identifiers, filtered to boards that were live with open jobs, company names read from the boards.
- [state-of-ats-2026](https://github.com/Kayvan-Zahiri/state-of-ats-2026) by Kayvan Zahiri (MIT License): Oracle Cloud HCM career-site hosts.
- Board URLs found in the public [Common Crawl](https://commoncrawl.org/) URL index.

Job postings themselves are fetched live from each company's public careers board at run time.

# Actor input Schema

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

Required. One entry per company career board. The ATS platform is auto-detected from the URL. Accepted forms: Greenhouse board URL (https://job-boards.greenhouse.io/stripe) or 'greenhouse:stripe'; Lever (https://jobs.lever.co/spotify) or 'lever:spotify', EU boards 'lever-eu:<slug>'; Ashby (https://jobs.ashbyhq.com/ramp) or 'ashby:ramp'; Workday careers URL (https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite or https://wd3.myworkdaysite.com/recruiting/<tenant>/<site>) or 'workday:nvidia.wd5/NVIDIAExternalCareerSite'; BambooHR (https://acme.bamboohr.com/careers) or 'bamboohr:acme'; Workable (https://apply.workable.com/huggingface/) or 'workable:huggingface'; Recruitee (https://acme.recruitee.com) or 'recruitee:acme'; SmartRecruiters (https://jobs.smartrecruiters.com/Wise) or 'smartrecruiters:Wise'; Personio (https://acme.jobs.personio.de) or 'personio:acme'; Oracle Cloud HCM Candidate Experience URL (https://jpmc.fa.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1001/jobs) or 'oraclehcm:jpmc.fa/CX_1001'; Rippling (https://ats.rippling.com/rippling/jobs) or 'rippling:rippling'; Teamtailor (https://career.teamtailor.com) or 'teamtailor:career' ('teamtailor:careers.acme.com' for a custom domain); Breezy HR (https://acme.breezy.hr) or 'breezy:acme'; Gem (https://jobs.gem.com/accel) or 'gem:accel'. A job URL from the board also works. A company name or domain (e.g. 'Bosch Group', 'stripe.com') is looked up in a built-in list of about 580 live-checked boards; names not in the list are reported under unresolvedInputs in the run summary. A bare slug like 'figma' is looked up in that list, then tried on Greenhouse, Ashby and Lever. Find the slug by opening any job on the company's careers page and reading the URL. Invalid boards are reported in the run summary and do not fail the run.

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

Keep only jobs whose title contains at least one of these words or phrases (case-insensitive substring match, e.g. 'engineer', 'product manager'). Leave empty to keep all titles.

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

Drop jobs whose title contains any of these words or phrases (case-insensitive substring match, e.g. 'intern', 'senior').

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

Keep only jobs with at least one location matching any of these: city, region/state, country name or ISO 3166-1 alpha-2 country code (e.g. 'London', 'California', 'Canada', 'US'). Leave empty for all locations. Combine with 'Remote only' for remote jobs in a given country.

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

If true, keep only remote jobs (flagged remote by the ATS, workplace type REMOTE, or 'Remote' in the location).

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

Keep only jobs whose department or team contains any of these (case-insensitive substring, e.g. 'engineering', 'sales', 'marketing'). Workday boards do not expose departments, so this filter removes all Workday jobs when set.

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

Keep only jobs posted in the last N days (e.g. 7). Jobs without a posted date are kept. Leave empty for any date.

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

If true (default), each job includes the full description as HTML (descriptionHtml) and plain text (descriptionText). Set false for a smaller, faster dataset with metadata only.

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

Maximum number of jobs returned per board, after filters. With 'Only new jobs' it caps new jobs per board per run (held-back jobs come in later runs). Leave empty for no cap. Use a small value (e.g. 10-100) for quick tests and to control cost ($0.0015 per job).

## `onlyNewSinceLastRun` (type: `boolean`):

If true, the Actor remembers which job IDs it already delivered per board (in the named key-value store below) and outputs only jobs not delivered before, marked changeType='new'. Use for scheduled job alerts and change feeds. If false (default), every matching job is returned on every run.

## `firstRunEmitsAll` (type: `boolean`):

Only used with 'Only new jobs'. When a board is seen for the first time: true (default) outputs all its current jobs once; false records them silently as a baseline so you only get jobs posted later.

## `emitRemovedJobs` (type: `boolean`):

Only used with 'Only new jobs'. If true, jobs that disappeared from a board since the last run are output as free records with changeType='removed', firstSeenAt and removedDetectedAt. Requires a complete board listing (for Workday: no cap or keyword pre-filter).

## `stateStoreName` (type: `string`):

Name of the key-value store that keeps delivered job IDs for 'Only new jobs'. Runs that share a name share history. Use a different name for each independent feed or alert.

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

Maximum number of HTTP requests in flight at once across all boards. The default of 5 is polite and fast enough for most runs.

## `maxRetries` (type: `integer`):

How many times to retry a request after HTTP 429, 5xx or a timeout, with exponential backoff.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "remoteOnly": false,
  "includeDescription": true,
  "maxJobsPerCompany": 100,
  "onlyNewSinceLastRun": false,
  "firstRunEmitsAll": true,
  "emitRemovedJobs": false,
  "stateStoreName": "ats-job-feed-state",
  "maxConcurrency": 5,
  "maxRetries": 4
}
```

# Actor output Schema

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

Dataset of job postings (one item per job, unified fields across Greenhouse, Lever, Ashby, Workday, BambooHR, Workable, Recruitee, SmartRecruiters, Personio, Oracle Cloud HCM, Rippling, Teamtailor, Breezy HR and Gem). Use /items without ?view for all fields.

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

Run summary JSON: totals, per-board status (ok, error, incomplete, not-started, charge-limit), job counts, errors such as "Board not found (404)", stoppedEarly info, unresolvedInputs (entries not recognized as a board or known company) and resolvedByLookup.

# 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://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
    ],
    "maxJobsPerCompany": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("steady-scrape/ats-job-feed").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://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    ],
    "maxJobsPerCompany": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("steady-scrape/ats-job-feed").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://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "maxJobsPerCompany": 100
}' |
apify call steady-scrape/ats-job-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steady-scrape/ats-job-feed"
        }
    }
}
```

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/e5sEN9iLUZVwpYdgm/builds/s3LDqYPsx0E0rhY97/openapi.json
