# Workday, Greenhouse, Lever, Ashby and ATS Jobs Scraper (`titian_fluorite/ats-jobs-scraper`) Actor

Workday jobs scraper plus Greenhouse, Lever, Ashby, Rippling, BambooHR, Jobvite and iCIMS careers page scraper. Give it companies, get one clean job feed row per opening.

- **URL**: https://apify.com/titian\_fluorite/ats-jobs-scraper.md
- **Developed by:** [Gerald Dobin](https://apify.com/titian_fluorite) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 listed 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Workday, Greenhouse, Lever, Ashby and ATS Jobs Scraper

Give this Actor a list of companies and it returns every open job those companies are advertising,
as clean rows in one schema. It reads each company's applicant tracking system through that
system's own public job board endpoint, so the data is the same data the company publishes on its
careers page: no rendering, no guessing, no stale copies. Fourteen systems are covered today:
Workday, Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio, Teamtailor, Recruitee,
Rippling, Jobvite, Breezy HR, BambooHR and iCIMS. You can point it at a board link such as
`https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` or `https://jobs.lever.co/palantir`,
at a company's own careers page such as `https://vercel.com/careers` (the Actor loads it once and
finds the board behind it), or at a short `system:slug` pair such as `greenhouse:stripe`. Titles,
departments, locations, employment type, posting dates, salary ranges where the company publishes
them, and the full job description in both HTML and plain text all come back in the same field
names no matter which system the job came from.

### Who it is for

**Job aggregators and job boards that need clean feeds.** Point the Actor at a few thousand
employers, schedule it daily, and load the rows straight into your index. Every row carries a
stable `jobId`, so you can upsert on it and spot the jobs that disappeared since yesterday without
diffing free text.

**Recruiters and sourcers tracking target companies.** Keep a watch list of the twenty companies
your candidates care about and see the moment a role opens, with the apply link and the pay range
already in the row instead of buried three clicks into a careers site.

**Sales teams using hiring signals as intent data.** A company that just posted four data
engineering roles is buying a data stack. Filter on job title keywords, push the results into your
CRM, and let your reps work accounts that are visibly investing rather than accounts that merely
match a firmographic filter.

### What you get

One dataset row per unique job. Here is a real row from a test run against Ramp's Ashby board, with
the two long description fields shortened for readability:

```json
{
  "ats": "ashby",
  "companySlug": "ramp",
  "companyName": null,
  "jobId": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "nativeId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "city": "New York City",
  "country": "United States",
  "remote": false,
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "seniority": null,
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "descriptionHtml": "<div><p>Ramp is a financial operations platform ...</p></div>",
  "descriptionText": "Ramp is a financial operations platform ...",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "sourceUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "scrapedAt": "2026-09-07T23:03:29.062Z"
}
```

Every field is present on every row. A field is `null` when the applicant tracking system does not
publish it, never missing and never an empty string. Dates are ISO 8601 in UTC. `workplaceType` is
one of `remote`, `hybrid`, `onsite` or `null`, and `remote` always agrees with it. Salary fields are
plain numbers plus a currency and a period, parsed out of whatever shape the board used.

### How to use it

**Inputs**

| Input | What it does |
|---|---|
| `companies` | One line per company: a board link, a careers page link, or a `system:slug` pair. Required. Workday boards are addressed by two names, so its pair carries both: `workday:nvidia.wd5/NVIDIAExternalCareerSite`. |
| `keywords` | Keep only jobs whose title contains one of these words. Case does not matter. |
| `locations` | Keep only jobs whose location, city or country contains one of these words. |
| `remoteOnly` | Keep only jobs the company marks as remote. |
| `postedAfter` | Keep only jobs posted on or after this date, written as `YYYY-MM-DD`. |
| `includeDescription` | On by default. Turn it off for a faster, cheaper run when you only need titles and links; the two description columns stay in the output as nulls. |
| `maxJobsPerCompany` | Stop after this many jobs per company. `0` means no limit. Worth setting on Workday boards, which often carry thousands of openings: the Actor stops paging as soon as it has this many. See the note below on how this interacts with filters. |
| `maxConcurrency` | How many companies to work on at once. Lower it if a board starts rate limiting. |

**How `maxJobsPerCompany` behaves when you also set a filter.** You always get up to that many
jobs that actually match. On the boards that publish the whole job in the listing, that costs
nothing extra. On the boards that need a second request per job, some filters can only be judged
once that second request has happened: a Workday listing carries no posting date at all, and an
iCIMS job card may carry no location. When a filter of that kind is set, the Actor keeps reading
further down the board until it has enough matching jobs, rather than taking the first few and
then discarding most of them. That costs more requests than an unfiltered run of the same size,
and it stops at 2000 jobs examined per company, so a filter that matches almost nothing on a very
large board returns what it found by then rather than reading the whole board. Filtering on job
title keywords never costs extra on any board, because every listing carries the title.

