UserLoad
Manage the human (and service-account) users in a customer's Nuntiq tenant, for example to sync users from an upstream system (Workday, AD/Entra, an HRIS) into Nuntiq on a schedule.
:::caution Not available to connectors yet
No connector entrypoint key may read or write users today. A UserLoad
call from any entrypoint (also get_custom_N) gets a 403
missing_permission, in production and in the toolkit. Until a users
entrypoint exists, sync users through the customer API with an API key.
This page describes the class as it will work then, and the API it calls.
:::
Import
from lib.objects.user import UserLoad
Standard BaseLoad methods
get_all(), search(), new(), save_all(), patch_all(),
delete_keys(), index_by_source(), obj.save(), obj.delete(),
obj.deactivate(). See
BaseLoad.
Users are tenant-wide: an API key limited to organizations cannot read or write them.
Keys
A user is found by its Nuntiq id, by source_system + source_record_id
(the user's id in the HR or identity system), or by login_account.
User fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | str | (assigned) | Server-assigned number, as text |
first_name | str | yes | |
last_name | str | yes | |
email | str | yes | Unique across live users. A deleted user that holds the email is restored under the new login_account |
login_account | str | yes | Stable login handle. Unique. Natural key |
login_type | int | yes | 1 = username/password, 2 = SSO |
password | str | Write-only, login_type 1 only. Stored as a hash; the user must change it at the next sign-in. Omit to keep the current password. A user switched to SSO loses its password | |
sso_provider | str | When login_type = 2: the alias of the tenant's identity-provider configuration | |
is_active | bool | Default true. Kept when a POST leaves it out, so a feed never reactivates a user someone switched off | |
active_from | str (ISO datetime) | Kept when left out | |
active_to | str (ISO datetime) | Kept when left out | |
send_invite | bool | Write-only. True emails an active password user a set-password link after the save | |
source_system / source_record_id / source_updated_at | str | External identity, as on every engine resource | |
must_change_password | bool | (read-only) | Set when a password is supplied |
created_at / updated_at | str | (read-only) |
Deactivating or deleting a user ends its sessions and open set-password links.
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. An unknown code rejects the user (unknown_group) |
name | (read-only) | Returned on GET for display; ignored on POST |
Replace-all semantics: when you POST a user, the groups list (if
present) replaces the user's memberships entirely. Leave it out to
keep the memberships: a user without groups and without an assignment
leaves the list out. To clear the memberships, assign an empty list (it is
sent, also when empty) or patch the user with 'groups': []:
u.groups = [] # on a user you save
load.patch_all([{'login_account': 'jane.doe', 'groups': []}])
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'
results = load.save_all()
return {'saved': len(results)}
Sync pattern: syncing from an upstream HRIS
Upsert keyed on the HR system's id, run on a schedule:
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.source_record_id = upstream['employee_id']
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 are updated; new ones are created. With
save_all(full_load=True) the run is a sync run: users of your source
system that the HR feed no longer has are deleted afterwards (nothing is
removed when a user was rejected).
Search
Exact-match filters: login_account, email, is_active, login_type,
source_system, source_record_id, plus updated_since and
include_deleted.
matches = load.search(login_account='jane.doe')
u = matches[0] if matches else None
for u in load.search(email='jane.doe@example.com'):
print(u.login_account, u.email)
# Active users only
for u in load.get_all(is_active=True): # sent as true
print(u.email)
Deactivate and delete
# Switch a user off and keep it
load.patch_all([{'login_account': 'svc-leaving', 'is_active': False}])
# The same for a user you read: PATCHes is_active to false, does not delete
u.deactivate()
# Soft delete: also removes the group and processing-team memberships
load.delete_keys([{'login_account': 'svc-leaving'}])
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:
ValidationError(a missing required field) andRecordsRejected(for example an unknown group or an email in use).