One-time backfill of Okta profile attributes for legacy users who predate the TriNet → Okta integration, sourced from the HR sheet export (“Schmidt Entities Employee List for Hillspire IT Use” → Active Employees tab).
Status: complete. The backfill ran live on 2026-05-19 — 147 users updated, zero failures. It does not need re-running. Keep it for the next wave of legacy accounts (an acquisition, or an entity brought under TriNet), not as a routine job. For ongoing single-attribute maintenance see
okta_title_sync.
TriNet provisions new hires into Okta with a full profile. Anyone hired before
that integration existed has an Okta account with most profile fields empty —
no employee number, no manager, no department, no office address. That gap shows
up everywhere downstream: org charts, app provisioning rules that key off
department, and any SCIM push that reads from Okta.
The tell is profile.employeeNumber: TriNet always sets it, so an active user
with an empty employeeNumber is a pre-integration account. That is the
delta this script fills.
ACTIVE Okta user, then filter client-side for an empty or
missing employeeNumber. (Okta’s search parameter can’t filter on a custom
attribute reliably, so the filter happens locally.)POST /api/v1/users/{id} so untouched attributes are
preserved.Dry run prints the exact payloads and writes nothing.
| Okta attribute | HR sheet column |
|---|---|
employeeNumber |
EEID — forced to string, so leading zeros survive |
organization |
Entity |
displayName |
Display Name |
title |
Job Title |
manager |
Manager Name |
managerId |
Manager Email |
department |
Department Name |
city / state |
Work City / Work State |
streetAddress / zipCode / countryCode |
derived from Office Location via a built-in lookup table |
Blank values, #N/A and * are skipped rather than written, so the script never
overwrites a real Okta value with an HR placeholder.
Office lookup. The HR sheet gives an office name (“Newport Office (RI)”),
not an address. OFFICE_TO_ADDRESS near the top of the script maps 13 known
offices to street/zip/country. Trailing state suffixes in parentheses are
stripped before matching. Remote deliberately has no countryCode — remote
staff are not all US-based, and guessing would be wrong.
Excluded entities. EXCLUDE_ENTITIES holds the spinout and
non-Hillspire-managed entities (Napatree, Swift Beat, Svitla Doba, Saidak
Avtomatyka, Sentala, BRDR Management, Team Savonte). Users at those entities are
reported and skipped, never written.
cd integrations/okta_profile_sync
npm install
cp .env.example .env # then fill in OKTA_DOMAIN and OKTA_API_TOKEN
Export the Active Employees tab to CSV. Keep it outside the repo — the convention across these integrations is:
~/Documents/hr-exports/okta_profile_sync/hr_export.csv
The CSV holds names, EEIDs, emails and managers for the whole company. Keeping it
out of the working tree removes the risk rather than relying on an ignore rule to
catch it. See okta_title_sync for the HR_EXPORT_CSV
pattern that does this properly.
The token needs profile-edit rights, so it must be minted by a Super Admin or User Admin. Revoke it when the run is finished — house rule.
node backfill-okta-profiles.js --csv ~/Documents/hr-exports/okta_profile_sync/hr_export.csv --dry-run
node backfill-okta-profiles.js --csv ~/Documents/hr-exports/okta_profile_sync/hr_export.csv
| Flag | Effect |
|---|---|
--csv <path> |
HR CSV export. Required. |
--dry-run |
Read-only: prints the delta and every payload, writes nothing |
The npm run dry-run / npm run run-live shortcuts in package.json assume a
CSV at ./hr_export.csv, which contradicts the keep-PII-outside-the-repo rule
above. Prefer the explicit --csv form.
The CSV parser is not quote-aware across line breaks. At least one HR row
(ukhan@hillspire.com) has a Job Title containing an embedded newline inside a
quoted field. This parser splits on newlines before parsing quotes, so that row
is mangled — any title this script wrote for an affected user should be
treated as suspect. okta_title_sync has a corrected
parser and has since rewritten titles tenant-wide, so in practice this is already
repaired; it matters only if you re-run this script.
title and other attributes may be profile-mastered by TriNet. Where Okta
lists an attribute as PROFILE_MASTER, a direct API write can be reverted on
TriNet’s next push. For the legacy users this script targets there is no TriNet
record to push, which is exactly why the backfill sticks — but the same write
against a TriNet-managed user would not.
Re-running is safe but pointless. The script re-derives the delta each run,
so a user who now has an employeeNumber drops out of scope automatically.
A JSON audit log and a Confluence-ready markdown report land next to the script. Both contain employee data and are gitignored.