Skip to main content

UserLoad

Manage the human (and service-account) users in a customer's Nuntiq tenant. Connectors typically use this to sync user data from an upstream system (Workday, AD/Entra, an HRIS) into Nuntiq on a schedule.

Import

from lib.objects.user import UserLoad

Standard BaseLoad methods

get_all(), search(), new(), save_all(), delete_where(), obj.save(), obj.delete(), obj.deactivate(). See baseload.md.

User fields

FieldTypeRequiredNotes
idint(assigned)Server-assigned. Never accept on POST — upsert key is login_account.
first_namestryes
last_namestryes
emailstryesUnique across the tenant
login_accountstryesStable login handle. Unique. This is the upsert key.
login_typeintyes1 = username/password, 2 = SSO
passwordstrPlaintext on POST; API hashes it before storage. Write-only — never returned on GET. Setting it auto-sets must_change_password = true. Omit to leave existing hash unchanged.
sso_providerstrWhen login_type = 2, the alias of the IdP config (matches idp_sso_config.alias — e.g. "nuntiq", "default").
is_activeboolDefaults to true. Set false via DELETE / deactivate_inactive, not POST.
active_fromstr (ISO datetime)Optional start date
active_tostr (ISO datetime)Optional end date. Set automatically by DELETE and deactivate_inactive.
must_change_passwordbool(read-only)Server-set when a password is supplied
created_at / updated_atstr(read-only)

UserGroupRef (nested)

User.groups is a list of group memberships. Each entry references a group by its stable external_code.

FieldRequiredNotes
external_codeyesMust match an existing group on the tenant. Unknown codes return 400.
name(read-only)Returned on GET for display; ignored on POST

Replace-all semantics: when you POST a user, the groups array (if present) replaces the user's memberships entirely. Omit the field to leave memberships untouched; send [] to clear them.

Building a user

from lib.objects.user import UserLoad

def run(context):
load = UserLoad(context)

u = load.new()
u.first_name = 'Jane'
u.last_name = 'Doe'
u.email = 'jane.doe@example.com'
u.login_account = 'jane.doe'
u.login_type = 1
u.password = 'initial-temp-pw' # hashed by API; sets must_change_password

g1 = u.new_group()
g1.external_code = 'AP_TEAM'

g2 = u.new_group()
g2.external_code = 'FINANCE'

load.save_all()
return {'created': 1}

Sync pattern — syncing from an upstream HRIS

Idempotent upsert keyed on login_account. Run on a schedule, all users in one batch:

def run(context):
load = UserLoad(context)

for upstream in fetch_users_from_hris(): # your code
u = load.new()
u.first_name = upstream['first_name']
u.last_name = upstream['last_name']
u.email = upstream['email']
u.login_account = upstream['login_account']
u.login_type = 2 # SSO
u.sso_provider = 'corp-okta'
# Replace group memberships from upstream
for code in upstream['team_codes']:
g = u.new_group()
g.external_code = code

load.save_all()

Existing users matching on login_account are updated; new ones are inserted.

# Exact match on login_account or id
u = next(load.search(login_account='jane.doe'), None)

# ILIKE on email / name
for u in load.search(email='@example.com'):
print(u.login_account, u.email)

# Active users only
for u in load.search(is_active=True):
print(u.email)

Soft delete

# By id (entity in hand)
u = next(load.search(login_account='jane.doe'))
u.delete() # soft: is_active = false, active_to = NOW()
# By filter
load.delete_where(
parameters=[{'login_account': 'svc-leaving'}],
action='delete',
)

deactivate_inactive(days=...)

Bulk-deactivate users who haven't actually signed in for N days. Useful for license-cleanup jobs against tenants with many provisioned-but-unused accounts.

result = load.deactivate_inactive(
days=90,
exclude_login_accounts=['svc-erp', 'svc-edi'], # service accounts: skip
dry_run=False, # set True to preview
)

print(f"Deactivated {result['count']} user(s)")
for r in result['deactivated']:
print(f" {r['login_account']} (last login: {r['last_login_at']})")
ArgRequiredNotes
daysyesPositive integer. Both the staleness threshold AND the minimum account age — brand-new accounts that haven't had time to log in are skipped.
exclude_login_accountsnoList of login_account strings to skip
dry_runnoWhen True, returns the candidate list without making any changes

Returns a dict: {deactivated: [...], count, truncated, dry_run, days}. last_login_at reflects the most recent non-impersonation successful login — impersonation sessions don't count as activity.

truncated is True when more than 1000 users were affected (the deactivated list is capped at 1000 but count is accurate).

What's next