# Workday Jobs Scraper - No API Key (`renzomacar/workday-jobs-scraper`) Actor

Scrape enterprise Workday career sites by URL. Handles the per-tenant host pattern, the 20-per-page cap and the pagination wrap bug. No API key, no login, no proxy.

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

## Pricing

from $4.00 / 1,000 job scrapeds

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?

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 — Enterprise Career Sites

Scrape **Workday career sites** by URL. No API key, no login, no cookies, no proxy, no browser. Workday runs the careers page of a very large share of the Fortune 500 — NVIDIA, Salesforce, Adobe, Pfizer and thousands more — and every one of those sites is backed by a JSON search API that this Actor talks to directly.

Workday is the hardest of the four ATSs in this fleet, because there is no single host and no single API path. This Actor exists because we did the work of figuring out the pattern and the three things that break naive implementations.

***

### Why Workday breaks scrapers, and what we do about it

**1. There is no single host.** Every customer gets their own. Two families exist in the wild and both are handled:

```
https://<tenant>.wd5.myworkdayjobs.com/<Site>              per-tenant host
https://wd1.myworkdaysite.com/recruiting/<tenant>/<Site>   shared host
```

The datacentre number varies per customer — we have verified `wd1`, `wd5` and `wd12` on live tenants — so it cannot be hardcoded. The underlying API is always:

```
POST {origin}/wday/cxs/{tenant}/{site}/jobs        paged search
GET  {origin}/wday/cxs/{tenant}/{site}{path}       one posting
```

Just paste the career-site URL from your browser. Locale segments (`/en-US/`) and deep job URLs are parsed and reduced to the right tenant and site automatically.

**2. The page size is capped at 20, server-side.** Requesting `limit: 100` returns **HTTP 400**, not a truncated page. Verified against NVIDIA. The Actor pages at exactly 20.

**3. Pagination does not end — it wraps.** This is the one that silently corrupts datasets. Salesforce reports 1,542 total jobs; `offset: 1540` correctly returns the last 2. But `offset: 2000` and `offset: 5000` both return **20 rows again** rather than an empty page. A loop that stops on "empty page" never stops, and quietly writes thousands of duplicates.

The Actor guards this three ways: it tracks every `externalPath` already seen and stops when a full page adds nothing new; it stops on any short page; and it respects the reported total. It also knows that `total` is only meaningful on the first page — on later pages NVIDIA returns `total: 0`, which would abort a naive loop immediately.

**4. A wrong site name returns 422, not 404.** `cushwake`, `ibm` and `cbre` all answered HTTP 422 on the site paths we tried, meaning the tenant host resolves but the site id is wrong. That gets its own reason code (`wrong_tenant_or_site`) with a message telling you which half to fix, instead of a generic failure.

***

### What you get, with the fill rate actually measured

Measured across **4 real tenants — 238 live postings** (NVIDIA, Salesforce, Adobe, Pfizer), with descriptions enabled:

| Field | Fill rate | Notes |
|---|---|---|
| `title` | 100% | |
| `companyName` | 100% | Workday's `hiringOrganization`, often the legal entity, e.g. "2100 NVIDIA USA" |
| `jobId` | 100% | |
| `requisitionId` | **100%** | The real req id, e.g. `JR2019870` — the most useful join key Workday gives you |
| `location` | 100% | |
| `locations[]` | 100% | Primary plus `additionalLocations` |
| `country` | **100%** | Full country name from the posting |
| `employmentType` | **100%** | From `timeType` |
| `postedAt` | 100% | From the detail record's `startDate` |
| `postedOnText` | 100% | Workday's own phrasing: "Posted Today", "Posted 30+ Days Ago" |
| `postedAtEstimated` | ~100% | ISO estimate derived from that phrasing, clearly named as an estimate |
| `jobUrl` | 100% | |
| `applyUrl` | 100% | |
| `descriptionText` | 100% | Requires `fetchDetails` (on by default) |
| `canApply` | 100% | Whether the req is still accepting applications |
| `tenant` / `careerSite` | 100% | So you can trace every row back to its source site |
| `isRemote` | 100% | See below |
| `workplaceType` | **34.9%** | Only tenants that populate `remoteType` |
| `salaryMin` / `salaryMax` / `salaryText` | **40.3%** | Parsed from the description |

