Gusto HR Source
nexusID integrates with Gusto as an authoritative HR source and drives the full joiner/mover/leaver (JML) lifecycle from it. Gusto is API-first (OAuth 2.0), so the integration pulls the employee directory directly from the Gusto API — no file transfer.
How it flows
Gusto API (OAuth2)
→ GustoProvider.fetchRecords() (integration/hr-gusto)
→ GustoConnector.upsert() (match by employeeId = Gusto uuid)
→ LifecycleEventService onJoiner / onMover / onLeaver
→ LifecycleScheduleExecutor (PROVISION → ENABLE → DISABLE)
→ Entra / AD / SCIM / downstream provisioning + deprovisioning
→ HR source precedence + WorkflowEngine (JOINER / MOVER / LEAVER)
Identical pipeline to the Paycom HR source — only the external connector differs.
Configure in the admin console (recommended)
Add Gusto as an HR_SOURCE integration in Admin → Integrations — no environment variables,
no redeploy. Pick the Gusto provider and fill in: accessToken (OAuth2 Bearer), companyId
(company UUID), baseUrl (default https://api.gusto.com), and apiVersion. Secrets are encrypted
at rest, and the unified HR sync sweep picks it up. See HR Sources for the model.

The GUSTO_SYNC_* variables below are an optional alternative for GitOps / air-gapped deployments
that keep secrets out of the database. They're off by default — most operators should use the
admin console above.
Inbound configuration (env, legacy)
GUSTO_SYNC_ENABLED=true
GUSTO_SYNC_ACCESS_TOKEN=... # OAuth 2.0 bearer access token
GUSTO_SYNC_COMPANY_ID=<company uuid>
GUSTO_SYNC_BASE_URL=https://api.gusto.com # https://api.gusto-demo.com for the demo env
GUSTO_SYNC_API_VERSION=2024-04-01 # sent as X-Gusto-API-Version
GUSTO_SYNC_CRON="0 0 0,6,12,18 * * *" # every 6h
nexusID calls GET /v1/companies/{companyId}/employees (paginated) with
Authorization: Bearer <token> and the X-Gusto-API-Version header.
Field mapping
Each canonical field maps to a dot-path in a Gusto employee object (defaults are Gusto-accurate):
| Canonical | Default Gusto path | Notes |
|---|---|---|
employeeId (required) | uuid | the Gusto employee UUID (match key) |
email | email | |
firstName / lastName | first_name / last_name | display name is derived if absent |
jobTitle | jobs.0.title | nested — first job's title |
department | department | |
status | terminated | boolean → true = TERMINATED, false = ACTIVE |
A leaver is detected when status resolves to TERMINATED/TERMED/INACTIVE. If your tenant
exposes a string status instead, point status at current_employment_status (it's upper-cased).
Scheduling & near-real-time
GustoSyncScheduler polls on GUSTO_SYNC_CRON (default every 6h). Lower the interval for
near-real-time change detection.
Multi-source precedence
When Gusto is one of several HR feeds, HR Source Precedence decides which
source wins per attribute. Add GUSTO to the precedence order.
Outbound (nexusID → Gusto write-back)
nexusID can push termination on leaver back to Gusto via POST /v1/employees/{id}/terminations.
It's off by default and never writes to Gusto unless explicitly enabled, best-effort (a failure
never breaks the JML flow), and skips Gusto-originated terminations to avoid echo.
GUSTO_SYNC_OUTBOUND_ENABLED=true
GUSTO_SYNC_OUTBOUND_ACCESS_TOKEN=... # falls back to GUSTO_SYNC_ACCESS_TOKEN
GUSTO_SYNC_RUN_TERMINATION_PAYROLL=false
Attribute updates (mover) are not pushed to Gusto: its update endpoints require a per-record optimistic-lock
version, so write-back is scoped to terminations.
Where it lives in code
- Provider:
modules/integration/hr-gusto/.../GustoProvider.kt(+GustoApiClient.kt,GustoWriteClient.kt) - Connector + scheduler + outbound + config:
modules/api/scim/.../connector/gusto/ - Outbound dispatch:
HrOutboundWriterinvoked fromLifecycleEventService