Workday is where large enterprises actually post. NVIDIA has 2,000 open roles
there, Salesforce 1,477, Adobe 832. It answers an anonymous POST with no key.
It also has two behaviours that are not in any documentation you can read
without an account, and both of them fail silently. One of them costs you 98%
of the board without raising anything.
The number that changes after page one
Ask for the first twenty postings and the response carries a total:
POST /wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs
{"appliedFacets":{}, "limit":20, "offset":0, "searchText":""}
20 jobPostings, total: 2000
Enter fullscreen mode Exit fullscreen mode
Ask for the next twenty and the count is gone:
offset 20 -> 20 jobPostings, total: 0
offset 40 -> 20 jobPostings, total: 0
Enter fullscreen mode Exit fullscreen mode
Not null, not absent. Zero. The postings keep coming; only the count collapses.
Measured on four enterprise tenants:
| tenant | total at offset 0 | at offset 20 | at offset 40 |
|---|---|---|---|
| NVIDIA | 2000 | 0 | 0 |
| Salesforce | 1477 | 0 | 0 |
| Adobe | 832 | 0 | 0 |
| Sony | 94 | 0 | 0 |
Same shape every time, so this is Workday and not one tenant's configuration.
Why that costs you 98% of the board
Here is the loop everyone writes, and it is not a bad loop:
offset, out = 0, []
while True:
page = fetch(offset)
posts = page["jobPostings"]
if not posts:
break
out += posts
offset += len(posts)
if offset >= page["total"]: # looks obviously right
break
Enter fullscreen mode Exit fullscreen mode
On page two page["total"] is 0, and 20 >= 0 is true. The loop exits,
reports no error, and hands back what it has.
I ran both versions against NVIDIA:
declared total on page one 2000
the obvious loop collected 40 2%
keeping the first total instead 2000 100%
Enter fullscreen mode Exit fullscreen mode
Forty postings out of two thousand, and nothing anywhere says so. No exception,
no warning, no partial-result flag. Just a job board that looks very quiet.
The fix is one line moved:
offset, out, total = 0, [], None
while True:
page = fetch(offset)
posts = page["jobPostings"]
if not posts:
break
out += posts
offset += len(posts)
if total is None: # the first answer is the only honest one
total = page.get("total") or 0
if total and offset >= total:
break
Enter fullscreen mode Exit fullscreen mode
Two changes that matter. Keep the first total. And guard on total being
truthy, so a board that never reports a usable count still runs to the empty
page instead of stopping immediately.
The page size is capped at 20, and 21 is a 400
The other undocumented one. limit is not a suggestion:
limit=20 -> HTTP 200
limit=21 -> HTTP 400
limit=25 -> HTTP 400
limit=50 -> HTTP 400
Enter fullscreen mode Exit fullscreen mode
Exactly twenty. And the failure arrives as a 400, which reads as "your request
body is malformed" rather than "that number is too big", so the natural
reaction is to go looking at your JSON.
Two thousand postings at twenty a page is a hundred round trips. That is the
price, and there is no parameter that lowers it.
You cannot build the URL from a company name
A Workday address carries three separate unknowns:
https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
^^^^^^ ^^^ ^^^^^^^^^^^^^^^^^^^^^^^
host data centre career site name
Enter fullscreen mode Exit fullscreen mode
The host label is usually the company, but the data centre is wd1 through
wd12 with no pattern, and the site name is whatever whoever set it up typed.
External_Career_Site, external_experienced, SonyGlobalCareers,
NVIDIAExternalCareerSite. Guessing across that space is mostly 404s.
The API path adds a fourth part that the public URL does not show:
public https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
api https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs
^^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode
On all four tenants I checked, that middle segment is the host label again.
Build it with a literal None in there and you get a 422, not a 404, which
at least fails in a way that does not look like a wrong company name.
So take the careers URL from the customer. It is the one thing they always
have, and it is the only reliable source for all three parts.
What the postings look like
Sparse, and worth knowing before you design a schema around it:
{
"title": "Distinguished Software Architect - Deep Learning",
"externalPath": "/job/US-CA-Santa-Clara/...",
"locationsText": "US, CA, Santa Clara",
"postedOn": "Posted Today",
"bulletFields": ["JR2001234"]
}
Enter fullscreen mode Exit fullscreen mode
Five keys. No department, no employment type, no salary, no description, and
postedOn is a relative phrase rather than a date.
Counted across 80 NVIDIA postings:
Posted 30+ Days Ago 20
Posted 5 Days Ago 13
Posted 13 Days Ago 13
Posted 2 Days Ago 12
Posted Today 5
Posted Yesterday 3
Enter fullscreen mode Exit fullscreen mode
Do not parse that into a timestamp. A quarter of them are "30+ Days Ago",
which has no date in it at all, and a timestamp you invented is worse than a
string you kept: the string is obviously approximate and the timestamp is not.
Give it its own field and let the consumer decide.
Measured on 2026-08-02. Four tenants carried a usable count: NVIDIA,
Salesforce, Adobe and Sony. Dell reported 0 at offset 0 as well, which is the
third trap below rather than a fifth data point. I
maintain a set of ATS scrapers that normalise
nine of these systems into one row shape, which is how I ran into all of it.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.