#### Three fields this Actor deliberately does **not** return

Workday's public career-site API has no equivalent, on any tenant we measured:

- **`department`** and **`team`** — Workday organises by job family internally, and none of it is exposed publicly.
- **`updatedAt`** — there is no modification timestamp. Use `postedAt` and `postedOnText`.

They are absent from the schema rather than shipped as columns that are null on 100% of rows.

#### Dates: what is real and what is an estimate

Workday's list endpoint gives you only a relative phrase — "Posted Today", "Posted 30+ Days Ago". The detail record gives a real `startDate`. So:

- `postedAt` — the real ISO date, 100% fill **when `fetchDetails` is on**
- `postedOnText` — Workday's phrase, verbatim, always available
- `postedAtEstimated` — an ISO date derived from the phrase, available even with `fetchDetails` off

Three fields because they have three different levels of trust, and collapsing them into one would hide that from you.

#### Remote: 34.9% from the ATS, honest nulls for the rest

Some tenants populate `remoteType` ("Office - Flexible", "Remote") and some do not. Salesforce does; NVIDIA and Adobe do not. Where it exists we use it (`remoteSource: "ats_field"`, 25% of rows). Where it does not we read the location text (`inferred_location`, 10%). Everything else comes back `unknown` with `isRemote: false` rather than a guess. Measured: **12.2% flagged remote** across the four tenants.

***

### Input

```json
{
  "companies": [
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "https://salesforce.wd12.myworkdayjobs.com/External_Career_Site"
  ],
  "searchText": "software engineer",
  "maxItems": 200,
  "fetchDetails": true
}
```

#### Accepted URL formats

```
https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
https://adobe.wd5.myworkdayjobs.com/en-US/external_experienced/job/San-Jose/x_JR1234
https://wd1.myworkdaysite.com/recruiting/<tenant>/<Site>
https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs
```

You can also pass an explicit object: `{ "tenant": "nvidia", "site": "NVIDIAExternalCareerSite", "host": "https://nvidia.wd5.myworkdayjobs.com" }`.

#### Input aliases

- companies: `companies`, `company`, `careerSiteUrls`, `urls`, `startUrls`, `boards`
- limit: `maxItems`, `maxResults`, `limit`, `maxJobs`
- search: `searchText`, `query`, `search`
- descriptions: `fetchDetails`, `includeDescription`, `fullDescription`

#### Use `searchText`, it is much cheaper

`searchText` is passed straight into Workday's own search. Searching "software engineer" on a 2,000-req tenant costs a handful of requests; downloading all 2,000 and filtering afterwards costs a hundred pages plus a detail request per job. Always filter upstream when you can.

#### Filters

| Option | Default | What it does |
|---|---|---|
| `maxItems` | 200 | Hard ceiling on rows written. **This is your spend cap.** |
| `maxJobsPerCompany` | — | Balances a run across several tenants |
| `searchText` | — | Workday-side search, cheapest possible filter |
| `fetchDetails` | true | Off = one request per 20 jobs and no descriptions; on = one extra request per job |
| `titleKeywords` / `locationKeywords` | — | Client-side substring filters |
| `remoteOnly` | false | Only `isRemote: true` |
| `concurrency` | 4 | Career sites in parallel |
| `detailConcurrency` | 6 | Detail requests in parallel; lower it if a tenant starts returning 429 |

***

### Output sample

