If you have ever written a crawler for a Fortune 500 career board hosted on Workday (*.myworkdayjobs.com), you have probably run into a baffling symptom: the board UI clearly says 2,000+ open roles, your script fetches Page 1 (20 roles) and Page 2 (20 roles) with HTTP 200 OK, and then your loop terminates cleanly at exactly 40 jobs.

No HTTP 429 rate limit. No 403 block. Just 98 percent of the company's job postings missing.

While building RevClamp TalentPulse (@revclamp/unified-ats-jobs-mcp - an independent B2B hiring intelligence tool by RevClamp, not affiliated with or endorsed by Workday, Greenhouse, Ashby, Lever, or SmartRecruiters), we traced this bug to an undocumented optimization inside Workday's Candidate Experience Service (CXS) JSON API.

1. Reproducing the Workday CXS Page-2 "total": 0 Quirk

Workday career boards fetch listings by sending a POST request to /wday/cxs/{tenant}/{site}/jobs with a JSON body specifying limit: 20 and offset.

Here is what the raw JSON response looks like on Page 1 (offset: 0) vs. Page 2 (offset: 20):

// Page 1 request: { "limit": 20, "offset": 0, "searchText": "" } { "total": 2048, "jobPostings": [ { "title": "Senior Systems Software Engineer", "externalPath": "/job/..." } // ... 20 items ] } // Page 2 request: { "limit": 20, "offset": 20, "searchText": "" } { "total": 0, "jobPostings": [ { "title": "Principal Distributed Storage Architect", "externalPath": "/job/..." } // ... 20 items ] } 

To avoid running an expensive COUNT(*) query on every page turn, Workday's backend only populates total when offset === 0. On every subsequent page (offset >= 20), Workday hardcodes "total": 0 even while returning a full page of 20 job postings.

If your pagination loop uses the standard offset check:

// Broken on Workday CXS after Page 2: const hasMore = offset + res.jobPostings.length < res.total; // On Page 2 (offset: 20): 20 + 20 < 0 evaluates to false! 

the loop always stops after Page 2 (40 items).

2. The Fix: Page-1 Total Latching + Stateless Cursor Persistence

Within a single in-memory run, the fix is straightforward: latch res.total on offset === 0 whenever it is a positive integer, and ignore res.total === 0 on later pages.

However, when exposing an ATS crawler as a stateless Model Context Protocol (MCP) tool for Claude, Cursor, or Windsurf, each page request is an independent tool call. If Call #2 starts at offset: 20 without knowing what total was on Page 1, how does it know the true board size?

We solve both problems by encoding the Page-1 latchedTotalCount directly inside a scope-hashed rtp1_ pagination cursor:

export function evaluateWorkdayPagination({ currentOffset, pageSize = 20, rawData, priorLatchedTotal = null, }) { const rawTotal = Number(rawData?.total); const latchedTotalCount = currentOffset === 0 && Number.isFinite(rawTotal) && rawTotal > 0 ? rawTotal : priorLatchedTotal; const postings = Array.isArray(rawData?.jobPostings) ? rawData.jobPostings : []; const nextOffset = currentOffset + postings.length; const hasMore = postings.length >= pageSize && (latchedTotalCount === null || nextOffset < latchedTotalCount); return { latchedTotalCount, nextOffset, hasMore, }; } 

By carrying latchedTotalCount forward inside the cursor token, even stateless multi-turn AI agent calls can paginate across thousands of Workday openings with zero headless browser overhead.