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
| Field | Type | Required | Notes |
|---|---|---|---|
id | int | (assigned) | Server-assigned. Never accept on POST — upsert key is login_account. |
first_name | str | yes | |
last_name | str | yes | |
email | str | yes | Unique across the tenant |
login_account | str | yes | Stable login handle. Unique. This is the upsert key. |
login_type | int | yes | 1 = username/password, 2 = SSO |
password | str | Plaintext 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_provider | str | When login_type = 2, the alias of the IdP config (matches idp_sso_config.alias — e.g. "nuntiq", "default"). | |
is_active | bool | Defaults to true. Set false via DELETE / deactivate_inactive, not POST. | |
active_from | str (ISO datetime) | Optional start date | |
active_to | str (ISO datetime) | Optional end date. Set automatically by DELETE and deactivate_inactive. | |
must_change_password | bool | (read-only) | Server-set when a password is supplied |
created_at / updated_at | str | (read-only) |
UserGroupRef (nested)
User.groups is a list of group memberships. Each entry references a group
by its stable external_code.
| Field | Required | Notes |
|---|---|---|
external_code | yes | Must 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.
Search
# 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']})")
| Arg | Required | Notes |
|---|---|---|
days | yes | Positive 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_accounts | no | List of login_account strings to skip |
dry_run | no | When 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
- BaseLoad — read/write patterns shared by every entity.
- ApiClient → Error handling —
catching
ValidationErroron save_all (e.g. missing required field, invalid login_type).