| name | grainger-search |
| description | Turn a vague product need into a Grainger category, its facet rail and a filtered SKU list, using the scripted tier only. Use this to establish what to search and how to narrow it, before spending any browser session. |
| allowed-tools | Read, Write, Bash(python3 *), Bash(mkdir *) |
Grainger search (scripted tier)
Cheap, stateless, no browser. Establishes the category, the available facets with
counts, and the SKUs that survive filtering. It cannot get you a price — that is
grainger-listing-check.
Run grainger-fetch-route's probe first if you have not already.
1. Find the category
python3 scripts/grainger_fetch.py suggest "torque wrench"
categorySuggestionResults[].url gives real category paths, and .key gives the
tilde-delimited taxonomy (~Tools~Hand Tools~Wrenches~Torque Wrenches). Use this
rather than guessing a URL — Grainger's category paths are not derivable from
product wording.
For titles against item numbers:
python3 scripts/grainger_fetch.py products "torque wrench"
This is the only scripted route that returns product titles. No prices.
2. Fetch the search and parse it
python3 scripts/grainger_fetch.py search "nitrile gloves" --save /tmp/g.html
python3 scripts/grainger_parse.py /tmp/g.html
Read the resolved url before reading the results. /search 302s to a category
page, sometimes with facets already applied — "nitrile gloves" arrives with
Material pre-selected to three values, while "safety glasses" arrives clean. You
cannot assume either way. If you report that narrowing as your own
filtering, you are describing work Grainger did. applied_filters.chips names
exactly what was applied, with counts.
Check session.signed_in in the parse output too: a signed-out fetch returns a
different facet rail and looks perfectly valid.
3. Narrow
python3 scripts/grainger_parse.py /tmp/g.html --facets --max-values 40
Each value carries count, available, selected and query.
available: true → use query verbatim as the next URL's query string. It
already encodes the applied facets, so it composes.
selected: true → query is null; remove_query undoes it.
available: false → the value exists in the taxonomy but matches nothing here,
and no query is offered. Hand-building one returns zero results. Widen instead.
applied_filters.clear_url drops everything and starts over.
4. Report
Return SKUs with what you actually have:
| Item | Title | Pack qty | Facets matched | Price |
|---|
| 793JR4 | from products typeahead | shipPackQuantity | from the chips | not retrieved (browser tier) |
Quote the item number — it is what Grainger's search, support and branches all
use. Quote pack quantity alongside it: Grainger prices per pack, and a cheap
box of gloves is often 1000 of them.
Traps
result_sku_count is one page of results, not the category total.
category_sku_count is every SKU in the category ignoring filters. They are
different numbers and mean different things.
inactive_product_count is real — check an item is not discontinued before
recommending it.
- An empty
productMap in the raw state is by design, not an empty result.
The SKUs live in hybrisProductSkuMap.
- Never state a price from this tier. There isn't one.