# Greenhouse, Lever, Ashby & Workday Job Scraper | 7 ATS (`softyways/greenhouse-lever-ashby-workday-job-scraper`) Actor

Scrape job postings from 7 ATS - Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable and Recruitee - into one unified schema. Every run tells you what changed since the last one: new, updated and removed listings. No login, no browser, no proxies. $1.50 per 1,000 jobs.

- **URL**: https://apify.com/softyways/greenhouse-lever-ashby-workday-job-scraper.md
- **Developed by:** [Softyways](https://apify.com/softyways) (community)
- **Categories:** Jobs, Developer tools, Automation
- **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 postings

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/platform/actors/running/actors-in-store#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

## Greenhouse, Lever, Ashby & Workday job scraper with change tracking

Scrape **job postings from company career pages** across **7 applicant tracking systems** — Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable and Recruitee — and get one clean dataset with every open role. Paste a careers URL, get structured jobs: title, location, department, employment type, apply link.

The part other scrapers leave to you: every run also tells you **what changed since the last one** — which jobs are new, which were edited, and which disappeared.

Runs on Apify, so you can schedule it daily, call it from the API, and pipe the output into a spreadsheet, a database or your own app.

### What does this job scraper do?

It reads the **public JSON endpoints** the ATS platforms already serve to their own careers pages. No login, no browser, no proxies, no anti-bot circumvention — which is why a run of 745 jobs takes about three seconds.

For each career page you give it, you get:

1. every currently open job posting, normalized into the same shape regardless of which ATS it came from;
2. a `changeType` on every row: `new`, `updated`, `unchanged` or `removed`.

### Which ATS platforms are supported?

| Platform | Example URL to paste |
| --- | --- |
| Greenhouse | `https://job-boards.greenhouse.io/stripe` |
| Lever | `https://jobs.lever.co/spotify` |
| Ashby | `https://jobs.ashbyhq.com/linear` |
| Workday | `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` |
| SmartRecruiters | `https://jobs.smartrecruiters.com/BoschGroup` |
| Workable | `https://apply.workable.com/moneyfarm` |
| Recruitee | `https://channable.recruitee.com` |

Paste the careers URL and the platform is detected automatically. You can also write it explicitly as `greenhouse:stripe`.

### How to scrape jobs from a Greenhouse job board

1. Open the company's careers page and copy the URL from the address bar.
2. Paste it into the **Career pages** field. Add as many companies as you like.
3. Click **Start**. The first run returns every open job, all marked `new`.
4. Run it again tomorrow — or put it on a schedule — and the second run tells you what moved.

The same three steps work for Lever, Ashby, Workday, SmartRecruiters, Workable and Recruitee.

### How do I know which jobs are new since yesterday?

Change tracking is on by default. State lives in a named key-value store (`ats-jobs-tracker-<trackerName>`) that survives between runs, so a daily schedule gives you a rolling feed of hiring changes.

- **`new`** — was not in the previous run
- **`updated`** — title, location, department or employment type changed; `previousTitle` and `previousLocation` hold the old values
- **`unchanged`** — still there, untouched
- **`removed`** — was in the previous run and is gone from the career page now

Set `outputMode` to `changes` to get only the first, second and fourth — useful when you are feeding alerts rather than rebuilding a table.

Use `trackerName` to keep separate sets of companies on separate histories.

### What data do you get for each job?

| Field | Description |
| --- | --- |
| `key` | Stable id across runs (`ats:board:jobId`) |
| `ats`, `board` | Which platform and which company board it came from |
| `companyName` | Company name as the platform reports it |
| `title` | Job title |
| `department`, `team` | As categorized by the company |
| `location`, `locations` | Primary location and the full list |
| `remote` | Remote flag where the platform exposes one |
| `employmentType` | Full-time, contract, internship… where available |
| `url`, `applyUrl` | Link to the posting and to the application form |
| `publishedAt`, `updatedAt` | Platform timestamps where available |
| `description` | Full text, only when `includeDescription` is on |
| `changeType` | `new`, `updated`, `unchanged`, `removed` |
| `firstSeenAt`, `lastSeenAt`, `removedAt` | When this Actor first and last saw the job |
| `previousTitle`, `previousLocation` | What the values were before an update |

You can download the dataset in JSON, CSV, Excel, HTML or XML, or read it straight from the API.

Example row:

```json
{
  "ats": "greenhouse",
  "board": "figma",
  "companyName": "Figma",
  "jobId": "5364702004",
  "title": "Account Executive, Emerging Enterprise",
  "department": "Sales",
  "location": "Berlin, Germany",
  "locations": ["Berlin, DE"],
  "employmentType": null,
  "remote": null,
  "url": "https://boards.greenhouse.io/figma/jobs/5364702004",
  "publishedAt": "2024-11-01T06:05:10-04:00",
  "changeType": "new",
  "firstSeenAt": "2026-07-31T12:00:00.000Z",
  "previousTitle": null
}
```

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `boards` | — | List of career page URLs. Required. |
| `trackChanges` | `true` | Compare with the previous run and label every job. |
| `outputMode` | `all` | `all` returns every job; `changes` returns only new, updated and removed ones. |
| `trackerName` | `default` | Runs sharing a name share history. |
| `includeDescription` | `false` | Adds the full job description. Slower, much larger output. |
| `maxConcurrency` | `5` | How many career pages to fetch at once. |

### How much does it cost to scrape job postings?

**$1.50 per 1,000 jobs**, billed per row returned. No monthly fee, no minimum.

| You track | Roughly |
| --- | --- |
| 10 small companies (~300 jobs) | $0.45 per run |
| A daily watch on 3,000 jobs | $4.50 per run |
| One-off pull of 10,000 jobs | $15.00 |

Two ways to spend less: leave `includeDescription` off, and set `outputMode` to `changes` once your first run has established the baseline — after that you only pay for what actually moved, which on a normal day is a small fraction of the board.

Apify's free plan comes with monthly credits, so you can try it without paying anything.

### Cost limit

Every run has a maximum total charge, and one dataset row is one charge. A run that finds more jobs than its limit pays for cannot return all of them.

When that happens this Actor stops writing instead of letting the extra rows disappear quietly:

- the rows it can still pay for go to **changes first** — new, updated and removed listings before unchanged ones;
- the jobs left out are **not recorded as seen**, so the next run reports them again instead of swallowing them forever;
- `RUN_SUMMARY` carries `costLimitReached`, `itemsWithheldByCostLimit` and `costLimitUsd`.

To fit a large run: raise **Maximum cost per run** in the run options, set `outputMode` to `changes`, or split the career pages across several runs.

### What happens when a career page breaks?

A board that goes quiet is never reported as "everything removed", because a false removal would have you take down listings that are still open:

- if a career page **errors** (network, 404, rate limit), its jobs are carried over untouched;
- if it **answers with an empty list**, removals are withheld until it comes back empty twice in a row — on a single run, a silent format change and a company that genuinely stopped hiring look identical.

Boards held back this way are listed under `removalsWithheld` in `RUN_SUMMARY`, together with counts, failures and cost-limit status.

### FAQ

#### Is scraping job postings legal?

This Actor reads **public job postings** through the endpoints the ATS platforms serve to their own careers pages — the same data any visitor sees, without logging in or bypassing any protection. Job postings are company records, not personal data: no candidate profiles, no emails, no contact details are collected. You are responsible for how you use the output; if you are unsure about your use case, take legal advice.

#### Do I need a proxy?

No. These are public JSON endpoints, so there is nothing to rotate around. That also keeps runs fast and cheap.

#### Why are department and posting date empty for Workday jobs?

Workday's listing endpoint returns the title, location and requisition id, and a relative string like "Posted Today" instead of a date. Fetching the real values would cost one extra request per job, so they are left empty rather than guessed.

#### Can I get only the changes?

Yes — set `outputMode` to `changes`. You still pay only for the rows returned.

#### Why is the description excluded by default?

ATS platforms rewrite whitespace and tracking parameters in descriptions constantly. Including them multiplies dataset size, and comparing them would flag every job as updated every day. The change fingerprint deliberately ignores the description.

#### What about Personio?

Personio's public search endpoint answers `429` to every request, including a single one after a long pause. That is a closed door, not a rate limit, and this Actor does not work around protections.

#### Can you add another ATS?

Open an issue on the Actor's **Issues** tab with the careers URL of a company that uses it. Any platform with a public JSON endpoint is straightforward to add.

### Development

```bash
apify run
```

```bash
npm test
```

# Actor input Schema

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

Paste company career page URLs. Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable and Recruitee are detected automatically. You can also write greenhouse:stripe.

## `trackChanges` (type: `boolean`):

Compare this run with the previous one and label every job as new, updated, unchanged or removed.

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

Return every job, or only what changed since the last run.

## `trackerName` (type: `string`):

Runs sharing a tracker name share change history. Use different names to track different sets of companies independently.

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

Adds the job description text. Makes runs slower and the dataset much larger.

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

How many career pages to fetch at the same time.

## Actor input object example

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/stripe"
  ],
  "trackChanges": true,
  "outputMode": "all",
  "trackerName": "default",
  "includeDescription": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Every posting collected in this run, with its change status.

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

Counts of new, updated, unchanged and removed postings, plus any career pages that failed.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "boards": [
        "https://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("softyways/greenhouse-lever-ashby-workday-job-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "boards": [
        "https://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("softyways/greenhouse-lever-ashby-workday-job-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "boards": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ]
}' |
apify call softyways/greenhouse-lever-ashby-workday-job-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=softyways/greenhouse-lever-ashby-workday-job-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/J3i80Cglxv9SCXM8V/builds/a72auR1w5MKFlT5Bf/openapi.json
