Salary Band Import Runbook
Use this runbook to turn published federal wage percentiles into a starting salary ladder for a discipline, using scripts/fetch-salary-bands.js.
This script writes nothing. It reads a tenant's headquarters location and its disciplines' occupation mappings, calls the CareerOneStop wage API, and prints the resulting ladder boundaries for a human to review and paste in — no database write, no endpoint call, no audit message. There is no "apply" flag.
Prerequisites
The tenant's HQ location must be set. In
/admin, find the tenant row and set HQ Location (e.g.Ann Arbor, MI). See the tenant-shape section ofCLAUDE.md. If it isn't set, pass--location="City, ST"on the command line instead.At least one discipline must have an occupation code mapped. On
/console/disciplines, open a discipline's Edit modal and pick its occupation from the grouped picker. Disciplines with no mapping are skipped and listed — never guessed at.CareerOneStop API credentials. Register for free at https://www.careeronestop.org/Developers/WebAPI/registration.aspx, then set:
CAREERONESTOP_USER_IDCAREERONESTOP_TOKEN
Do not commit these anywhere, and do not add them to a tenant file — they are process environment variables only.
Running it
node scripts/fetch-salary-bands.js --tenant=<id> [--location="Ann Arbor, MI"] [--dry-run]--tenant=<id>— required, matches the tenants file.--location=<..>— optional; overrides the tenant's storedhq_locationfor this run only.--dry-run— lists which disciplines would be queried and with what location, without making any network calls or requiring API credentials. Use this to sanity-check the setup before spending an API call.
Reading the output
For each mapped discipline, the script prints:
- The occupation title and code it queried.
- Which geography tier the numbers came from —
metro/local,state, ornational. A ladder built on national numbers for a company in a small town is a legitimate answer, but you need to know that's what you're looking at before you paste it in. - The four raw wage anchors (10th, 25th, 75th, 90th percentile annual wage).
- The five resulting ladder boundaries, formatted for pasting straight into the salary ladder modal on
/console/benchmarks(floor, the three interior boundaries, ceiling).
At the end, a summary line: how many disciplines produced a ladder, how many were skipped or failed, and how many had no occupation mapped at all.
The published figures are a starting point, not a verdict. Some businesses deliberately pay above the local market to win talent; some pay below it and compete on other things. Treat the printed ladder as a first draft to sanity-check against your own pay strategy, not a number to paste in unquestioned.
Why some disciplines don't get a ladder
The script refuses rather than approximates. You'll see one of these reasons:
- "no occupation mapped" — the discipline has no
soc_codeset. Set one on/console/disciplinesand re-run. - "top-coded/censored value" — published wage data caps very high earners at a bound (e.g. "≥ $239,200") rather than a real figure. Arithmetic on a capped value is meaningless, so the script refuses that geography tier and tries the next broader one; if every tier is capped, the discipline is reported unavailable.
- "no data published for this geography" — the tier is simply empty for that occupation × location (a suppressed cell). The script falls through to the next broader tier automatically.
- A data-anomaly refusal (e.g. two percentiles came back equal) — the published figures for that geography don't support a valid ladder. This is reported directly; it does not fall through to a different tier, since the data existed but didn't make sense.
- An HTTP or network failure — reported with the status code. The script never substitutes a different geography to paper over a transport error.
What the script never does
- It never mixes geography tiers within one ladder (no splicing a national 90th onto a local 25th).
- It never treats a capped/censored figure as if it were a real number.
- It never guesses an occupation for an unmapped discipline.
- It never writes to the database, calls a write endpoint, or posts to the audit channel.
After running it
Copy the five printed boundaries into the discipline's salary ladder modal on /console/benchmarks (owner only). Nothing changes until you do — the script's job ends at the terminal.