> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md).

# Import users from a CSV file

Load a user CSV into OpenIAM from the shipped example configuration — two test records first, then the file you were given.

## What this is, and when you need it

A CSV load is an ordinary synchronization whose source is an uploaded file. It is the fastest way to get real identities into OpenIAM, and the safest place to prove a match rule and a transformation before either goes near an HR feed or a directory.

Use this page for a file of people; roles, groups, organizations and entitlement memberships load through their own configurations and in a fixed order — see [Import roles, groups, organizations and entitlement memberships](/administration-guide/lifecycle/synchronize-and-import-data/import-roles-groups-and-memberships.md). Two test users first, every time: the second run of the same unchanged file must update or do nothing, never create the same two people again.

## At a glance

|                          |                                                                                                                                                                                                                                                            |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | One `SYNCH_CONFIG` row and two test users from the file — two `SYNCHRONIZATION_RECORD` and two `IMPORT_USER` entries, two users you can open, and a second run of the same file that creates nothing. With provisioning off, nothing on any managed system |
| **Where**                | **Provisioning → Synchronization**, `/webconsole/synchronizations` to list and `/webconsole/synchronization` to create; **Administration → Groovy Manager** for the two shipped CSV scripts                                                                |
| **You need first**       | A file of two or three test records, the column holding your immutable correlation key, and Groovy Manager access to copy the two shipped CSV scripts                                                                                                      |
| **Takes**                | 20–30 minutes for the two test records; the real extract scales with the row count                                                                                                                                                                         |
| **Reversible**           | Partly — untick **Active** and remove the named test users; nothing undoes a real extract whose wrong match value updated the wrong people                                                                                                                 |

## Before you begin

* The file, with a header row whose column names are exactly what the transformation script reads, case included.
* The delimiter it actually uses, as a single character. **Splitter** is that character and nothing else.
* The immutable correlation key and its column — `EMPLOYEE_ID` in the shipped example. Not a display name, not an email address.
* Groovy Manager access, to copy `sync/user/csv/CSVSampleValidationScript.groovy` and `sync/user/csv/CSVSampleTransformationScript.groovy` into a customer-owned path before you change either.

## Load two test users

{% stepper %}
{% step %}

### Copy the shipped CSV example

`/webconsole/synchronizations` lists `CSV USER Example` already working. Copy it rather than building a config from scratch, and copy its two scripts into a customer-owned path in **Groovy Manager** before you change anything — an upgrade replaces a shipped script in place.

| Shipped configuration | `PROCESS_RULE` | Match field / source field    | Scripts                                                                                                                      |
| --------------------- | -------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `CSV USER Example`    | `USER`         | `EMPLOYEE_ID` / `EMPLOYEE_ID` | `sync/user/csv/CSVSampleValidationScript.groovy`, `sync/user/csv/CSVSampleTransformationScript.groovy`; body `test-user.csv` |
| `CSV ROLE Example`    | `ROLE`         | `NAME` / `NAME`               | The role CSV pair; body `test-roles.csv`                                                                                     |

Both have shipped since 4.0.0.0, so both are present on any current deployment; if `/webconsole/synchronizations` does not list them, somebody deleted them.

The script contract is [Edit a synchronization transformation script](/administration-guide/automation/synchronization-scripts.md).
{% endstep %}

{% step %}

### Prepare the file

Two unique test identities, no production records, a stable source identifier, a name, a status, and whatever organization or user-type value the transformation requires.
{% endstep %}

{% step %}

### Create the config, Active off

`/webconsole/synchronization`. Name it unmistakably (`CSV-JOINER-TEST`, not `User Sync`). Set **Synchronize Object** to `USER`, **Synchronization Source** to `CSV`, **Sync Type** to `FULL`. Set **Records per Batch** to `2`. Leave **Active**, **Detect orphans**, **Provision to target systems?** and **Sync Frequency** empty. Upload the file and set **Splitter** to the real delimiter.

The values you are creating are the same shape as the shipped `CSV USER Example`: `SYNCH_SRC = 'CSV'`, `PROCESS_RULE = 'USER'`, `SYNCH_TYPE = 'FULL'`, `MANAGED_SYS_ID = '0'`, `MATCH_FIELD_NAME = 'EMPLOYEE_ID'`, `MATCH_SRC_FIELD_NAME = 'EMPLOYEE_ID'`, scripts `sync/user/csv/CSVSampleValidationScript.groovy` and `sync/user/csv/CSVSampleTransformationScript.groovy`.

That combination — inactive config, **Records per Batch** `2`, a two-record file, provisioning and orphan detection off — is the whole test scope for the first run.
{% endstep %}

{% step %}

### Configure matching

**OpenIAM field name** `EMPLOYEE_ID`, **Source Attribute Name** the CSV column holding it. Confirm the column name matches the header exactly — case included.
{% endstep %}

{% step %}

### Select the scripts

**Validation Rule** and **Transformation Rule** are both required. Start from the shipped CSV pair, copied into your own path.
{% endstep %}

{% step %}

### Activate and run once

Tick **Active**, save, then **Sync now**. It tests the source connection first and refuses to start on failure; for a CSV source that test is a file check, reported as `FILE_NOT_EXIST`. Success message: *"Synchronization has been started. This is a background process and may take some time depending on data volume."*
{% endstep %}

