The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Dol Whd listing page.
Concluded US Department of Labor Wage & Hour Division compliance actions since
FY2005 — wage theft, back wages, minimum-wage and overtime findings, child labour,
H-1B and H-2A violations. osha already covers the OSHA slice of DOL enforcement;
this is the sibling nobody could query.
Part of Pipeworx — an MCP gateway connecting AI agents to 1683+ live data sources.
Promoted out of _incubator on 2026-09-01 (fleet #1052) once Bruce supplied
the key. Built originally against a 10-row catalog preview under fleet #651,
because until that key existed every row request answered 401.
| Tool | Answers |
|---|---|
whd_search | "Has employer been cited for wage violations?" — filter by employer, NAICS, state, date |
whd_employer | Full case history and aggregate totals for one employer |
whd_top_backwages | "Who owes the most back wages in industry?" |
whd_coverage | What the dataset covers, its publication date and findings-date range |
Platform key (PLATFORM_DOL_KEY, set on gateway and registry-api) with BYO
override via _apiKey. A free key from https://dataportal.dol.gov covers every
DOL dataset and works on issue — there is no approval step. _apiKey is not
in any tool's required list, because the gateway injects the platform key.
This is not the data.gov umbrella key ([[reference-datagov-key-universal]] does not apply); the DOL Open Data Portal issues its own.
The catalog is open and the rows are not, which is a trap. GET /v4/datasets
and GET /v4/datasets/10362 both answer 200 with no key, so the source looks
open until you ask for data — at which point a keyless row request returns 401
"The API key is either incorrect or missing from your query."
Worse, auth differs by endpoint. /v4/datasets* accepts an X-API-KEY:
request header; /v4/get/... rejects it with 401 and needs the key as a
query parameter. Standardise on the header because the catalog call worked
and every data call 401s, which reads as a bad key rather than a wrong place to
put it. The pack always uses the query parameter.
The upstream throttles per SOURCE IP, not per key. Roughly 20 calls in a few
minutes returns HTTP 429 with x-amzn-errortype: ForbiddenException and no
Retry-After, for about ten minutes. Measured: a bogus key from a throttled IP
also gets 429 while omitting the key entirely still gets 401, which is what pins
it to the IP. So this is a shared-egress hazard for us rather than a per-key
quota. The pack backs off twice and then returns
{ found: false, reason: "upstream_rate_limited" } rather than throwing, so a
throttle stays distinguishable from an employer with no cases.
data.dol.gov is a React SPA whose bundle /static/js/main.1788ccf8.js carries
DOL's own API documentation. Parameters: limit, offset, sort (asc/desc),
sort_by (field name), fields, filter_object, X-API-KEY — all as query
parameters. Operators, quoted from that documentation: eq, neq, gt, lt,
in, not_in, like. Nothing outside that list is assumed to work.
filter_object is a JSON string of nested and / or groups over
{field, operator, value}, per DOL's worked example:
{"data":[]}. res.ok is true for 204, so parsing without checking the status
throws on empty input — turning "this employer has no cases", the commonest
negative result there is, into what looks like a parse bug of ours.filter_object keywords must be lowercase — field, operator, value,
and, or. Anything else answers 500 with a generic "check for typos"
message that names no field. A wrong dataset name gives the same 500, which
is why whd/whisard (the name in the original brief) read as a DOL outage
rather than a typo. The dataset is whd/enforcement.like needs % to behave as a substring match. DOL documents like as
the substring operator but not its syntax. Measured 2026-09-01: %Walmart%
and bare Walmart both return rows, but the wildcard form is a strict
superset — it also matches "Subway Georgetown Walmart", which the bare form
does not. Bare like is an anchored match. Every employer search here uses
%term%.like, and every
hit carries matched_field saying which one landed.bw_atp_amt is the TOTAL, and the FLSA columns are a trap. The
statute-level columns (flsa_bw_atp_amt, sca_bw_atp_amt, cwhssa_bw_amt,
dbra_…, mspa_…, h1b_…, fmla_… …) are mutually exclusive and sum to
bw_atp_amt — verified on cases with two statutes (Corrections Corp of
America: 7,118,609 SCA + 953,252 CWHSSA = 8,071,861; Hewlett-Packard:
4,831,719 SCA + 401,211 FLSA = 5,232,930, both exact). But flsa_ot_…,
flsa_mw_…, flsa_15a3_… and the flsa_smw* set are a breakdown of
flsa_bw_atp_amt, not siblings of it, so adding all the *_bw_*_amt columns
double-counts. The pack returns back_wages_agreed_usd (the total),
back_wages_by_statute, and flsa_breakdown separately, plus a
back_wages_reconciles flag so a change in that structure surfaces in a
response rather than silently inflating a number.findings_end_date sort to the top of a sort=desc, so the obvious query
returns null; and DOL's own data carries typo'd future dates — the newest
non-null row is stamped 3021-05-01. whd_coverage bounds the window at
both ends and says so in window_note.www.dol.gov HTML pages 403 a plain UA behind Akamai and need the full
browser header set (BROWSER_HEADERS in
workers/data-pipeline/src/datasets/dod-contracts.ts). That is a
header-fingerprint block, not a CF-egress block — do not build a proxy for
it. apiprod.dol.gov is a separate host and shows no such gating.bw_atp_amt the total of the act columns, or a separate bucket?
It is the total of the statute-level columns — settled on real rows, see
the trap above. This was the pack's one open question while it had no key.apiprod.dol.gov does not gate CF egress. (The
www.dol.gov Akamai block noted above is a different host and still applies
to HTML scraping.)PLATFORM_DOL_KEY is set as a wrangler secret on both pipeworx-gateway
and pipeworx-registry-api.The same v4 API serves 42 datasets across nine agencies (MSHA 15, OSHA 11,
ILAB 7, ETA 4, and one each for EBSA, TRNG, VETS, WB and WHD). The full list,
with the call shape and every trap above, is recorded in
docs/dol-open-data-catalog.md.
Survey only — nothing there is built, and post-saturation nothing should be
built from it speculatively; it exists so a demand signal has somewhere to land.
Add to your MCP client (Claude Desktop, Cursor, Windsurf, etc.):
tools/list at https://gateway.pipeworx.io/dol-whd/mcp returns the tools in the table
above plus the shared Pipeworx meta-tools — ask_pipeworx,
discover_tools, search_within, remember/recall and the rest of the
gateway-wide set. So the tool count you see is larger than this table: a
single-pack endpoint currently lists roughly 30 shared tools alongside the
pack's own. The connection's initialize response states its exact scope, and
is the authoritative answer for a given day.
This is deliberate, not multiplexing by accident. The meta-tools are what let a
scoped connection answer a question this pack does not cover — via
ask_pipeworx, which routes across the whole catalog — without you adding a
second MCP server. There is currently no way to mount a pack endpoint without
them; if the extra schemas cost you more context than the routing is worth,
connect to the full gateway once rather than to several pack endpoints.
Or connect to the full Pipeworx gateway to get every pack's tools listed directly, instead of just this one's:
Both URLs reach the same gateway and the same 1683+ data sources. The
only difference is which pack's tools are listed directly; ask_pipeworx
reaches all of them from either one.
No account needed for the first calls. Inspect any tool: GET https://gateway.pipeworx.io/v1/tools/whd_search. Find one: POST https://gateway.pipeworx.io/v1/tools/search_packs with {"query":"..."}.
This package also runs as a local stdio MCP server — no Pipeworx account, no gateway round-trip:
Or run it directly to confirm it starts:
It speaks MCP over stdin/stdout and answers initialize/tools/list/tools/call
for only this pack's tools — none of the shared meta-tools the gateway
connection above adds. Same source, same tools, no ask_pipeworx routing.
Instead of calling tools directly, you can ask questions in plain English — this works on the pack endpoint above as well as on the full gateway:
The gateway picks the right tool and fills the arguments automatically.
MIT