**A monitoring pattern that works.** Schedule the Actor once a day against the same company list,
write every run into the same named dataset, and deduplicate on `jobId` on your side. A `jobId` you
have not seen before is a new opening. A `jobId` that stops appearing is a closed one. Because the
id is built from the ATS, the board slug and the system's own job id, it stays the same across runs
and across changes to the job title or location.

**Calling it from the API**

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~ats-jobs-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": ["greenhouse:stripe", "workday:ww.wd1/careers", "https://vercel.com/careers"],
    "keywords": ["engineer"],
    "includeDescription": false
  }'
```

Add `&format=csv` to that URL for a spreadsheet instead of JSON. Results also stay in the run's
dataset, so you can page through them later with the standard dataset endpoints.

**MCP.** Apify exposes its Actors over the Model Context Protocol, so an assistant such as Claude
can call this scraper as a tool and answer questions like "which of these fifteen companies is
hiring platform engineers in Berlin right now" without you writing any glue code.

### Coverage and limits

**Covered.** Workday, Greenhouse (including EU boards and embedded job boards), Lever, Ashby,
Workable, SmartRecruiters, Personio, Teamtailor, Recruitee, Rippling, Jobvite, Breezy HR, BambooHR
and iCIMS. Between them these cover most startups and mid-market employers, a good share of large
European ones, and with Workday and iCIMS a large part of the enterprise market too.

**Workday takes two names, not one.** A Workday board lives at a tenant host and publishes one or
more career sites on it, so both halves are needed. Paste the board URL in any shape it comes in,
including the ones with `/en-US/` or a `/job/...` path, and the Actor works out the pair. If you
prefer the short form, write it as `workday:tenant.wdN/SiteName`, for example
`workday:nvidia.wd5/NVIDIAExternalCareerSite`. Workday boards are often very large, so set
`maxJobsPerCompany` when you only want a sample: the Actor stops paging as soon as it has enough
instead of walking the whole board.

**Not covered.** SuccessFactors, Taleo, Oracle Recruiting and JazzHR are not supported. Those
systems either have no public job board endpoint or gate it behind per-tenant credentials.

**A company must expose a public board.** If an employer has switched its board to private, or
publishes jobs only inside a rendered careers page with no underlying system, there is nothing to
read and the Actor says so.

**Descriptions.** Greenhouse, Lever, Ashby, Teamtailor and Recruitee return the job body in the
same call as the listing. Workday, SmartRecruiters, Workable, Rippling, BambooHR, Jobvite, iCIMS
and Breezy need one extra request per job, so those companies take longer and cost more compute;
turn `includeDescription` off when you do not need the body. Personio returns descriptions only
when the employer fills them in on the board.

**Dates.** `postedAt` and `updatedAt` are only filled in when the board publishes a date the Actor
can read exactly, in ISO 8601 or that system's documented format. A date it cannot read becomes
null rather than a guess. Workday is the clearest case: its board list dates a job only as "Posted
Today" or "Posted 5 Days Ago", which become real dates, while "Posted 30+ Days Ago" is a floor
rather than a date and becomes null instead of pretending to be exactly thirty days old. `postedAfter` is checked when the run starts, so a value the Actor cannot
parse stops the run with a message instead of quietly disabling your filter.

**Salary.** Ashby, Lever, Recruitee and Breezy publish structured pay ranges and the Actor passes
them through. BambooHR and Jobvite publish pay as a display string or a schema.org block, and the
Actor reads it when the numbers and the currency are unambiguous; a range it cannot read
confidently is left null rather than reported wrongly. Greenhouse, SmartRecruiters and Workday
usually do not publish a structured range, so `salaryMin` and `salaryMax` are `null` there even
when a range appears inside the description text.

**Companies that fail.** A company that resolves to no known system produces a single row with
`error: "ATS not detected"`. A slug that no longer exists produces a row with
`error: "board not found"`. If a board answers with something that is not the shape it documents,
you get `error: "unexpected response from <system>"` rather than a silent empty result, so a
changed endpoint cannot look like a company that stopped hiring. These rows are free.

**Addresses the Actor will not fetch.** Only plain public `http` and `https` URLs on the default
port are accepted. Links to an IP address, to `localhost` or a `.internal` name, or to any host that
resolves onto a private, loopback, link local or cloud metadata address are refused, and that check
is applied again on every redirect. Job descriptions are stripped down to ordinary formatting tags
before they reach you, so no scripts, styles, embeds or `javascript:` links come through.

**Run size.** Up to 5000 companies and 200000 jobs in one run. Split larger lists across runs.

### Pricing

Pay per unique job delivered. You are charged once for each job row the Actor writes into the
dataset, and nothing else. There is no monthly subscription and no minimum.

- Duplicate jobs inside a run are removed before they reach the dataset and are never charged.
- Error rows are never charged.
- Jobs removed by your `keywords`, `locations`, `remoteOnly` or `postedAfter` filters never reach
  the dataset, so they are never charged. On the systems that need a second request per job, those
  filters are applied before that request, so a narrow filter is a cheap run.

A run that finds nothing costs nothing. If you set a maximum spend on the run, the Actor stops
fetching once it hits that ceiling and finishes cleanly with whatever it has already delivered,
rather than failing.

### Support

Something wrong with a row, or a company whose board the Actor cannot see? Open an issue on the
Actor's Issues tab with the exact `companies` entry you used and the run id. Requests for another
applicant tracking system are welcome; say which one and name a company that uses it, so the
endpoint can be checked against a real board.

Runs are stateless and store nothing beyond the dataset and the key value store record for the run
itself. The Actor reads only public job board endpoints and sends no credentials anywhere.

# Actor input Schema

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

One line per company. Paste a board link such as https://job-boards.greenhouse.io/stripe, https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite, https://jobs.lever.co/palantir or https://ats.rippling.com/closinglock/jobs, or the company's own careers page such as https://example.com/careers. You can also type a system and slug pair such as greenhouse:stripe, rippling:closinglock or bamboohr:greenpeace when you already know the board name. Workday needs both halves of its address, written as workday:tenant.wdN/SiteName, for example workday:nvidia.wd5/NVIDIAExternalCareerSite. Supported systems: Workday, Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio, Teamtailor, Recruitee, Rippling, Jobvite, Breezy HR, BambooHR and iCIMS. If a company careers page is given, the scraper loads it once and finds the job board behind it.

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

Keep only jobs whose title contains one of these words. Case does not matter. Leave empty to keep every job.

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

Keep only jobs whose location, city or country contains one of these words, for example Berlin, London or United States. Leave empty to keep every location.

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

Keep only jobs the company marks as remote.

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

Keep only jobs posted on or after this date, written as YYYY-MM-DD. Jobs with no posting date are dropped when this is set. Leave empty to keep every job.

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

Include the full job description as HTML and as plain text. Turn this off for a faster and cheaper run when you only need titles, locations and links.

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

Stop after this many jobs for each company. Use 0 for no limit. Worth setting for Workday boards, which often carry thousands of openings: the scraper stops paging as soon as it has this many, so a small number is a fast and cheap run.

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

How many companies to work on at the same time. Lower this if a job board starts rate limiting you.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "workday:ww.wd1/careers",
    "https://jobs.lever.co/palantir",
    "https://ats.rippling.com/closinglock/jobs",
    "ashby:linear"
  ],
  "keywords": [],
  "locations": [],
  "remoteOnly": false,
  "includeDescription": true,
  "maxJobsPerCompany": 0,
  "maxConcurrency": 10
}
```

# Actor output Schema

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

Rows with ats, companySlug, jobId, title, department, location, remote, employmentType, salary fields, descriptionText, postedAt, applyUrl and more.

## `jobsCsv` (type: `string`):

The same rows as CSV for spreadsheets and job board imports.

# 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",
        "workday:ww.wd1/careers",
        "https://jobs.lever.co/palantir",
        "https://ats.rippling.com/closinglock/jobs",
        "ashby:linear"
    ],
    "keywords": [],
    "locations": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("titian_fluorite/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://job-boards.greenhouse.io/stripe",
        "workday:ww.wd1/careers",
        "https://jobs.lever.co/palantir",
        "https://ats.rippling.com/closinglock/jobs",
        "ashby:linear",
    ],
    "keywords": [],
    "locations": [],
}

# Run the Actor and wait for it to finish
run = client.actor("titian_fluorite/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://job-boards.greenhouse.io/stripe",
    "workday:ww.wd1/careers",
    "https://jobs.lever.co/palantir",
    "https://ats.rippling.com/closinglock/jobs",
    "ashby:linear"
  ],
  "keywords": [],
  "locations": []
}' |
apify call titian_fluorite/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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