```json
{
  "source": "workday",
  "companyToken": "nvidia",
  "companyName": "2100 NVIDIA USA",
  "tenant": "nvidia",
  "careerSite": "NVIDIAExternalCareerSite",
  "jobId": "e5ad6f3f1847101249dbeef5317c0000",
  "requisitionId": "JR2019870",
  "title": "Senior Systems Software Engineer, Data Center Platform Enablement",
  "employmentType": "FULL_TIME",
  "location": "US, CA, Santa Clara",
  "locations": ["US, CA, Santa Clara"],
  "country": "United States of America",
  "isRemote": false,
  "workplaceType": null,
  "remoteSource": "unknown",
  "postedAt": "2026-08-21T00:00:00.000Z",
  "postedOnText": "Posted Today",
  "postedAtEstimated": "2026-08-22T04:15:11.002Z",
  "canApply": true,
  "jobUrl": "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite/job/US-CA-Santa-Clara/...",
  "applyUrl": "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite/job/US-CA-Santa-Clara/.../apply",
  "descriptionText": "NVIDIA's invention of the GPU in 1999 ...",
  "scrapedAt": "2026-08-22T04:15:11.002Z"
}
```

Four saved dataset views ship with the Actor: **Job overview**, **Remote & locations**, **Tenants & requisitions** and **Apply links**.

***

### How errors are handled

**Errors never enter your dataset.** They go to the `FAILURES` record in the run's key-value store, with reason codes that tell you what to fix:

- `career_site_not_found` — HTTP 404, no such site.
- `wrong_tenant_or_site` — HTTP 422. The tenant host answered but has no site by that name. The message names both halves so you know which one is wrong.
- `unresolvable_input` — not a recognisable Workday URL.
- `no_open_roles` — reachable, zero postings.
- `detail_fetch_failed` — the listing came through but one detail request did not. **The job is still delivered**, with the fields the listing carried; you simply get no description for that row.
- `pagination_stopped` — a page failed mid-crawl. Everything collected before it is still delivered.
- `budget_exhausted` — `maxItems` was reached before this site was visited. Nothing is ever skipped in silence.

**If nothing was delivered, the run ends FAILED** with the reason breakdown, so a dead pipeline announces itself.

***

### Pricing and the free tier

Pay per event:

- **Actor start** — one small charge per gigabyte of memory
- **Job scraped** — charged **after** the row is written

The per-job rate is slightly above the other ATS Actors in this fleet, because Workday genuinely costs more to scrape: one request per 20 jobs for the listing, plus one request per job for the description. You are charged only for rows that land in the dataset, batched, after the push succeeds.

**The free tier returns real data.** No API key, no proxy, no credentials. A free account running `{"companies": ["https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"], "maxItems": 10}` gets ten real jobs.

Default `maxItems` is deliberately 200 rather than 1000, because Workday's request pattern is the expensive one and a default should not surprise you.

***

### Speed and tuning

Measured: **238 postings across 4 tenants in 41 seconds** with descriptions on. Without descriptions (`fetchDetails: false`) the same listing work takes a few seconds, because it becomes one request per 20 jobs.

Two knobs matter:

- **`fetchDetails: false`** for a fast, shallow crawl when you only need titles, locations, requisition ids and URLs.
- **`detailConcurrency`** to trade speed against politeness. Default 6. Raise it on a tenant that tolerates it; lower it if you see 429s in the log.

Requests are gzip-compressed, retried with exponential backoff on 429/5xx, and never retried on 404 or 422.

***

### Common questions

**Do I need a Workday API key?** No. `/wday/cxs/...` is the same endpoint the public careers page calls from your browser. There is no authentication on it.

**How do I find the tenant and site?** You do not need to — paste the career-site URL. If you want to know: in `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`, the tenant is `nvidia` and the site is `NVIDIAExternalCareerSite`.

**I get HTTP 422 for a company I know uses Workday.** The site id is wrong. Companies often run several (`External`, `External_Career_Site`, `<Name>Careers`). Open their careers page in a browser and copy the URL exactly, including capitalisation — site ids are case-sensitive.

