# ATS Job Board Feed: Greenhouse, Lever & Ashby Jobs (`opendatalabs/ats-job-board-feed`) Actor

Pulls open jobs from company job boards on Greenhouse, Lever and Ashby through their public APIs into one clean schema. Filter by location, department, keyword or remote, and track only new, changed and closed jobs between runs. No recruiter names or emails.

- **URL**: https://apify.com/opendatalabs/ats-job-board-feed.md
- **Developed by:** [Shyheim Ulrickson](https://apify.com/opendatalabs) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.70 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Board Feed: Greenhouse, Lever & Ashby Jobs

**Use it to** get every open job from the companies you track, straight from their Greenhouse, Lever and Ashby job boards, in one clean table, and on a schedule get only the jobs that are new, changed or closed.

**Cost:** $2 per 1,000 jobs returned, plus $0.50 per 1,000 closed-job alerts. A default run is capped at 100 rows, which is at most $0.20.

**Why this one**

- **Three ATSs, one schema.** Greenhouse, Lever and Ashby jobs come back with the same fields: company, title, location, department, remote flag, dates, apply link and pay range when it is published.
- **Only what changed.** Change tracking returns new and changed jobs plus a `job-closed` row when a posting comes down, so a daily schedule is a hiring-signal feed, not the same list every day.
- **Straight from the source.** It reads each ATS's official public job-board API, the same feed the company's careers page uses. No browser, no login, no proxies.
- **No personal data.** Recruiter and hiring-manager names, emails, phone numbers and profile links never reach the output.

**Sample output** (real run on the prefilled boards `airbnb` on Greenhouse, `leverdemo` on Lever and `ashby` on Ashby, Oct 20, 2026):

| company | title | location | department | isRemote | ats |
|---|---|---|---|---|---|
| Airbnb | Associate Principal, Procurement Innovation Supplier Programs | United States | Financial Planning and Analysis | true | greenhouse |
| Leverdemo | Customer Success Manager | Atlanta, Georgia | Customer Success | false | lever |
| ashby | Senior Product Designer | Remote - US | Design | true | ashby |

That run read 253 open jobs across the three boards in under 3 seconds, made 3 API requests, and returned 100 rows (the default cap, shared across the boards).

### What it does

- **Accepts whatever you paste.** A board token (`airbnb`), the careers board URL (`https://boards.greenhouse.io/airbnb`, `https://jobs.lever.co/leverdemo`, `https://jobs.ashbyhq.com/ashby`), a single job URL, or the API URL itself.
- **Normalises three ATSs.** Departments, teams, employment type, workplace type, all listed locations and salary ranges are mapped into the same fields whichever ATS the company uses.
- **Filters before you pay.** Title keywords (include and exclude), location, department or team, and remote/not remote. Only matching jobs are returned and charged.
- **Tracks changes.** In change mode each board's baseline lives in a named key-value store. Jobs are labelled `new` or `changed`, unchanged jobs are skipped, and jobs that disappeared are returned as `job-closed` rows.
- **Optional descriptions.** Turn on **Include job description text** to get the description as plain text, with contact details removed.

### Input

Minimum viable input:

```json
{
  "greenhouseBoards": ["airbnb"]
}
```

Engineering jobs in New York or remote, across several companies:

```json
{
  "greenhouseBoards": ["airbnb", "https://boards.greenhouse.io/stripe"],
  "leverSites": ["leverdemo"],
  "ashbyBoards": ["ashby"],
  "titleKeywords": ["engineer"],
  "excludeTitleKeywords": ["intern"],
  "locationKeywords": ["new york", "remote"],
  "maxItems": 500
}
```

A daily hiring-signal monitor that alerts only on jobs posted from now on:

```json
{
  "greenhouseBoards": ["airbnb"],
  "ashbyBoards": ["ashby"],
  "mode": "changes",
  "skipExistingOnFirstRun": true,
  "stateStoreName": "my-hiring-monitor"
}
```

#### Key settings

| Field | Default | Notes |
|---|---|---|
| `greenhouseBoards` | — | Greenhouse board tokens or URLs. If none of the three board fields is filled, the run uses the demo boards (`airbnb`, `leverdemo`, `ashby`), capped at 100 jobs (at most $0.20), instead of failing. |
| `leverSites` | — | Lever site names or URLs. EU sites (`jobs.eu.lever.co`) are detected automatically. |
| `ashbyBoards` | — | Ashby job board names or URLs. Case-sensitive. |
| `mode` | `snapshot` | `changes` returns only new, changed and closed jobs. |
| `emitClosedJobs` | `true` | Change mode: return `job-closed` rows. |
| `skipExistingOnFirstRun` | `false` | Change mode: save today's jobs silently and alert only on later ones. |
| `stateStoreName` | `ats-job-board-feed-state` | Change mode: where the baseline lives. Use one per saved task. |
| `titleKeywords` / `excludeTitleKeywords` | — | Case-insensitive, any word matches. |
| `locationKeywords` / `departmentKeywords` | — | Case-insensitive, any word matches. |
| `remote` | `any` | `remote` or `notRemote` to filter. |
| `includeDescription` | `false` | Adds redacted plain-text descriptions. |
| `maxItems` | `100` | Rows per run, shared fairly across boards. |
| `requestsPerSecond` | `2` | Global ceiling. robots.txt crawl-delay is honoured as well. |

### Output

One row per job. A real row from the run above:

```json
{
  "type": "job",
  "ats": "lever",
  "boardToken": "leverdemo",
  "company": "Leverdemo",
  "jobId": "ad208490-4052-4f91-a57e-433f2d1e484b",
  "title": "Customer Success Manager",
  "location": "Atlanta, Georgia",
  "locations": ["Atlanta, Georgia"],
  "department": "Customer Success",
  "team": "Customer Success Team",
  "employmentType": "Regular Full Time (Salary)",
  "workplaceType": "hybrid",
  "isRemote": false,
  "postedAt": "2026-08-31T18:12:19.703Z",
  "applyUrl": "https://jobs.lever.co/leverdemo/ad208490-4052-4f91-a57e-433f2d1e484b/apply",
  "jobUrl": "https://jobs.lever.co/leverdemo/ad208490-4052-4f91-a57e-433f2d1e484b",
  "salary": { "min": 85000, "max": 175000, "currency": "USD", "interval": "per-year-salary" },
  "scrapedAt": "2026-10-20T14:24:26.947Z"
}
```

In change mode a row also carries `changeType` (`new` or `changed`) and `firstSeenAt`. A posting that came down is returned as a `job-closed` row with this shape (illustrative timestamps):

```json
{
  "type": "job-closed",
  "ats": "greenhouse",
  "boardToken": "airbnb",
  "company": "Airbnb",
  "jobId": "7789554",
  "title": "Associate Legal Counsel, Japan",
  "location": "Tokyo, Japan",
  "department": "Legal",
  "changeType": "closed",
  "firstSeenAt": "2026-10-19T12:00:00.000Z",
  "closedDetectedAt": "2026-10-20T12:00:00.000Z",
  "scrapedAt": "2026-10-20T12:00:00.000Z"
}
```

`ats` + `boardToken` + `jobId` is a stable unique key across runs. The dataset ships two views: **Jobs** and **New, changed and closed**.

`RUN_SUMMARY` in the key-value store lists, per board, how many jobs were on the board, how many matched your filters, how many rows were written, the change counts, any error, and the charged events.

### Use cases

- **Sales and recruiting signals.** A company opening five data roles is buying data tools. Watch your target accounts and get only the new postings.
- **Job aggregators and niche job boards.** Pull clean, deduplicated jobs from the source ATS instead of scraping careers pages.
- **Competitive intelligence.** Track which teams competitors are growing or shutting down, through `new` and `job-closed` rows.
- **Labour-market research.** Remote share, locations and published pay ranges across companies, in one schema.

### Limits and things to know

- **You supply the boards.** The Actor reads companies you name; it does not search all of Greenhouse, Lever or Ashby. The token is the first part of the path on the company's job board.
- **Public postings only.** It reads the public job-board APIs that the ATSs publish for careers pages. Unlisted Ashby postings are skipped.
- **Personal data is removed, best effort.** Only an allow-list of job fields is copied; Greenhouse custom fields, which can hold recruiter or hiring-team details, are never output. Descriptions are off by default, and when on, emails, phone numbers, personal profile links and named recruiter or "Hi, I'm …" introductions are replaced with placeholders.
- **Company name.** Greenhouse reports the company name; for Lever and Ashby the board name is used.
- **`updatedAt` is Greenhouse only.** Lever and Ashby do not publish an update time, so "changed" there means a tracked field such as title, location, team or pay changed.
- **Change mode's first run returns everything** unless you set `skipExistingOnFirstRun`. Renaming `stateStoreName` starts a fresh baseline.
- **No false closures from a bad response.** A board that fails to load leaves its baseline untouched, and a board that suddenly shows zero jobs must stay empty for two runs before its jobs are reported closed.
- **Rows held back by `maxItems` or your spending limit are not lost** in change mode: they are returned on the next run.
- **robots.txt is honoured** on every API host, including any crawl-delay.

### Pricing events

| Event | Price | Charged when |
|---|---|---|
| `job` | $0.002 ($2 per 1,000) | One job row was written to the dataset. |
| `job-closed` | $0.0005 ($0.50 per 1,000) | One job-closed row was written in change mode. |

Jobs filtered out, unchanged jobs in change mode, and boards that failed to load are not charged. A board that cannot be read (for example a mistyped name) adds one free `board-error` row with the reason and is named in the run's status message; the other boards still run. When the run reaches your maximum total charge it writes only the rows already paid for and stops cleanly, with the reason in `RUN_SUMMARY`. `RUN_SUMMARY.charging` shows units of work next to units billed.

### FAQ

#### How do I find a company's board token?

Open the company's careers page and look at the job-board address: `boards.greenhouse.io/<token>`, `jobs.lever.co/<site>` or `jobs.ashbyhq.com/<name>`. You can also paste that URL, or any single job URL, and the Actor works out the token.

#### How do I get only new jobs every day?

Set **Run mode** to only new and changed jobs and schedule the Actor (Console > Schedules). Turn on **Skip existing jobs on the first run** if you want alerts only for jobs posted from now on.

#### Can it search all companies on Greenhouse, Lever or Ashby?

No. You choose the companies. The Actor reads the public job boards of the companies you list, which keeps results precise and runs cheap.

#### Does it collect recruiter names or emails?

No. Only job fields are copied. Descriptions are off by default and, when on, have emails, phone numbers, personal profile links and named recruiter introductions removed.

#### Can I send new jobs to Google Sheets, Slack or my CRM?

Yes. Use Apify's integrations (Google Sheets, Slack, Zapier, Make, n8n, webhooks) or the API to forward each scheduled run's new jobs.

### Feedback

This Actor is new and actively maintained. If a board fails, a field is missing, or a result looks wrong, please open an issue on the **Issues** tab with the run ID and the board. We read and reply to every issue, and requests for more ATSs or filters are very welcome.

# Actor input Schema

## `greenhouseBoards` (type: `array`):

Greenhouse job boards to read. Give the board token (airbnb) or any board or job URL, such as https://boards.greenhouse.io/airbnb or https://job-boards.greenhouse.io/airbnb/jobs/123. The token is the first part of the path on the company's hosted board. If no board is given in any of the three fields, the run uses the demo boards (airbnb, leverdemo, ashby).

## `leverSites` (type: `array`):

Lever job sites to read. Give the site name (leverdemo) or a URL such as https://jobs.lever.co/leverdemo. EU-hosted sites (jobs.eu.lever.co/...) are detected from the URL and read from Lever's EU API.

## `ashbyBoards` (type: `array`):

Ashby job boards to read. Give the job board name (ashby) or a URL such as https://jobs.ashbyhq.com/ashby. Names are case-sensitive and may contain spaces, exactly as they appear in the company's jobs.ashbyhq.com address.

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

Snapshot returns every open job that matches your filters. Only new and changed jobs compares each board against the previous run's baseline, saved in a named key-value store, and returns just the jobs that are new or changed, plus job-closed rows for jobs that came down. The first change-tracking run returns everything and saves the baseline.

## `emitClosedJobs` (type: `boolean`):

Change tracking only. When on, a job delivered on an earlier run that is no longer on the board is returned as a job-closed row. A board that suddenly returns zero jobs is only treated as closed after two empty runs in a row, to avoid false alarms.

## `skipExistingOnFirstRun` (type: `boolean`):

Change tracking only. When on, the first run quietly saves the current jobs as the baseline without returning or charging for them, so later runs only alert on jobs posted from now on.

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

Change tracking only. Name of the key-value store that keeps the baseline between runs. Use a different name per saved task if you track different filters on the same boards. Letters, digits and hyphens only.

## `titleKeywords` (type: `array`):

Keep only jobs whose title contains at least one of these words or phrases. Matching ignores case. Leave empty to keep every title.

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these words or phrases, for example intern or senior. Matching ignores case.

## `locationKeywords` (type: `array`):

Keep only jobs whose location (any of its listed locations) contains at least one of these words, such as a city, state or country. Matching ignores case. Leave empty for all locations.

## `departmentKeywords` (type: `array`):

Keep only jobs whose department or team contains at least one of these words, such as engineering or sales. Matching ignores case. Leave empty for all departments.

## `remote` (type: `string`):

Keep all jobs, only remote jobs, or only on-site and hybrid jobs. Remote is read from the ATS's own workplace field when the company sets it, and otherwise from the location text.

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

Adds the job description as plain text. Off by default to keep rows small. Email addresses, phone numbers, personal profile links and named recruiter or hiring-manager mentions are removed from the text.

## `maxItems` (type: `integer`):

Upper limit on rows returned per run, shared fairly across all boards. The default of 100 keeps a run at about $0.20. Rows held back by this limit are returned on the next change-tracking run rather than lost.

## `requestsPerSecond` (type: `integer`):

Ceiling on API requests per second across the whole run. Each board is usually one request, so the default is plenty. Any crawl-delay published in the ATS's robots.txt is also honoured.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for one board API response before retrying. Very large boards with descriptions can take a while.

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

How many times to try a request that fails with a network error, a rate limit or a server error, with growing pauses in between.

## `respectRobotsTxt` (type: `boolean`):

Checks each ATS API host's robots.txt before requesting it and skips anything it disallows. Leave this on.

## Actor input object example

```json
{
  "greenhouseBoards": [
    "airbnb",
    "https://boards.greenhouse.io/stripe"
  ],
  "leverSites": [
    "leverdemo",
    "https://jobs.lever.co/leverdemo"
  ],
  "ashbyBoards": [
    "ashby",
    "https://jobs.ashbyhq.com/ashby"
  ],
  "mode": "snapshot",
  "emitClosedJobs": true,
  "skipExistingOnFirstRun": false,
  "stateStoreName": "ats-job-board-feed-state",
  "titleKeywords": [
    "engineer",
    "data scientist"
  ],
  "excludeTitleKeywords": [
    "intern"
  ],
  "locationKeywords": [
    "new york",
    "london",
    "remote"
  ],
  "departmentKeywords": [
    "engineering"
  ],
  "remote": "any",
  "includeDescription": false,
  "maxItems": 100,
  "requestsPerSecond": 2,
  "requestTimeoutSecs": 60,
  "maxRetries": 4,
  "respectRobotsTxt": true
}
```

# Actor output Schema

## `results` (type: `string`):

Job rows (snapshot mode) or new, changed and job-closed rows (change tracking).

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

Run summary: per-board job counts, rows written, change counts, failures and charged events.

# 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 = {
    "greenhouseBoards": [
        "airbnb"
    ],
    "leverSites": [
        "leverdemo"
    ],
    "ashbyBoards": [
        "ashby"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("opendatalabs/ats-job-board-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 = {
    "greenhouseBoards": ["airbnb"],
    "leverSites": ["leverdemo"],
    "ashbyBoards": ["ashby"],
}

# Run the Actor and wait for it to finish
run = client.actor("opendatalabs/ats-job-board-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 '{
  "greenhouseBoards": [
    "airbnb"
  ],
  "leverSites": [
    "leverdemo"
  ],
  "ashbyBoards": [
    "ashby"
  ]
}' |
apify call opendatalabs/ats-job-board-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,opendatalabs/ats-job-board-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/0WsTNiC4i6p8DTV6k/builds/6fAwZmVUBfwFGk3dM/openapi.json
