# Workday Jobs Scraper: Full Descriptions, Past the 2,000 Cap (`firstpartydata/workday-jobs-scraper`) Actor

Scrapes job postings from any Workday careers site (\*.myworkdayjobs.com). Returns the full job description and the exact posting date, which Workday's listing endpoint does not provide, and splits large boards by category to get past Workday's hard 2,000-result-per-query ceiling.

- **URL**: https://apify.com/firstpartydata/workday-jobs-scraper.md
- **Developed by:** [First Party Data](https://apify.com/firstpartydata) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 job posting returneds

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?

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

## Workday Jobs Scraper, Full Descriptions, Past the 2,000 Cap

Pulls job postings from any Workday careers site (the `*.myworkdayjobs.com`
kind) through Workday's own public endpoints. No login, no proxy, no browser.

Two things here that most Workday scrapers do not do, both of them measurable in
about a minute against any competitor.

***

### 1. It returns the description and the real posting date

Workday's listing endpoint is thin. Per posting it gives a title, a URL path, a
location string, and a **relative** date: literally the string `"Posted Today"`
or `"Posted 30+ Days Ago"`. No description, no employment type, no exact date.
That listing is what a Workday scraper normally hands you, which is why Workday
job data tends to be unusable for anything except counting.

There is a second public endpoint, one per posting, and this actor follows it:

| Field | Listing endpoint | What you get here |
|---|---|---|
| Description | not published | **2,400–9,500 characters**, HTML and plain text |
| Posted date | `"Posted 3 Days Ago"` | **exact ISO date** |
| Employment type | not published | `FULL_TIME` / `PART_TIME`, vendor wording in `employmentTypeRaw` |
| Work arrangement | not published | Workday's own remote flag, where the employer set it |
| Apply URL | derived from a path | the employer's canonical URL |
| Requisition ID | partial | `jobReqId` |

Verified against eight unrelated tenants, Abbott, NVIDIA, Thales, IQVIA,
Labcorp, Blue Origin and two others, 8 of 8 returned full detail.

It costs one extra request per posting, so it only runs when **Include full job
descriptions** is on, and only for postings that already survived your title and
location filters.

***

### 2. It gets past Workday's 2,000-result ceiling

This one is worth understanding, because it is invisible and it is why two
Workday scrapers can disagree about how many jobs a company has.

**Workday will not return more than 2,000 postings for any single query, and it
does not tell you.** The `total` field is silently clamped to `2000`. Page past
it and the API keeps answering, with rows you have already seen.

Measured on NVIDIA's board:

```
total reported by the API ............ 2,000
distinct postings from naive paging .. 2,000   (offsets 2000-3980 return only duplicates)
postings that actually exist ......... 2,667   (the facet counts say so)
                                       ─────
silently missing ......................  667   = 25% of the board
```

Every facet is a separate query with its own ceiling, so the way through is to
split the board and merge. This actor detects the clamp automatically by
comparing the category counts against the reported total, and only then splits:

```
INFO  workday/nvidia|wd5|NVIDIAExternalCareerSite: 2,667 postings exist but a
      single query returns at most 2,000. Splitting by jobFamilyGroup into 15
      queries to reach the rest.
INFO  Done in 127.3s, 2667 jobs from 1 live board
```

All 2,667. If a board fits under the cap, nothing extra happens and it is not
slower.

This is not one unlucky board. Reading the four largest Workday employers in the
bundled list, measured 14 September 2026. Those boards move daily, so the figure
moves with them:

| Employer | Naive paging | This actor | Recovered |
|---|---:|---:|---:|
| Abbott | 2,000 | **2,669** | +669 |
| NVIDIA | 2,000 | **2,671** | +671 |
| Thales | 2,000 | **2,603** | +603 |
| Airbus | 2,000 | **2,559** | +559 |
| | 8,000 | **10,502** | **+31.3%** |

Any Workday tenant with more than 2,000 open roles is affected, and there are a
lot of them, because that is a normal size for an enterprise employer.

***

### 3. Filtering happens inside Workday, not after

Workday supports a server-side search and server-side facets, and this actor
passes yours straight through. That matters for cost as well as speed: rows
filtered out at the source are rows you are never billed for.

```json
{
  "companies": ["https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"],
  "searchText": "kernel",
  "jobCategory": ["Engineering"]
}
```

```
INFO  workday/nvidia|wd5|NVIDIAExternalCareerSite: filtering at the source to
      Job Category: Engineering.
INFO  Done in 12.4s, 249 jobs (288 postings scanned)
```

288 postings read instead of 2,667, for the same answer.

Category names are matched against whatever that tenant actually publishes, so
you can write `"Engineering"` without hunting for Workday's opaque internal ids.
Name one that does not exist and the log tells you which categories the board
does have.

***

### Input

Paste the careers URL. Any of these shapes works:

```
https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
https://abbott.wd5.myworkdayjobs.com/en-US/abbottcareers
workday:nvidia|wd5|NVIDIAExternalCareerSite
```

A Workday address has three parts: the **tenant** (`nvidia`), the **host**
(`wd5`, which genuinely varies: `wd1` through `wd12` are all in use), and the
**site name** (`NVIDIAExternalCareerSite`). Paste the whole URL and all three are
read off it.

**Or just give the company name.** With `stryker`, `medtronic` or `gartner` the board
is searched for by probing the shapes that real Workday tenants actually use,
cheapest first, stopping at the first hit:

```
INFO  Found a Workday board for "stryker": stryker / wd1 / strykercareers (1455 postings).
INFO  Found a Workday board for "gartner": gartner / wd5 / EXT (762 postings).
```

Be aware of the limit, and it is a real one. Measured against all **3,103**
Workday boards in the bundled list: `wd1`/`wd5`/`wd3` cover 83% of hosts, and a
name search reaches **15% of them**. Site names mostly follow no pattern at all,
`EXT`, `BAH_Jobs`, `signetjeweleryretailsales`, `external_experienced`, and the
search also assumes the tenant matches the company name, which often it does not.
Airbus files under `ag`.

That figure used to read 95% and about half, measured against 120 boards. It was
not wrong then and it is not a regression now: those 120 were large, well-known
employers whose tenant is their own name, and the bundled list is 3,103 boards of
every size. The old number was a property of the sample. **Paste the URL**: it
always works, costs one request, and is the documented way in. When a name search
misses, the log says so and tells you that.

Or switch on **Search the built-in company list** and skip URLs entirely: this
actor ships **3,103 verified live Workday boards carrying 718,931
open postings**, largest employers first.

**The ten biggest Workday employers in the bundled list**

1. **Dollartree**: 24,132 roles
2. **Cvshealth**: 19,485 roles
3. **Lowes**: 12,543 roles
4. **Globalhr**: 4,757 roles
5. **Experienced**: 4,462 roles
6. **NON Postings**: 4,280 roles
7. **Northrop Grumman**: 3,753 roles
8. **Davita**: 3,329 roles
9. **US Emplsv**: 3,085 roles
10. **Micron**: 2,992 roles

Verified live at build time (2026-09-15): every board answered its API with at
least one open posting. Workday publishes no company display name, so these are
derived from the tenant identifier.

***

### Output

One row per posting, with a stable `id` so repeat runs can be de-duplicated:

```json
{
  "id": "workday:nvidia|wd5|NVIDIAExternalCareerSite:JR2017846",
  "title": "Senior Embedded Software Engineer, DPU - Networking",
  "company": "NVIDIA",
  "ats": "workday",
  "locationRaw": "US, MA, Westford",
  "city": null,
  "region": null,
  "country": "United States",
  "countryCode": "US",
  "countryInferred": false,
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "Full time",
  "seniorityLevel": "SENIOR",
  "seniorityInferred": true,
  "workplaceType": null,
  "isRemote": null,
  "descriptionText": "We are looking for a versatile Senior Software Engineer...",
  "applyUrl": "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite/job/...",
  "postedAt": "2026-09-11T00:00:00.000Z",
  "postedOnRaw": "Posted Yesterday",
  "requisitionId": "JR2017846",
  "scrapedAt": "2026-09-12T07:54:44.226Z"
}
```

`isRemote` is **three-valued**: `true`, `false`, or `null` when the employer did
not say. Most Workday tenants leave the remote flag unset, so `null` is common
and it is not the same as `false`. Filtering on **Remote roles only** excludes
unknowns rather than guessing at them.

***

### Running it as a feed

Switch on **Only new or changed jobs** and the actor remembers what it returned
last time, then bills only what moved. Unchanged postings are skipped entirely.
Postings that vanished from the board are listed in the
`REMOVED_SINCE_LAST_RUN` record, so you can close them out of your own index.

On a large Workday board that is the difference between paying for a full re-read
every morning and paying for one, once.

***

### What to expect, honestly

- **Workday is the slowest major ATS to read.** It pages 20 postings at a time,
  so a 2,600-role board is 130 round trips before descriptions. Budget minutes,
  not seconds. Nothing can fix this; the page size is Workday's.
- **Descriptions double the work.** One extra request per posting. Turn them off
  when you only need to know who is hiring for what.
- **Most tenants do not set the remote flag.** Expect `isRemote: null` on the
  majority of postings; the description usually says, but this actor will not
  guess from prose.
- **No structured salary.** Workday exposes no pay field. Where a range is
  written into the description in an unambiguous, currency-marked form it is
  parsed out; otherwise `compensation` is `null` rather than a guess.
- **Boards move.** Tenants get renamed and sites get retired. Those return 404,
  are counted separately in `RUN_SUMMARY.boardsNotFound`, and never fail the run.
- **Public data only, and worth being precise about.** The endpoint this actor
  reads is the one a Workday careers page calls to draw its own list. It is
  public and unauthenticated, and it is not a feed Workday documents for
  third-party use. Nothing authenticates, submits an application, or touches
  candidate data, and the requests carry a User-Agent naming this actor.

Unofficial: this actor is not affiliated with or endorsed by Workday, Inc.

# Actor input Schema

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

One or more Workday careers sites, e.g. https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite. Pasting the whole URL is exact and always works, because the tenant, the wdN host and the site name are all read off it. You can also give a bare company name such as stryker and the board will be searched for, which succeeds about half the time: site names are frequently unguessable (EXT, BAH\_Jobs). Leave empty and switch on the built-in list below.

## `useBuiltInCorpus` (type: `boolean`):

Read the bundled list of verified live Workday boards instead of, or as well as, the URLs above. Largest employers first. Use this for market-wide questions; use the URLs field when you already know which employers you care about.

## `corpusSample` (type: `integer`):

How many bundled Workday boards to read, largest first. Workday boards are big and slow, so start small. 10 boards can already be tens of thousands of postings. 0 means every board in the list.

## `searchText` (type: `string`):

A keyword query handed to Workday's own search rather than applied afterwards. This is the cheapest filter available: Workday narrows the result set before anything is fetched, so you are not billed for rows you did not want. Example: kernel engineer.

## `jobCategory` (type: `array`):

Workday's own Job Category facet, matched by name against whatever the board publishes. Write Engineering, not an internal id. Name one that does not exist and the run log lists the categories that board actually offers. Combine with the search box above to narrow twice at the source.

## `titleInclude` (type: `array`):

Keep only postings whose title contains one of these, case-insensitively. Applied before any description is fetched, so it also cuts the per-posting detail requests. Example: engineer, developer.

## `titleExclude` (type: `array`):

Drop postings whose title contains any of these. Example: intern, contract, director.

## `location` (type: `array`):

Keep only postings whose location contains one of these. Workday writes locations as the employer typed them, often 'US, CA, Santa Clara', so match on a city or country rather than a postal code.

## `countries` (type: `array`):

Keep only postings in these countries. Accepts an ISO code or a country name (DE and Germany both work), because Workday's detail endpoint returns the country as a name while other sources use codes. Requires Include full job descriptions, since the listing endpoint carries no country field.

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

Keep only postings whose description contains one of these. Requires Include full job descriptions to be on, since the listing carries no description. Prefer the Workday search box above where you can. It is faster and cheaper.

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

Keep only postings Workday marks as remote. Note that most Workday tenants never set the remote flag, so this is a strict filter and will return little on many boards. Postings with no stated arrangement are excluded rather than guessed at.

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

0 disables the filter. With descriptions on, this uses the exact date from Workday's detail endpoint; with descriptions off it uses the approximate date parsed from strings like 'Posted 3 Days Ago'.

## `includeDescriptions` (type: `boolean`):

Follow each posting to Workday's detail endpoint for the full description, the exact posting date, the employment type and the remote flag. This is the feature most Workday scrapers lack. It costs one extra request per posting, so turn it off when you only need to know who is hiring for what.

## `onlyNewOrChanged` (type: `boolean`):

Return only postings that are new or that have changed since the last run with the same settings. Unchanged rows are skipped and not billed, which is what makes a daily feed on a large Workday board affordable. The first run establishes the baseline and returns everything.

## `stateKey` (type: `string`):

Names the history incremental mode reads and writes. Leave blank and one is derived from your settings, so two differently-filtered feeds never contaminate each other.

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

Hard ceiling on results. Workday boards are large (a single enterprise tenant can hold several thousand roles), so this defaults low deliberately. Raise it once you know the shape of the board. 0 means no limit.

## `maxPerCompany` (type: `integer`):

Stops a single large board filling the whole result. Leave at 0 and a market-wide run with a ceiling spreads it over at least 40 boards on its own: without that, the bare default returned 1,000 rows from one or two companies, because one Greenhouse board arrives in a single page and takes the whole ceiling. Counted per board, not per employer, so a company that runs two boards can return twice this. It is never applied when you name companies yourself, or when "maxItems" is 0.

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

How many Workday boards to read at once. Detail requests within a board are separately limited. The default is sensible; raise it only when reading many boards at once.

## `atsPlatforms` (type: `array`):

Fixed to Workday for this Actor.

## Actor input object example

```json
{
  "companies": [
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "stryker"
  ],
  "useBuiltInCorpus": false,
  "corpusSample": 25,
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescriptions": true,
  "onlyNewOrChanged": false,
  "stateKey": "",
  "maxItems": 1000,
  "maxPerCompany": 0,
  "concurrency": 10,
  "atsPlatforms": [
    "workday"
  ]
}
```

# Actor output Schema

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

One item per posting, in a single schema across all supported applicant tracking systems.

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

Counts of boards read, not found, rate-limited, forbidden and read with nothing open, postings scanned, the filters applied, and any diagnostics explaining an empty result.

## `removedSinceLastRun` (type: `string`):

Written on every incremental run, empty array included: the ids your previous run returned that this one did not. Usually the employer took the posting down, but a posting that ages past postedWithinDays or is retitled out of a keyword match leaves the same way. Boards this run could not read are excluded, so a vendor outage does not empty your feed on paper.

# 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://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "stryker"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("firstpartydata/workday-jobs-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 = { "companies": [
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "stryker",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("firstpartydata/workday-jobs-scraper").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://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "stryker"
  ]
}' |
apify call firstpartydata/workday-jobs-scraper --silent --output-dataset

```

## MCP server setup

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

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/scgtMfeMmqSVDiRE5/builds/lv02iJa5OAFQfZf3h/openapi.json