**Why is `companyName` a legal entity like "IL00 Mellanox Technologies, Ltd."?** That is the hiring organisation on the requisition, which is what Workday returns. Use `tenant` for a stable company key.

**Why does NVIDIA report `total: 2000` when it has more?** Workday caps the reported total on some tenants. Use `searchText` to slice the result set rather than trying to page past the cap.

**Can I filter by location or department on Workday's side?** Workday supports faceted filtering, but facet ids are tenant-specific and undocumented. This Actor uses the reliable universal lever, `searchText`, and does the rest client-side.

**How fresh is the data?** Live on every run.

***

### Related Actors in this fleet

- **Greenhouse Jobs Scraper** — startups and mid-market, one request per board
- **Ashby Jobs Scraper** — structured salary numbers on ~72% of postings
- **Lever Jobs Scraper** — team, commitment, workplace type and country at ~99–100%
- **Startup Jobs Aggregator** — Greenhouse, Ashby and Lever in one normalised run

### Legal

This Actor reads the public, unauthenticated JSON endpoint that backs each Workday career site's own search page. It sends no credentials, solves no challenges and bypasses no access control. You are responsible for how you use the data, including any applicable data-protection rules.

# Actor input Schema

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

Full career-site URLs, one per line. Both host families work: https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite and https://wd1.myworkdaysite.com/recruiting/<tenant>/<Site>. Aliases: company, careerSiteUrls, urls, startUrls.

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

Hard ceiling on rows written. Workday pages 20 at a time and one extra request per job is needed for the description, so keep this deliberate. Aliases: maxResults, limit, maxJobs.

## `maxJobsPerCompany` (type: `integer`):

Max jobs per career site.

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

Passed straight to Workday's own search box. Far cheaper than downloading everything and filtering after.

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

Keep only postings whose title contains one of these (case-insensitive). Leave empty for all. Aliases accepted in raw input: keywords, searchTitle.

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

Keep only postings whose location or office list contains one of these. Example: "New York", "Remote", "London".

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

Keep only postings flagged remote. Check the remoteSource field to see whether the flag came from the ATS or was inferred.

## `postedAfter` (type: `string`):

ISO date, e.g. 2026-07-01. Drops anything published before it.

## `fetchDetails` (type: `boolean`):

Workday's list endpoint carries only title, location and posted-on. Descriptions, requisition ids and countries need one extra request per job. Turn this off for a fast, shallow crawl.

## `includeHtmlDescription` (type: `boolean`):

Include raw HTML description.

## `dedupe` (type: `boolean`):

Remove duplicate postings.

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

How many requests to run at the same time. Lower it if you hit rate limits.

## `detailConcurrency` (type: `integer`):

Raise for speed, lower if a tenant starts returning 429.

## Actor input object example

```json
{
  "companies": [
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "https://salesforce.wd12.myworkdayjobs.com/External_Career_Site"
  ],
  "maxItems": 200,
  "maxJobsPerCompany": 50000,
  "titleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "fetchDetails": true,
  "includeHtmlDescription": false,
  "dedupe": true,
  "concurrency": 4,
  "detailConcurrency": 6
}
```

# Actor output Schema

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

One row per open role from the Workday career site, deduplicated across pages.

# 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",
        "https://salesforce.wd12.myworkdayjobs.com/External_Career_Site"
    ],
    "titleKeywords": [],
    "locationKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/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",
        "https://salesforce.wd12.myworkdayjobs.com/External_Career_Site",
    ],
    "titleKeywords": [],
    "locationKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/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",
    "https://salesforce.wd12.myworkdayjobs.com/External_Career_Site"
  ],
  "titleKeywords": [],
  "locationKeywords": []
}' |
apify call renzomacar/workday-jobs-scraper --silent --output-dataset

```

## MCP server setup

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