# Career Site Jobs: Workday, Greenhouse, Lever & Ashby (`pfistyle.digital/ats-jobs`) Actor

Workday, Greenhouse, Lever & Ashby jobs in one flat feed with new-job monitoring. Reads full Workday portals beyond the 2,000-job cap. $1 per 1,000 jobs. Works via API and MCP. Unreadable portals are free.

- **URL**: https://apify.com/pfistyle.digital/ats-jobs.md
- **Developed by:** [pfistyle.digital](https://apify.com/pfistyle.digital) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 81.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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

## Career Site Jobs: Workday, Greenhouse, Lever & Ashby

Read public company career sites in one actor:

- **Full Workday portals beyond the 2,000-job cap**: Workday returns at most 2,000 results per search; the actor splits large portals automatically and merges them without duplicates (measured: NVIDIA 2,671 of 2,674 jobs).
- **Four job platforms**: Workday, Greenhouse, Lever and Ashby, via their public job board endpoints.
- **One flat row per job**: title, company, location, remote, posted date, link, source.
- **New-job monitoring**: with `onlyNew`, each run returns jobs absent from the selected previous dataset.

**Pay per event, no subscription:** $1 per 1,000 emitted jobs. Unreadable portals and known jobs skipped with `onlyNew` are free. No browser, no proxy. Works via API and MCP.

Speed, measured on Apify (2026-10-08): about 9 jobs per second on Workday (1,000 jobs in under 2 minutes); Greenhouse and Ashby boards return all jobs in a single request. Large Workday portals beyond Workday's 2,000-result limit are read completely (e.g. all ~2,670 NVIDIA jobs).

### Use cases

- Track new openings at target companies
- Monitor hiring trends and competitor expansion
- Build job feeds for spreadsheets, analytics and AI agents
- Identify companies hiring for a particular role or location

### Input

Paste career page URLs, one per line. That is all you need:

```json
{
  "portals": ["https://boards.greenhouse.io/airbnb"],
  "maxJobsPerPortal": 5
}
```

Accepted forms (all tested):

| Source | Careers page / job board | Also works |
|---|---|---|
| Greenhouse | `https://boards.greenhouse.io/airbnb`, `https://job-boards.greenhouse.io/airbnb`, EU boards | single job links, embed links (`?for=airbnb`), API links, `greenhouse:airbnb` |
| Lever | `https://jobs.lever.co/palantir`, `https://jobs.eu.lever.co/...` | single job links, API links, `lever:palantir` |
| Ashby | `https://jobs.ashbyhq.com/ramp` | single job links, API links, `ashby:ramp` |
| Workday | `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | with language (`/en-US/`), single job links, `myworkdaysite.com` links, `workday:nvidia.wd5/NVIDIAExternalCareerSite` |
| Any of the first three | company name (`Airbnb`) or domain (`airbnb.com`) | detected automatically (Greenhouse, then Lever, then Ashby) |

Upper/lower case, extra spaces, empty lines, `https://` missing and several entries in one line (separated by new lines, commas or semicolons) are all accepted. Duplicates are removed. Via the API, `portals` must be a list (Apify checks this before the run starts).

Entries that cannot be used never fail the run: they are skipped free of charge and listed in `SUMMARY` with a `reason` (`invalid_portal`, `not_detected`, `workday_needs_url`, ...) and a `hint` with a correct example. A run fails only if no portal is given at all.

Options:

- `maxJobsPerPortal`: leave empty to read all jobs; counts retrieved jobs, including known jobs.
- `onlyNew` (default `false`): return and charge jobs absent from `previousDatasetId`; deduplicate within the current run if it is omitted.
- `previousDatasetId` (optional): read-only dataset used to identify previously emitted jobs.

### Output

One row per job (real rows, 2026-10-08):

```json
{
  "title": "Account Manager",
  "company": "Airbnb",
  "location": "London, United Kingdom",
  "remote": false,
  "postedAt": "2026-09-09",
  "url": "https://careers.airbnb.com/positions/8184174?gh_jid=8184174",
  "source": "greenhouse",
  "jobId": "8184174",
  "portal": "greenhouse:airbnb"
}
```

```json
{
  "title": "Senior Systems Architect - Datacenter",
  "company": "nvidia",
  "location": "US, CA, Santa Clara",
  "remote": false,
  "postedAt": "2026-10-08",
  "url": "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite/job/US-CA-Santa-Clara/Senior-Systems-Architect---Datacenter_JR2019127",
  "source": "workday",
  "jobId": "/job/US-CA-Santa-Clara/Senior-Systems-Architect---Datacenter_JR2019127",
  "portal": "workday:nvidia.wd5/NVIDIAExternalCareerSite"
}
```

`postedAt` is an ISO date or `null`. Company is Greenhouse's `company_name` when present, otherwise the organization shortcut; Workday uses the tenant. Company names are never inferred. Workday uses `externalPath` as its stable job ID. No descriptions or recruiter names are collected.

### Pricing

| Event | Price |
|---|---|
| `job` (one emitted row) | $0.001 |
| Actor start (per run, per GB of memory) | $0.00005 |

Known jobs skipped by `onlyNew` and unreadable portals have no job charge. Startup and platform usage are additional. The run checks the cost limit before every charge and stops cleanly if a charge is refused. Unpriced local builds emit without charging.

### Scheduled monitoring

Create an Apify Schedule, for example daily at 06:00 in your chosen timezone. Omit the job limit to read all openings:

```json
{
  "portals": ["ashby:ramp", "greenhouse:airbnb"],
  "onlyNew": true,
  "previousDatasetId": "PREVIOUS_DATASET_ID"
}
```

Set `previousDatasetId` to the last run's dataset ID (select it with the dataset picker). The selected dataset is only read and is never modified. Missing, invalid or inaccessible datasets produce a log warning and a `comparisonNotice` in `SUMMARY`; the run continues with duplicate suppression within the current run. Without a previous dataset, every distinct current row is eligible for output. No persistent history is maintained. A dataset containing only new/changed rows, a filtered result or a cost-interrupted result is an incomplete baseline: rows absent from it can be emitted again. For reliable repeated monitoring, compare with a complete snapshot dataset and update the input through your scheduler or integration; a static schedule does not automatically select the last run.

Jobs are matched by `source:portal:jobId`. Changes to existing job IDs are not emitted; no removal rows are produced.

### Use with AI agents / MCP

MCP server URL:

```
https://mcp.apify.com?tools=pfistyle.digital/ats-jobs
```

Example prompt: *"Use pfistyle.digital/ats-jobs to find new openings at greenhouse:airbnb and lever:palantir. Return titles, locations and job links."*

Direct API call:

```bash
curl -X POST "https://api.apify.com/v2/acts/pfistyle.digital~ats-jobs/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"portals":["greenhouse:airbnb","lever:palantir"],"maxJobsPerPortal":20}'
```

### How it works

The actor reads public JSON endpoints: Greenhouse boards API, Lever postings API (`skip`/`limit=100`), Ashby job-board API, and Workday CXS (`limit=20`, offsets through `total`). Requests are limited to one per second per host, with a 30-second timeout and up to three retries with backoff for 429/5xx and connection errors. Repeated pages without new IDs stop pagination. Ashby jobs with `isListed: false` are excluded.

### Known limitations

#### Which portals work

Any company whose career site runs on the hosted job boards of Workday, Greenhouse, Lever or Ashby. Tested on Apify on 2026-10-08 without blocks or rate limits: nvidia and salesforce (Workday), airbnb (Greenhouse), palantir (Lever), ramp (Ashby). Portals that cannot be read are skipped and free; the reason is in `SUMMARY`.

#### Limitations

- Only Greenhouse, Lever, Ashby and Workday are supported. A company's own careers domain is detected only if it uses Greenhouse, Lever or Ashby with a board named after the company; Workday always needs the careers page URL (Workday cannot be found by company name). Other ATS platforms and private boards are skipped free.
- Workday returns at most 2,000 results per search. Larger portals are split automatically by Workday's own filters (e.g. job family) and merged without duplicates. If a portal cannot be split below 2,000, it is read up to 2,000 jobs and reported in `SUMMARY` with `truncated: true`, `reason: portal_cap` and the portal's `totalAtPortal`.
- Workday dates are derived from English relative labels (Today, Yesterday, N Days Ago). `30+ Days Ago` has no exact date and returns `null`.
- Greenhouse and Workday remote detection only checks for “remote” in location text. Lever and Ashby use their remote fields; hybrid jobs are not automatically remote.
- Missing or invalid dates return `null`. Job descriptions, filters and job-detail requests are not supported.
- A job limit may mark a portal `stopped` even if it happens to equal the actual job count; cleanup requires proven complete retrieval.
- Dataset job IDs drive monitoring; changes to an existing job are not emitted with `onlyNew`.

Each run writes a per-portal `SUMMARY` record in its default key-value store:

```json
[
  {"portal":"workday:nvidia.wd5/NVIDIAExternalCareerSite","source":"workday","status":"ok","jobs":2671,"newJobs":2671,"truncated":false,"totalAtPortal":2674,"invalidRecords":1,"reason":null,"detail":null},
  {"portal":"lever:missing","source":"lever","status":"skipped","jobs":0,"newJobs":0,"truncated":false,"totalAtPortal":null,"invalidRecords":0,"reason":"not_found","detail":"Public ATS endpoint unavailable (HTTP 404)"}
]
```

`status`: `ok`, `skipped`, `partial` (failed after output or Workday limit), `stopped` (cost or job limit), `not_processed`. `reason`: `not_found`, `blocked`, `invalid_portal`, `unreachable`, `cost_limit`, `job_limit`, `portal_cap`, or `null`. `jobs` counts emitted rows; `newJobs` counts emitted previously unknown rows. `totalAtPortal` is the job count reported by the portal (Workday). `invalidRecords` counts broken records from the portal that were skipped (not charged). The status line shows, for example, `2 jobs from 1 portals; skipped, not charged: lever:missing (not_found)`.

### Unreadable portals are free

Unreadable portals are skipped and processing continues. Partial reads charge only delivered rows; delivered jobs can be excluded on a later run by selecting this run's dataset with `previousDatasetId`. Invalid inputs, blocked or missing endpoints, and connection failures produce no job charge for an unreadable portal.

# Actor input Schema

## `portals` (type: `array`):

One per line. Paste the URL of the company's careers page or job board, for example <code>https://boards.greenhouse.io/airbnb</code>, <code>https://jobs.lever.co/palantir</code>, <code>https://jobs.ashbyhq.com/ramp</code> or <code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite</code>. Links to single jobs, shortcuts like <code>greenhouse:airbnb</code>, plain company names and domains (<code>airbnb.com</code>) also work: Greenhouse, Lever and Ashby boards are detected automatically. Workday needs the careers page URL. Entries that cannot be read are skipped free of charge; the reason and a correct example are in the run's SUMMARY.

## `maxJobsPerPortal` (type: `integer`):

Leave empty to read all jobs of each portal. Use a small number for a quick test.

## `onlyNew` (type: `boolean`):

Compare with rows in previousDatasetId. Without a readable previous dataset, only duplicates within this run are suppressed.

## `previousDatasetId` (type: `string`):

Optional read-only dataset for comparison with a previous run. Set previousDatasetId to the last run's dataset ID. If absent or unreadable, only duplicates within this run are suppressed. A changes-only dataset is not a complete historical baseline.

## Actor input object example

```json
{
  "portals": [
    "https://boards.greenhouse.io/airbnb"
  ],
  "maxJobsPerPortal": 5,
  "onlyNew": false
}
```

# Actor output Schema

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

Flat job rows from public career portals.

# 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 = {
    "portals": [
        "https://boards.greenhouse.io/airbnb"
    ],
    "maxJobsPerPortal": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("pfistyle.digital/ats-jobs").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 = {
    "portals": ["https://boards.greenhouse.io/airbnb"],
    "maxJobsPerPortal": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("pfistyle.digital/ats-jobs").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 '{
  "portals": [
    "https://boards.greenhouse.io/airbnb"
  ],
  "maxJobsPerPortal": 5
}' |
apify call pfistyle.digital/ats-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pfistyle.digital/ats-jobs"
        }
    }
}
```

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/dKYuccfFlfAV3rN4o/builds/IboNFKgiQbUmr0NEr/openapi.json
