Build1 publisher3 min readPublished
A 200 OK that means zero: Workday's job API empties out above limit 20
A developer normalizing seven applicant tracking systems hit the failure mode that never pages anyone: a well-formed success response containing nothing at all.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- The author built a job-board normalizer across seven applicant tracking systems: Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and Recruitee.
- Every interesting bug in the project came from an API that answered 200 OK while telling the client something false.
- Workday's job search is a POST, not a GET: POST https://${host}/wday/cxs/${tenant}/${site}/jobs with a JSON body of appliedFacets, limit, offset and searchText.
- Setting limit to 100 returns 200 OK and a well-formed response body in which jobPostings is an empty array; no error, no warning, no 400.
- Workday's real limit ceiling is 20; higher values return an empty array.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer who built a job-board normalizer across seven applicant tracking systems - Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and Recruitee - reports that the request which breaks Workday's public search does not fail loudly [1]. Raise `limit` from 20 to 100 on the POST search endpoint and you get 200 OK, a well-formed body, and an empty `jobPostings` array, with no error, no warning and no 400 [3][4].
That is the expensive shape of wrong. According to the writeup, a naive implementation does not crash; it concludes the employer has no open roles and moves on, so a run across hundreds of tenants emits zero rows for every Workday employer while looking perfectly healthy [6]. The author says every interesting bug in the project came from an API answering 200 OK while asserting something false [2], and that this one cost an afternoon before he thought to compare `limit: 20` against `limit: 100` on the same tenant [7].
The real ceiling is 20 [5]. Paginating at 20 instead of 100 is five times the round trips, which is exactly why someone raises the number in the first place [19]. There is a second wall behind it: on large tenants, pagination stops returning new results once `offset` passes 10,000, and nothing in the response distinguishes a wall from the end of the list [8]. At 20 postings per page that is 500 requests before the data quietly stops [20]. Without an explicit cap, the loop either terminates on an empty page and under-reports or spins; the author's fix is to cap at 10,000 and record that the cap was hit [9]. His general rule is worth stealing: an ATS saying "no more results" and an ATS refusing to give you more results are identical over HTTP, so you have to decide which one you are looking at and log the decision [10].
Discovery has the same honesty problem. Greenhouse, Lever and Ashby take a single slug you can guess from a company's domain, while Workday needs three values - host, tenant and site, as in `nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite` [11]. The `wd5` is whichever Workday cluster the customer landed on and the site name is free text the customer chose, with no registry and no lookup API to enumerate [12]. Six of the seven systems support auto-discovery from a domain; Workday does not, so the author has users paste the careers URL, parses it, and left Workday out of automatic discovery rather than ship a function that misses every Workday employer [13].
Absence lies elsewhere too. Ashby's public board endpoint returns `title`, `location`, `employmentType` and `descriptionHtml` with no compensation field at all - not null, absent - which reads like data that does not exist [14]. Add `includeCompensation=true` and every posting carries a compensation object with tier summaries, currency codes and min/max values [15]. On the board tested, 131 of 137 postings had compensation data the default response omits entirely [16], about 96% of the board [21].
The audit that caught the rest was internal: 30% of output rows had `companyName: null`, and the nulls were 100% of the rows from specific sources [17]. Ashby's response has two top-level keys, `jobs` and `apiVersion`; Lever returns a bare array with no envelope; neither carries the employer's display name at any level [18].
Two things to watch in your own pipeline. First, whether a per-source zero-row result raises an alert instead of being written as a value - that is the only defence against a silent 200. Second, whether your code comments about upstream fields were ever verified; the author's own comment claiming Ashby exposed `organizationName` on some boards was a guess, and wrong [22].