# Greenhouse, Lever, Ashby, Workable, Workday & Recruitee (`debonaire_sedan/job-tracker-career-boards`) Actor

Collect and filter current job listings from Greenhouse, Lever, Ashby, Workable, Workday and Recruitee career pages. Export normalized titles, locations, descriptions, salary, remote status, posting dates, and application links.

- **URL**: https://apify.com/debonaire_sedan/job-tracker-career-boards.md
- **Developed by:** [Mohammad Salman Farsi Khan](https://apify.com/debonaire_sedan) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

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

## Greenhouse, Lever, Ashby, Workable, Workday & Recruitee Jobs Scraper

Collect open roles from public company career pages and normalize them into a searchable dataset. This career page scraper fetches job listings from six supported applicant tracking systems and can inspect an ordinary company website for a linked board as a best-effort fallback.

Search for a greenhouse jobs api, lever jobs scraper, ashby jobs, workable jobs, or workday careers: this Actor covers all six with one normalized output.

### What it does

- Fetches public job listings from 6 supported ATS platforms: Greenhouse, Lever, Ashby, Workable, Workday and Recruitee.
- Detects the board from a direct career URL, or checks up to three company website pages for a supported career-board link (best effort).
- Tries all supported boards when you enter only a company name.
- Normalizes titles, locations, departments, employment types, remote status, salary, dates, descriptions, and application links.
- Adds rule-based seniority, job category, country code, and days-since-posted fields without AI calls.
- Supports filters for title keywords, location text, remote-only jobs, and posting age.
- Writes job rows to the dataset. Per-company summaries and run totals are stored separately under `SUMMARY` in the key-value store.
- Assigns stable job IDs and removes duplicates within a run.

### Who it is for

- **Recruiters** collecting roles across career pages.
- **Sales teams** tracking hiring as a company signal.
- **Job boards** gathering and standardizing public job listings.

### Supported boards

| Board | Example public career URL |
|---|---|
| Greenhouse | `https://boards.greenhouse.io/stripe` |
| Lever | `https://jobs.lever.co/aircall` |
| Ashby | `https://jobs.ashbyhq.com/openai` |
| Workable | `https://apply.workable.com/getresponse` |
| Workday | `https://illumina.wd1.myworkdayjobs.com/illumina-careers` |
| Recruitee | `https://natilik.recruitee.com` |

This Actor is not affiliated with, endorsed by, or sponsored by the ATS vendors whose boards it supports. It reads public career-board pages and Workday's internal JSON endpoints only.

### How to use it in 3 steps

1. Enter direct career-board URLs, ordinary company website URLs, or company names in the career pages list. A normal Workday careers URL such as `https://illumina.wd1.myworkdayjobs.com/illumina-careers` is enough: the tenant, the `wd` server and the site are read from the URL. The older `tenant|wd1|site|locale` pipe form still works.
2. Set an optional job limit or filters. `maxJobsPerBoard: 0` means no limit.
3. Start the Actor and review or export the job rows in the dataset.

### Input example

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/aircall",
    "https://jobs.ashbyhq.com/openai",
    "https://ramp.com"
  ],
  "maxJobsPerBoard": 20,
  "titleKeyword": "Engineer",
  "remoteOnly": false
}
```

You can also enter a company name such as `Stripe`. Use `locationContains`, `postedWithinDays`, or `includeSalaryDetails` to refine the run. Salary details are enabled by default, and `includeRaw` is off by default.

### Output fields

Every dataset row is one job. Fields are additive: a field is populated only when the source actually provides the value, otherwise it is empty or null.

| Field | Type | Description |
|---|---|---|
| id | string | Stable job identifier used for deduplication within a run. |
| company | string | Board slug, for example `ramp`. |
| company_name | string or null | Human-readable company name where the source or input provides one. For Greenhouse this is the response `company_name` when present; otherwise it falls back to the name typed in `companies`, or the slug. Null for Workday when no name is available. |
| title | string | Job title. |
| location | string | Human location string from the source. |
| is_remote | boolean or null | Remote marking from the source board, as implemented. Can disagree with `workplace_type`. |
| department | string | Department or team from the source. |
| salary_text | string | Salary text from the source, where provided. |
| posted_at | string | ISO posting date, where available. |
| posted_at_source | string or null | How `posted_at` was obtained: `exact` for an exact source date, `estimated` for a Workday relative label, `unknown` for a Workday `Posted 30+ Days Ago` label with no detail where `posted_at` is null. |
| job_url | string | Job link. The public posting URL, or null for a Workday posting with no public link. |
| seniority | string | Rule-based seniority from the title. Empty when no level is recognized. |
| job_category | string | Rule-based job category from the title. |
| country_code | string | Country code resolved from location/source data. |
| days_since_posted | integer or null | Days from `posted_at` to the run date. Empty when `posted_at` is empty. |
| description_text | string or null | Plain text from the job description, where the adapter provides one. Null for boards without a description and for Workday postings fetched without detail. |
| description_html | string or null | Raw HTML job description from the source, where the adapter provides one. Null when the source has no description. |
| employment_type | string or null | Source employment type where provided, for example full-time or contract. Null when the source does not expose it. |
| workplace_type | string or null | Source workplace label, for example remote, hybrid, or on-site, where the board exposes one. Null when unknown. |

`description_text`, `description_html`, `employment_type`, `posted_at_source`, `workplace_type` and `company_name` are populated only where the code already has the value. They are null when the source does not provide the value.

The dataset holds job rows only. The per-company summary and run totals are stored in the run's key-value store under `SUMMARY`. Each job row is one billable `job` event; the `SUMMARY` record and per-company/board state records are not billed as job events.

### Pricing

**$1.50 per 1,000 jobs. See the Pricing tab for current rates.** Each job row is one billable `job` event. Company summaries and run totals in `SUMMARY` are not charged as job events.

When a run is on the Apify free plan and is not the configured owner account, the Actor still collects and normalizes jobs the same way, but it stops pushing job rows after the first 50 billable job rows for that run. That cap applies to pushed and billed job rows only, not to the number of postings the Actor may find, filter, or summarize internally. The cap is only on Apify's free plan; it is not a feature of the scraper itself and is not applied outside the free plan. For current limits and pricing, see the Pricing tab.

### FAQ

#### Can I enter an ordinary company website?

Yes, as a best-effort fallback. The Actor checks the supplied page and up to two more career-page locations, for at most three page fetches per company. It follows linked career pages and looks for links or embedded scripts that point to a supported board. If it finds no board, it logs the supported URL formats and continues the run.

#### What happens when I enter only a company name?

The Actor tries the name against supported boards that use company identifiers. A Workday careers URL carries the tenant and site, so enter it directly or provide the ordinary company website that links to it. A direct career-board URL is the clearest input when a company uses more than one board.

#### Will every job have salary and a posting date?

Only when the board provides the field. The Actor leaves unavailable values empty. Greenhouse dates, departments, offices, and descriptions come from the main jobs response. When `includeSalaryDetails` is true, capped parallel detail requests are used for Greenhouse pay ranges only; turn it off to skip those requests.

#### What is different about Workday rows?

Workday detail fetching is on by default through the `includeWorkdayDetail` input (default `true`). When it is on, every Workday posting gets one detail request, and `detailMax` does not limit those requests. The detail record contributes:

- `description_html` from the detail `jobDescription`
- `description_text` from the same HTML via the plain-text routine
- `posted_at` from the detail `startDate`, with `posted_at_source = "exact"`
- `employment_type` from the detail `timeType`
- `job_url` from the detail `externalUrl` when present, otherwise from the listing path

When `includeWorkdayDetail` is off, or when a detail request fails after retries, the row is still kept. In that case `description_text`, `description_html`, and `employment_type` may be empty, and `posted_at` may come only from the listing's relative label or be null. A relative label such as `Posted 30+ Days Ago` has no day count, so `posted_at` stays null and `posted_at_source` is `unknown` for that posting when no detail is available.

Workday listings can exceed the verified 2,000-result cap on a single list query. When the offset-0 total is at or above 2,000, the Actor splits the board by the first usable facet and fetches each slice separately. If the unique postings found equal the sum of that facet's counts in the base response, the board is complete. If they do not, the board is incomplete, and `SUMMARY` records both numbers. A slice that still reaches the cap is capped, and a posting with no public link is still emitted with `job_url` null.

#### How are the derived fields decided?

They use fixed keyword and location rules, with no AI calls. Seniority is assigned only when a recognized level appears in the title. Executive-level seniority is reserved for C-level, VP, President, and Head of titles. Job category uses title keywords and falls back to `other` when a title is present but no listed category matches. Country codes come from country data, major city names, or an unambiguous location value. Days since posted is calculated from the ISO posting date.

#### How accurate are the derived fields?

They are not yet measured in this version. A labelled fixture file exists for the owner to fill; until it has rows and a test prints a result, the Actor does not publish an accuracy number.

#### How are remote status and workplace type related?

`is_remote` follows the source board's remote marking as implemented. `workplace_type` is the source's raw label. The two can disagree. For example, an Ashby posting can show `workplaceType` "Hybrid" with `isRemote` true when a secondary location is remote. The `remoteOnly` filter uses `is_remote`, not `workplace_type`.

#### Where are run summaries stored?

The dataset contains job rows only. The per-company summary and run totals are stored in the run's key-value store under `SUMMARY`.

#### Workday terms and robots.txt

Workday's site terms (<https://www.workday.com/en-us/legal/site-terms.html>) prohibit scraping and bypassing robots.txt. Workday's End User Agreement, section 4.3(c), prohibits scraping Website content. This Actor calls Workday's internal `/wday/cxs/` JSON endpoints, which are not a documented public API. Before each Workday list or detail request the Actor fetches `robots.txt` once per host with `Accept: text/plain, */*` and checks the URL against the parsed rules using the same User-Agent it sends. Status 404 means allowed; status 200 is parsed; any other status stops that board and reports it. The Actor also blocks any path containing `/refreshFacet/`.

The Actor does not use a browser-like User-Agent and does not add proxy rotation, fingerprint spoofing, CAPTCHA solving, or headless browsers to get past blocks. If a Workday board is blocked, the Actor stops that board and reports it. Users are responsible for complying with Workday's terms.

#### Coverage

The supported-boards table above lists the boards this version handles. Field coverage is
measured, never written by hand: every local run writes `column_fill_*.csv` under
`runs/reports/`, with one row per board and the filled/total count for each output field.
The current numbers are in “Measured output coverage” under Live results above.

#### Watch mode

Watch mode is not enabled in this version. When it is enabled later, set `onlyChanges` to true and the Actor will keep state in a named key-value store called `job-tracker-state`. In watch mode, only new and changed rows are emitted and billed; unchanged rows are neither emitted nor charged. The first run emits and charges every posting. Changes to descriptions and dates are not tracked in the first watch-mode version.

### Live results

Measured runs, recorded as they were executed (Python 3.14.7, no Apify SDK installed).
Reproduce any of them with the command shown.

#### 2026-10-13 — offline run, five boards, no Apify

Command: `python tools/run_local.py --input examples/sample_actor_input.json --max 10`

Result: 5/5 boards returned data — greenhouse/stripe 722 postings, lever/aircall 77,
ashby/openai 800, workable/getresponse 7, recruitee/natilik 6; 43 rows written for
`maxJobsPerBoard: 10`; 15 HTTP requests; 8.4 s. Writes `runs/reports/join_*.json`, one
CSV per platform, and `column_fill_*.csv`.

#### 2026-10-13 — the same input through the Actor code path

Command: `python -m src.main` with `INPUT.json` in the Actor folder. With no Apify SDK
installed this uses the local runtime and writes `dataset.jsonl` and `summary.json`.

Result: `boards_found 5`, `boards_failed 0`, `jobs_found 1612`, `jobs_pushed 43`,
`rows_filtered_out 0`, `rows_without_link 0`, `elapsed_seconds 8.0`. The 43 dataset rows
carry all 25 columns, including `company_name` and `posted_at_source`.

#### 2026-10-13 — Workday detail against a live tenant

Input: `{"companies": ["https://illumina.wd1.myworkdayjobs.com/illumina-careers"],
"maxJobsPerBoard": 3, "includeWorkdayDetail": true}`

Result: 165 unique postings listed. Every listed row came back with `description_text`,
`description_html`, `employment_type`, an exact `posted_at` from the detail record, and a
resolvable `job_url`. The board is reported `incomplete` because its unique posting count
is below the sum of the base facet counts; that is the documented completeness rule, not a
request failure.

#### 2026-10-13 — per-ATS smoke test

Command: `python tools/smoke_test.py` — `RESULT: all four boards returned a sample job row`.

#### Measured output coverage

From the offline run's `column_fill_*.csv` (43 rows across five boards, share of rows with
a non-empty value): `id`, `title`, `company`, `location`, `job_url`, `apply_url`,
`posted_at`, `description_text` and `description_html` 100%; `employment_type` 76.7%;
`is_remote` 58.1%; `workplace_type` 53.5%; `salary_currency` 34.9%; `salary_min`,
`salary_max` and `salary_text` 30.2%; `office` 23.3%. A field stays empty when the source
does not publish it.

#### Not yet measured

- \[ ] NVIDIA Workday list-only run, `includeWorkdayDetail: false`
- \[ ] A run on Apify itself, to confirm the platform entrypoint and billing events
- \[ ] Derived-field accuracy against the labelled fixture (see the accuracy caveat above)

# Changelog

This Actor's version history is a separate document: https://apify.com/debonaire_sedan/job-tracker-career-boards/changelog.md

# Actor input Schema

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

Enter a company website or supported board URL; the actor checks up to three pages for a supported ATS link (best-effort fallback), while a company name is tried on boards that accept a company identifier.

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

The maximum number of rows to return from each board; set to 0 for no limit.

## `titleKeyword` (type: `string`):

Optional case-insensitive text that must appear in the job title.

## `locationContains` (type: `string`):

Optional case-insensitive text that must appear in the job location.

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

Keep only jobs marked remote by the source board.

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

Optional age filter. Jobs without a usable posted date are omitted when this filter is set.

## `includeSalaryDetails` (type: `boolean`):

Use salary ranges in the Greenhouse listing when available, and request capped per-job details for pay ranges only when enabled. Greenhouse dates, departments, offices and descriptions always come from the main response.

## `concurrency` (type: `integer`):

Set how many board APIs to query at once.

## `includeDetail` (type: `boolean`):

Fetch optional per-job details for supported boards other than Greenhouse and Workday (Workday uses includeWorkdayDetail).

## `detailMax` (type: `integer`):

Limit optional per-job detail requests per board (Workday uses includeWorkdayDetail).

## `includeRaw` (type: `boolean`):

Include each source job object in the output row.

## `includeWorkdayDetail` (type: `boolean`):

Fetch Workday job detail for every Workday posting (description, exact posted date, and employment type). This does not count against detailMax.

## `startUrls` (type: `array`):

Use legacy platform and company records instead of career-page URLs.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/trovohealth",
    "https://jobs.lever.co/kinsta",
    "https://jobs.ashbyhq.com/obvious"
  ],
  "maxJobsPerBoard": 20,
  "remoteOnly": false,
  "includeSalaryDetails": true,
  "concurrency": 4,
  "includeDetail": false,
  "detailMax": 300,
  "includeRaw": false,
  "includeWorkdayDetail": true
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# 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://boards.greenhouse.io/trovohealth",
        "https://jobs.lever.co/kinsta",
        "https://jobs.ashbyhq.com/obvious"
    ],
    "maxJobsPerBoard": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("debonaire_sedan/job-tracker-career-boards").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://boards.greenhouse.io/trovohealth",
        "https://jobs.lever.co/kinsta",
        "https://jobs.ashbyhq.com/obvious",
    ],
    "maxJobsPerBoard": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("debonaire_sedan/job-tracker-career-boards").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://boards.greenhouse.io/trovohealth",
    "https://jobs.lever.co/kinsta",
    "https://jobs.ashbyhq.com/obvious"
  ],
  "maxJobsPerBoard": 20
}' |
apify call debonaire_sedan/job-tracker-career-boards --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,debonaire_sedan/job-tracker-career-boards"
        }
    }
}
```

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/8ntkXMmDLg9ajKDbI/builds/WSAH4efqrv67aQBZv/openapi.json
