DEV Community

Daniel Meshulam
Daniel Meshulam

Posted on Fully Autonomous

Workday Jobs Scraper: every posting from any Workday careers site, by company name

Large employers post their jobs on Workday: NVIDIA, Salesforce, Walmart, Target, Wells Fargo, Pfizer, Boeing. Every one of those *.myworkdayjobs.com career sites looks different in the browser and is the same API underneath:

import requests

r = requests.post(
    "https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs",
    json={"appliedFacets": {}, "limit": 20, "offset": 0, "searchText": ""},
)
data = r.json()
data["total"], [j["title"] for j in data["jobPostings"][:3]]
Enter fullscreen mode Exit fullscreen mode

No key, no login, no cookies: it is the endpoint the careers page itself calls. A scraper is a loop over offset. Three things turn that loop into a wrong answer, and none of them raises an error.

1. You need the URL, and a company name does not give it to you

The address carries three facts a name cannot derive: the host label (nvidia), the data centre (wd5; companies sit anywhere from wd1 to wd504 and move between them) and the career site's own name (NVIDIAExternalCareerSite). Guessing across all three mostly produces 404s and 422s.

What works is a map built from boards that have actually answered with real postings. The one this post's scraper ships holds 3,056 employers, so NVIDIA, Wells Fargo or PwC resolve without anyone hunting for a URL. A name it does not know is better refused than guessed.

2. limit is 20, and total is a ceiling

Ask for 50 a page and the answer is an error. Page past 2,000 and big boards just serve page one again, so a loop that stops on an empty page never stops. On those boards total says 2,000 whatever the real count is. I wrote up the ceiling and the way past it (the facet counts in the same answer) here.

3. Filtering after you read misses most of the matches

This is the one that costs the most, because the output looks fine.

Most scrapers read the newest N postings (often 1,000), then keep the ones whose title matches. On a small board that is everything. On a big one it is a sample. Measured on 2026-10-18 with "software engineer":

Board Matches in the newest 1,000 Matches on the board
NVIDIA 196 442
Salesforce, "account executive" 250 448
Mastercard 191 225

The fix is already in the request: searchText. Workday searches title and description, so the answer is a superset, and you keep the rows whose title holds the words:

def search(url, text):
    out, offset = [], 0
    while True:
        page = requests.post(url, json={"appliedFacets": {}, "limit": 20,
                                        "offset": offset, "searchText": text}).json()
        posts = page.get("jobPostings") or []
        hits = [p for p in posts if text.lower() in p["title"].lower()]
        out += hits
        # Workday ranks title matches first: once a page past the second has
        # none, the rest are postings that only mention the words.
        if not posts or (offset >= 40 and not hits):
            return out
        offset += 20
Enter fullscreen mode Exit fullscreen mode

On eight title and board pairs that kept every one of 3,264 title matches and read 239 of the 502 pages the searches declared. The same goes for a place: searchText="Israel" at NVIDIA finds 233 jobs, where the newest 1,000 held 61.

Or skip all of it

I maintain Workday Jobs Scraper on Apify, which does the three things above: a company name for 3,056 employers, the 2,000 ceiling, and titles and places sent to Workday's own search. A company that is not on Workday is looked up where it does post (Oracle, Eightfold, Phenom, SAP and more), and filtered-out rows are not charged.

{
  "companies": ["NVIDIA", "Wells Fargo"],
  "titleKeywords": ["software engineer"],
  "locationKeywords": ["Israel"]
}
Enter fullscreen mode Exit fullscreen mode

It is also callable from an AI agent through the Apify MCP server, so "find software engineer openings at NVIDIA in Israel" is one tool call.

Top comments (0)