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

Aggregate public ATS job boards across Greenhouse, Lever, Ashby and Workday: normalize, dedupe, and emit hiring signals. Public endpoints only; PII fields filtered.

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

## Pricing

from $2.00 / 1,000 job scrapeds

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

## Job Board Scraper — Greenhouse, Lever, Ashby & Workday (ATS Aggregator)

A single **job scraper** that aggregates public **jobs data** across four major applicant
tracking systems — **Greenhouse**, **Lever**, **Ashby** and **Workday** — then normalizes every
posting into one schema, de-duplicates roles that appear on more than one board, and (in monitor
mode) emits **hiring signals** when a company opens or closes reqs.

It reads only official, public job-board endpoints (the same JSON these career sites load in your
browser). It does **not** touch any login-gated page and it **filters personal data** out of the
output by design.

- **Greenhouse API** — `boards-api.greenhouse.io` public board API
- **Lever postings** — `api.lever.co/v0/postings` public postings API
- **Ashby** — `api.ashbyhq.com/posting-api/job-board` public posting API
- **Workday scraper** — public `/wday/cxs/{tenant}/{site}/jobs` career-site search endpoint

### Use cases

- **B2B sales & market intent** — "who is hiring a Salesforce admin / a Kubernetes SRE / a
  RevOps lead right now" is a strong buying signal. Monitor mode turns new reqs into `jobs_added`
  events you can route to sales.
- **Recruiting & talent mapping** — track competitor headcount plans and open roles by department
  and location across every ATS at once.
- **Salary / compensation research** — capture `salary_min` / `salary_max` where the board exposes
  it (Ashby and Lever frequently do; pay-transparency roles on others).
- **Job-board aggregation** — build a clean, de-duplicated jobs feed from many companies and many
  ATS platforms without writing four different scrapers.

### Input

```jsonc
{
  "targets": [
    { "ats": "greenhouse", "token": "stripe",   "company": "Stripe" },
    { "ats": "lever",      "token": "palantir",  "company": "Palantir" },
    { "ats": "ashby",      "token": "ramp",      "company": "Ramp" },
    { "ats": "workday",    "token": "salesforce", "company": "Salesforce",
      "workday": { "host": "salesforce.wd12.myworkdayjobs.com", "tenant": "salesforce", "site": "External_Career_Site" } }
  ],
  "includeDescription": false,   // true = add PII-scrubbed plain-text description (max 5000 chars)
  "mode": "snapshot",            // "snapshot" | "monitor"
  "maxJobsPerTarget": 0           // 0 = all public postings
}
```

**Finding the token/host for each ATS**

| ATS | `token` is… | Extra |
| --- | --- | --- |
| greenhouse | board token in `boards.greenhouse.io/<token>` | — |
| lever | slug in `jobs.lever.co/<token>` | — |
| ashby | slug in `jobs.ashbyhq.com/<token>` | — |
| workday | tenant name | also set `workday.host` / `workday.tenant` / `workday.site` from the public career-site URL `https://<host>/<lang>/<site>` |

### Output

Every job is normalized to the same whitelist of fields (no raw source JSON is ever passed
through):

```jsonc
{
  "ats": "ashby", "company": "Ramp",
  "job_id": "…", "canonical_key": "sha1(company|title|location)",
  "title": "Customer Activation Manager | Enterprise",
  "department": "Sales", "team": null,
  "location": "New York", "locations": ["New York", "Remote (US)"], "remote": false,
  "salary_min": 147000, "salary_max": 262000, "salary_currency": "USD",
  "posted_at": "2026-05-11T12:47:52.238Z", "updated_at": null,
  "url": "https://jobs.ashbyhq.com/ramp/…",
  "description_text": null,
  "sources": [ { "ats": "ashby", "company": "Ramp", "job_id": "…", "url": "https://…" } ]
}
```

### De-duplication & hiring signals

**De-dupe.** The same role can appear multiple times (posted per-shift, or listed on two ATS during
a migration). Items sharing a `canonical_key` — `sha1(company | normalized_title | primary_location)`
— collapse into the single most complete record, and every origin is kept in `sources[]`.

**Monitor mode.** Set `"mode": "monitor"`. The actor stores a per-company snapshot (the set of
`canonical_key`s) in a persistent key-value store and, on the next run, emits signal items:

```jsonc
{ "type": "signal", "event": "jobs_added",   "company": "Stripe", "count": 5, "titles": ["…"], "total_active": 123, "ts": "…" }
{ "type": "signal", "event": "jobs_removed", "company": "Stripe", "count": 2, "titles": ["…"], "total_active": 123, "ts": "…" }
```

Job items have no `type` field; signal items are tagged `"type": "signal"` and operational
records (a board that could not be fetched) are tagged `"type": "status"`, so all three are easy to
filter apart in the dataset.

### Compliance

- **Public endpoints only.** The actor calls the official, public job-board APIs that these career
  sites serve to anonymous browsers. It never accesses `/admin`, applicant, or any login-gated
  page, and for Workday it only calls the public CXS search endpoint — never an authentication host.
