Staged import with preview
CSV upload and directory-connector syncs now feed a common pipeline: matching rules, a preview you can read before anything is applied, master-priority protection so one source cannot overwrite another's attribute, and how to delete a source you no longer need.
This is a different, newer import path from the one described in Bulk importing users. That article's CSV/JSON endpoints create fixed-role member accounts with no matching logic — good for a one-off batch of brand-new people. The pipeline in this article is built for repeated, ongoing imports from a real source of truth: it can create, link, update, deactivate or skip a person depending on what it finds, and it always shows you a preview before it writes anything.
Console: Identity → Import, at /iam/import. The same pipeline is what feeds the Active Directory agent's sync loop and any directory-pull connector — a CSV you upload by hand and an automated directory sync both land here.
Matching rules
Each import source declares how it matches an incoming row against an existing user:
| Rule | Matches on |
|---|---|
by_email | The row's email address against an existing user's. |
by_external_id | The identifier this specific source has previously recorded for a user — its own external reference, not a Qorionix id. |
by_expression | A CEL expression in the mapping scope (see The expression language behind group rules, policies and mappings), reading the incoming row as appuser and the candidate existing user as user. Useful when neither email nor a stored external id is a reliable key — matching on an employee number, for instance. |
What a match becomes
| Outcome | When |
|---|---|
create | No existing user matches. A new account is created. |
link | A match is found, but this source has never linked that user before — a first sighting. |
update | A match is found and this source already links that user — a refresh of previously imported data. |
deactivate | A user this source previously linked is no longer present in the source, and the source has DeprovisionOnMissing enabled. Deactivation only ever touches users this exact source owns — an unrelated account that simply has no external id recorded is never touched by this. |
skip | Nothing to do — the row already matches what's stored. |
Preview, then apply
Running a source produces a preview: every row's decision, computed purely, with no writes at all. You read the preview — how many creates, links, updates, deactivations, skips — before anything happens to a real account. Apply is a separate, explicit action that commits exactly what the preview showed. For the Active Directory agent's own sync loop, preview and apply run automatically on every scheduled pass rather than waiting for a click, so a directory sync actually converges rather than sitting as an unreviewed diff.
Every decision applied writes one row to the run's own record — this doubles as the run's audit trail; there is no separate audit table to cross-reference. GET .../runs/{id} shows you a run's outcome and its per-record decisions. While a run is being applied it sits in an applying state; it only reaches its terminal applied or failed state once every record has been written.
Master-priority protection
Every import source has a priority (lower number wins). When two sources both write to the same attribute on the same user — your AD sync says one department, a CSV upload from HR says another — the write is compared against whichever source currently owns that specific attribute. A lower-priority source's write to an attribute a higher-priority source already owns is silently skipped for that one attribute — no error, no row failure — while every other attribute on the same record still writes normally. This is the same ErrAttributeManaged protection Profile fields and custom attributes describes for the unified schema's master_source field, applied per-source rather than per-attribute-definition.
CSV, and its ceiling
A CSV source is header-mapped (columns matched by name), and short rows are skipped and counted rather than aborting the whole file. The same 1,000-row ceiling as Bulk importing users applies here.
Deleting a source
Each source in the list has a delete action (a trash icon) that opens a confirmation dialog before removing anything. Deleting a source removes the source itself, every run staged or applied under it, and every per-record decision those runs produced — the confirmation says so, because there is no separate undo once you confirm.
The delete is refused with a 409 if one of that source's runs is currently applying — you cannot delete a source out from under a run that is mid-write. Wait for the run to finish (either applied or failed) and delete again. Deleting a source that does not exist, or that belongs to another organisation, returns a plain 404.
The endpoints
POST/GET .../iam/import/sources
DELETE .../iam/import/sources/{id} 409 if a run is applying
POST .../iam/import/sources/{id}/runs JSON body
POST .../iam/import/sources/{id}/runs/csv raw CSV body
GET .../iam/import/runs/{id}
POST .../iam/import/runs/{id}/applyConsole screen: sources, a way to stage a CSV inline, run history, a per-record diff, an apply action and a delete action per source — functional, though more compact than a fully polished admin screen.