# ATS Job Feed: Greenhouse, Lever, Ashby, Workable, Recruitee (`kayorama/ats-job-board-feed`) Actor

Get every open job from company career pages on Greenhouse, Lever, Ashby, Workable and Recruitee in one clean format, with salary, location and remote fields. Track new and closed jobs between runs. No personal data.

- **URL**: https://apify.com/kayorama/ats-job-board-feed.md
- **Developed by:** [Kayorama](https://apify.com/kayorama) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Security:** Limited permissions
- **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/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: Greenhouse, Lever, Ashby, Workable, Recruitee

Get every open job from a company's own careers page as clean rows. Paste the job board link (or just type the company name) and the Actor reads the official public job feed of Greenhouse, Lever, Ashby, Workable or Recruitee. Turn on change tracking and schedule it, and each run tells you which jobs are new, which changed and which were taken down.

It reads only the public feeds these job boards publish for career sites. No logins, no captcha solving, no proxies needed. It follows each host's robots.txt and waits between requests.

### Good for

- Watching hiring at competitors, customers or target accounts (sales signals)
- Feeding a niche job board or a newsletter with fresh roles
- Tracking how fast a company grows a team, or when it starts hiring in a new country
- Salary research where the company publishes pay ranges (common on Ashby and Lever)

### What you get

One row per job:

| Field | Example |
| --- | --- |
| `company` | GitLab |
| `title` | Senior Backend Engineer, Plan |
| `department`, `team` | Engineering, Plan |
| `location` | Remote, EMEA |
| `locations` | all listed locations |
| `country` | GB |
| `remote`, `workplaceType` | true, remote / hybrid / on-site |
| `employmentType` | FullTime |
| `postedAt`, `updatedAt` | ISO dates |
| `url`, `applyUrl` | job page and apply link |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`, `salaryText` | 120000, 150000, USD, year |
| `descriptionText` (or `descriptionHtml`) | the job ad itself |
| `ats`, `boardToken`, `jobId`, `requisitionId` | where the row came from |
| `scrapedAt` | ISO timestamp |

When change tracking is on, each row also has:

- `changeType`: `first_scan`, `new`, `updated`, `unchanged` or `closed`
- `firstSeenAt`: when the job first showed up in your runs
- `closedAt`: when the job disappeared from the board

Salary fields are filled when the ATS publishes structured pay. If it does not, the Actor looks for a clear range like "$120K - $150K" in the job text.

The dataset has two views: **Jobs** and **Changes**. A `SUMMARY` record in the run's key-value store lists every board with the number of jobs found, rows written, new and closed counts, and any error.

### No personal data

The rows hold job facts only. Recruiter and hiring manager names, emails and phone numbers are never copied, and email addresses, phone numbers and mailto or tel links are removed from job descriptions before they are saved.

### Input

| Field | What it does |
| --- | --- |
| `boardUrls` | Job board links, for example `https://job-boards.greenhouse.io/gitlab`, `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/ramp`, `https://apply.workable.com/huggingface`, `https://bunq.recruitee.com`. Shorthand like `lever:spotify` also works. |
| `companyNames` | Company names when you do not know the link. The Actor tries likely board names on each ATS and keeps the first one that has jobs. |
| `lookupAts` | Which ATS to try for company names |
| `keywords`, `excludeKeywords` | Keep or drop jobs by words in the title |
| `locations`, `departments` | Keep jobs whose location or department contains one of these |
| `remoteOnly` | Only remote jobs |
| `postedWithinDays` | Only jobs posted in the last N days |
| `includeDescription`, `descriptionFormat`, `maxDescriptionChars` | Job text as plain text, HTML or both, with a length cap |
| `trackChanges` | Compare with the previous run and mark new, updated and closed jobs |
| `outputMode` | `all` rows, or `changesOnly` (new, updated and closed jobs only) |
| `stateStoreName` | Named key-value store that keeps the snapshots between runs |
| `maxJobsPerBoard`, `maxJobsTotal` | Limits per board and per run |
| `requestDelayMs` | Wait between requests to the same host (default 1000 ms) |

Example:

```json
{
    "boardUrls": ["https://jobs.ashbyhq.com/ramp", "lever:spotify"],
    "companyNames": ["Notion"],
    "keywords": ["engineer", "data"],
    "trackChanges": true,
    "outputMode": "changesOnly"
}
```

### Change tracking

The first run with `trackChanges` saves a snapshot of each board and marks rows as `first_scan`. From the next run on, rows show `new`, `updated` or `unchanged`, and jobs that are gone come back once as a `closed` row. Schedule the Actor daily and use `changesOnly` to get a short list of what moved.

Snapshots are kept per board, so you can add or remove boards at any time. Filters only change which rows you see; the snapshot always covers the whole board, so a filter change does not create fake new or closed jobs. If a board is cut short by `maxJobsPerBoard`, closed jobs are not reported for that run because the Actor could not see the full board.

### Pricing

Pay per event:

- **Job posting**: each job row written to the dataset (open, new, updated or closed)
- **Job board checked**: each board read successfully

Boards that cannot be found, company names that do not match a board, and boards that fail are not charged. Set a maximum cost per run in the run options and the Actor stops cleanly when it is reached.

### Limits and good to know

- Supported: Greenhouse, Lever (US and EU), Ashby, Workable and Recruitee. Workday, SmartRecruiters and custom career sites are not covered.
- Lever and Ashby do not publish a company name in their feeds, so `company` holds the board name (for example `ramp`) for those boards.
- If a host answers with HTTP 429, the Actor waits as long as the host asks (Retry-After) up to a cap, then skips that board for the run and says so in the summary.
- Data comes straight from each company's public job board. If a company hides a job or changes its board name, that shows up in the next run.

# Actor input Schema

## `boardUrls` (type: `array`):

Career page links on Greenhouse (job-boards.greenhouse.io/gitlab), Lever (jobs.lever.co/spotify), Ashby (jobs.ashbyhq.com/ramp), Workable (apply.workable.com/huggingface) or Recruitee (bunq.recruitee.com). You can also write a short form like greenhouse:gitlab or lever:spotify.

## `companyNames` (type: `array`):

Don't know which ATS a company uses? Type the name, for example Notion or Hugging Face. We try the likely board slug on each ATS and use the first one that has open jobs. Lookups that find nothing are not charged.

## `lookupAts` (type: `array`):

Which systems to check when looking up a company name. Fewer systems means fewer requests.

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

Keep only jobs whose title contains at least one of these words, for example engineer or sales. Case does not matter. Leave empty for all jobs.

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

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

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

Keep only jobs where the location or country contains one of these, for example Amsterdam, Germany or Remote.

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

Keep only jobs whose department or team contains one of these, for example Engineering.

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

Keep only jobs the ATS marks as remote (or with Remote in the location on Greenhouse).

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

Keep only jobs published in the last N days. Leave empty for any date. Jobs without a publish date are dropped when this is set.

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

Add the full job description. Turn off for smaller, faster runs. Email addresses and phone numbers in descriptions are always replaced with \[redacted].

## `descriptionFormat` (type: `string`):

Plain text, HTML, or both.

## `maxDescriptionChars` (type: `integer`):

Longer descriptions are cut to this many characters.

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

Compare each board with the previous run and mark every job as new, updated or unchanged, plus a row for each job that was taken down (closed). The first run only saves the snapshot. Use it with a schedule.

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

All jobs: every open job every run. Changes only: just new, updated and closed jobs, which keeps scheduled runs cheap. Only used when change tracking is on.

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

Named key-value store that keeps the job list between runs. Use a different name for each watch list. Lowercase letters, digits and dashes.

## `maxJobsPerBoard` (type: `integer`):

Read at most this many jobs from one board. If a board has more, closed jobs are not reported for it in that run.

## `maxJobsTotal` (type: `integer`):

Stop writing job rows after this many in one run.

## `requestDelayMs` (type: `integer`):

Minimum wait between two requests to the same API host. A longer Crawl-delay in robots.txt wins.

## Actor input object example

```json
{
  "boardUrls": [
    "https://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp",
    "https://apply.workable.com/huggingface",
    "https://bunq.recruitee.com"
  ],
  "companyNames": [],
  "lookupAts": [
    "greenhouse",
    "lever",
    "ashby",
    "workable",
    "recruitee"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "locations": [],
  "departments": [],
  "remoteOnly": false,
  "includeDescription": true,
  "descriptionFormat": "text",
  "maxDescriptionChars": 20000,
  "trackChanges": false,
  "outputMode": "all",
  "stateStoreName": "ats-job-board-feed-state",
  "maxJobsPerBoard": 1000,
  "maxJobsTotal": 10000,
  "requestDelayMs": 1000
}
```

# Actor output Schema

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

One row per job posting (or only new, updated and closed jobs in changes-only mode).

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

SUMMARY record: totals plus one entry per board (jobs on board, rows written, new/closed counts, errors).

## `changes` (type: `string`):

The same rows with the change tracking columns.

# 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 = {
    "boardUrls": [
        "https://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
        "https://apply.workable.com/huggingface",
        "https://bunq.recruitee.com"
    ],
    "companyNames": [],
    "keywords": [],
    "excludeKeywords": [],
    "locations": [],
    "departments": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("kayorama/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 = {
    "boardUrls": [
        "https://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
        "https://apply.workable.com/huggingface",
        "https://bunq.recruitee.com",
    ],
    "companyNames": [],
    "keywords": [],
    "excludeKeywords": [],
    "locations": [],
    "departments": [],
}

# Run the Actor and wait for it to finish
run = client.actor("kayorama/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 '{
  "boardUrls": [
    "https://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp",
    "https://apply.workable.com/huggingface",
    "https://bunq.recruitee.com"
  ],
  "companyNames": [],
  "keywords": [],
  "excludeKeywords": [],
  "locations": [],
  "departments": []
}' |
apify call kayorama/ats-job-board-feed --silent --output-dataset

```

## MCP server setup

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