- **Personal data is filtered out.** Output is a strict field whitelist: recruiter names, contact
  emails and similar fields are never mapped. When `includeDescription` is enabled, the description
  text is additionally run through an email/phone scrubber before it is stored.
- **robots.txt** is fetched and logged for each host as a courtesy trace.
- **Your responsibility.** You should review each ATS provider's Terms of Service and each target
  company's terms before scraping at scale, and confirm your use complies with applicable law
  (including data-protection rules in your jurisdiction). This actor is a tool; how you use it is up
  to you.

### FAQ

**Which ATS platforms are supported?** Greenhouse, Lever, Ashby and Workday public boards.

**Do I need an API key or login?** No. Only public endpoints are used; no authentication.

**Does it use a headless browser?** No — it is pure HTTP/JSON, which is fast and cheap.

**Can it fetch full job descriptions?** Yes, set `includeDescription: true`. Descriptions are
converted to plain text, scrubbed of emails/phone numbers, and truncated to 5000 characters.

**How does Workday pagination work?** The actor pages the public CXS endpoint 20 results at a time.
Workday only returns the true total on the first page, which the actor accounts for.

**What if one company or ATS fails?** Each target is isolated — a failure is recorded as a `status`
item (e.g. `endpoint_unavailable`, `rate_limited`, `workday_probe_failed`) and the run continues.

### Local development

```bash
npm install
npm test                 # unit tests (sanitize / normalize / dedupe / signals / schema)
npm run typecheck
apify run                # or: npx tsx src/main.ts   (reads storage/key_value_stores/default/INPUT.json)
```

# Actor input Schema

## `targets` (type: `array`):

ATS boards to aggregate. Each item: { ats, token, company, \[workday:{host,tenant,site}] }. ats is one of greenhouse|lever|ashby|workday. token is the public board token (Greenhouse board token, Lever/Ashby company slug). For workday, also provide the host/tenant/site of the public career site.

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

When true, output description\_text (HTML converted to plain text, PII stripped, truncated to 5000 chars). Default false to keep payloads small and minimize data.

## `mode` (type: `string`):

snapshot = output the current normalized jobs. monitor = compare against the previous run (stored in the key-value store) and emit jobs\_added / jobs\_removed hiring signals.

## `maxJobsPerTarget` (type: `integer`):

Cap on jobs fetched per target. 0 = no cap (fetch all public postings).

## Actor input object example

```json
{
  "targets": [
    {
      "ats": "greenhouse",
      "token": "stripe",
      "company": "Stripe"
    },
    {
      "ats": "lever",
      "token": "palantir",
      "company": "Palantir"
    },
    {
      "ats": "ashby",
      "token": "ramp",
      "company": "Ramp"
    },
    {
      "ats": "workday",
      "token": "salesforce",
      "company": "Salesforce",
      "workday": {
        "host": "salesforce.wd12.myworkdayjobs.com",
        "tenant": "salesforce",
        "site": "External_Career_Site"
      }
    }
  ],
  "includeDescription": false,
  "mode": "snapshot",
  "maxJobsPerTarget": 0
}
```

# 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 = {
    "targets": [
        {
            "ats": "greenhouse",
            "token": "stripe",
            "company": "Stripe"
        },
        {
            "ats": "lever",
            "token": "palantir",
            "company": "Palantir"
        },
        {
            "ats": "ashby",
            "token": "ramp",
            "company": "Ramp"
        },
        {
            "ats": "workday",
            "token": "salesforce",
            "company": "Salesforce",
            "workday": {
                "host": "salesforce.wd12.myworkdayjobs.com",
                "tenant": "salesforce",
                "site": "External_Career_Site"
            }
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pentababy/ats-job-board-scraper").call(input);

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = { "targets": [
        {
            "ats": "greenhouse",
            "token": "stripe",
            "company": "Stripe",
        },
        {
            "ats": "lever",
            "token": "palantir",
            "company": "Palantir",
        },
        {
            "ats": "ashby",
            "token": "ramp",
            "company": "Ramp",
        },
        {
            "ats": "workday",
            "token": "salesforce",
            "company": "Salesforce",
            "workday": {
                "host": "salesforce.wd12.myworkdayjobs.com",
                "tenant": "salesforce",
                "site": "External_Career_Site",
            },
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pentababy/ats-job-board-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 '{
  "targets": [
    {
      "ats": "greenhouse",
      "token": "stripe",
      "company": "Stripe"
    },
    {
      "ats": "lever",
      "token": "palantir",
      "company": "Palantir"
    },
    {
      "ats": "ashby",
      "token": "ramp",
      "company": "Ramp"
    },
    {
      "ats": "workday",
      "token": "salesforce",
      "company": "Salesforce",
      "workday": {
        "host": "salesforce.wd12.myworkdayjobs.com",
        "tenant": "salesforce",
        "site": "External_Career_Site"
      }
    }
  ]
}' |
apify call pentababy/ats-job-board-scraper --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ap36h68SdhL3cQNba/builds/kb0o525VzfBh6ZH2P/openapi.json