{% step %}

### Verify, then repeat unchanged

Two records read, two users created, correct attributes, no duplicates. Then run it again with **no file changes**: the second run must update or no-op, never create two more.
{% endstep %}
{% endstepper %}

The sample source files worth reading before you design a header are `sync/user/csv/Example CSV/Sync_user_CSV_all_fields_example.csv` — 60 columns, with `;`-separated `ROLES` and `GROUPS` columns — and `Sync_user_CSV_short_ph_addr_email_fields_example.csv` for the minimal case. The shipped scripts themselves live under `sync/user/csv/`, `sync/role/csv/` and `sync/group/csv/`.

## Verification

Two records read, two users created, attributes correct. The audit and report evidence is the same for every synchronization — [Set up a synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md#verification). Two checks are specific to a CSV load:

* **Open one imported user** and confirm the columns landed where you expected. A renamed column produces a user with fields missing and no failed record.
* **Run the same file again, unchanged.** Two more users means the match value is not finding the people the first run created.

## When it goes wrong

| Symptom                                                                           | Cause                                                                                 | Fix                                                                                                         |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Start refused, `FILE_NOT_EXIST`                                                   | CSV source with no uploaded file, or a file the service cannot read                   | Re-upload.                                                                                                  |
| The whole line lands in a single column                                           | `SPLITTER` is the line delimiter, not a multi-value separator                         | Set `SPLITTER` to the file's real field delimiter, and split multi-value columns inside the transformation. |
| Records fail with `VALIDATION_ERROR` and text about restricted special characters | A login, first name, last name or email address in the file fails a save-time pattern | See [Why a leading exclamation mark fails validation](#why-a-leading-exclamation-mark-fails-validation).    |

The rest of the failure catalogue — the run refusing to start, records read but nothing created, the wrong person updated — is on [Set up a synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md#when-it-goes-wrong).

## Rollback

Untick **Active** before you change anything. A failed test import leaves two named test users: remove or correct those, and nothing else. Do not bulk-delete on a count that looked wrong — distinguish created from matched from pre-existing using the `SYNCHRONIZATION_RECORD` audit entries first.

## Beyond the two test records

{% hint style="warning" %}
The risk changes here. Two test records with provisioning off is low-risk; the real extract is not, because a wrong match value updates the wrong people and reports success. Prove the match value is unique before you point this config at the file you were given.
{% endhint %}

{% hint style="warning" %}
**`SPLITTER` is the CSV delimiter, not a multi-value splitter.** It is a single character, and it is the one used to split each *line* into columns. The `;` inside a `ROLES` column in the shipped sample is split by the transformation script's own code, not by this field. Setting `SPLITTER` to `;` on a comma-delimited file does not give you multi-value columns; it gives you one column.
{% endhint %}

## Why a leading exclamation mark fails validation

A source value such as `!Smith`, or an address beginning `!`, does not fail in the transformation. It fails at **save** time, per record, in one of two validators:

* The **login** on every principal being added or replaced, plus the **first name** and the **last name**, are each checked against their own configurable pattern, and all the failures are collected into a single `VALIDATION_ERROR`.
* The same three patterns plus the email one are applied again on the user itself. An empty value always passes; a non-empty one has to satisfy the pattern, case-insensitively.

Three shipped patterns, and the anchoring is the whole explanation:

| Property                                                         | Shipped default       |
| ---------------------------------------------------------------- | --------------------- |
| `org.openiam.email.validation.regexp.name.first`, `...name.last` | `^[a-zA-Z0-9_,]*$`    |
| `org.openiam.email.validation.regexp.login`                      | `^[a-zA-Z0-9_,.@-]*$` |
| `org.openiam.email.validation.regexp`                            | the address pattern   |

Confirm the three current values under **Administration → System Configuration** — they are ordinary properties and a previous administrator may have widened them already.

Neither character class contains `!`. And although the check is a "match anywhere" search rather than a full match, each pattern is anchored `^…$`, so the only substring that can satisfy it is the entire value, and `^` matches only at position 0. A leading `!` therefore makes the match impossible from the first character. There is no partial acceptance and no stripping: the value is rejected whole.

The exception carries one error token per failing field, rendering as **"Login must not contain restricted special characters"**, **"First Name must not contain restricted special characters"** or **"Last Name must not contain restricted special characters"**. Because it is raised per record at save time the run continues, and that text is what appears in the run report's error column — which is where you will see it first.

Three legitimate responses, in order of preference: widen the property if `!` is genuinely part of your naming standard — the three patterns are configuration, catalogued in [System configuration](/administration-guide/platform/system-configuration.md); normalise the value in the transformation under an approved rule; or reject the row deliberately with `SKIP` so that somebody looks at it. Manufacturing an address or a name to get past the pattern is not one of them.

## Related

* [Set up a synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md) — the general procedure, the verification evidence and the full failure catalogue.
* [How a synchronization matches and processes a record](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md) — why the match column has to be unique, and what happens when it is not.
* [Import roles, groups, organizations and entitlement memberships](/administration-guide/lifecycle/synchronize-and-import-data/import-roles-groups-and-memberships.md) — the other CSV shapes, and the order they have to run in.
* [Synchronization configuration fields](/administration-guide/reference/synchronization-configuration-fields.md) — every field on the form.

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
