SysEngineering

Okta Profile Sync

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.

Why

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.

How it works

  1. Pull every 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.)
  2. Join that delta to the HR CSV on the Okta login, matched case-insensitively against Preferred Work Email.
  3. Build a partial profile payload per user — only fields the HR sheet actually has a value for.
  4. Write with a partial POST /api/v1/users/{id} so untouched attributes are preserved.

Dry run prints the exact payloads and writes nothing.

What it writes

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.

Setup

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.

Run

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.

Things that bite

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.

Output

A JSON audit log and a Confluence-ready markdown report land next to the script. Both contain employee data and are gitignored.