> 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.md).

# Set up a synchronization

Create a synchronization configuration for an HR, directory, database or custom source, run it once against a disposable test scope, and read the evidence it leaves.

## What this is, and when you need it

**Synchronization** turns records from outside OpenIAM — an HR system, a directory, a database, a CSV, an audit log — into governed OpenIAM objects. One `SYNCH_CONFIG` row per feed, three Groovy scripts, one match rule.

The failure that costs money is not a failed import. It is a *successful* import with the wrong match rule, the wrong transformation, or **Provision to target systems?** on before the transformation was proven — because that combination changes access for a population, in one run, at machine speed. So the first run is always two or three disposable records with provisioning off.

This page is the procedure for any source. A plain user file has a shorter path, [Import users from a CSV file](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md); entitlements and memberships have their own order, [Import roles, groups, organizations and entitlement memberships](/administration-guide/lifecycle/synchronize-and-import-data/import-roles-groups-and-memberships.md).

## At a glance

|                          |                                                                                                                                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **You will end up with** | One `SYNCH_CONFIG` row and a completed first run over two or three disposable records — one `SYNCHRONIZATION_RECORD` per source row and one `IMPORT_*` per object written                                          |
| **Where**                | **Provisioning → Synchronization**, `/webconsole/synchronizations` to list and `/webconsole/synchronization` to create; **Administration → Groovy Manager** to register the scripts                                |
| **You need first**       | A validation and a transformation script that both compile, the source's immutable correlation key, and a source extract of two or three test records                                                              |
| **Takes**                | 45–90 minutes for a first configuration; the run itself scales with the source                                                                                                                                     |
| **Reversible**           | Partly — untick **Active** and revert to the last reviewed script version, but identities a run created or overwrote are not undone by that, and a wrong match value that updated the wrong people reports success |

## Before you begin

* The source's immutable correlation key, and the OpenIAM field it matches. Not a display name. Not `sAMAccountName` — it is mutable and cloud-only users do not have one.
* A validation script and a transformation script, both registered and compiling. The form marks both **Validation Rule** and **Transformation Rule** required and will not save without them.
* For a `CONNECTOR_ASYNC` source, an **attribute-names lookup script** — the form marks it required as soon as you choose that source — and a managed system whose heartbeat reads `Ok`, because **Sync now** tests the connection before it starts anything.
* A source extract containing two or three test records and no production data.
* Expected counts, written down before the run.

## Create and run the configuration

Route `/webconsole/synchronizations` lists configs; `/webconsole/synchronization` creates one and `/webconsole/synchronization?id=<syncConfigId>` edits one. Saving a configuration audits `SYNCHRONIZATION_CONFIG_SAVED`; deleting one audits `SYNCHRONIZATION_CONFIG_DELETED`.

{% stepper %}
{% step %}

### Copy the closest shipped configuration

Build nothing from scratch. `/webconsole/synchronizations` already lists working configurations; copy the one closest to your case, and copy its scripts into a customer-owned path in **Groovy Manager** before changing anything, because an upgrade replaces a shipped script in place. The script contract is [Edit a synchronization transformation script](/administration-guide/automation/synchronization-scripts.md).
{% endstep %}

{% step %}

### Create the config with Active off

`/webconsole/synchronization`. Name it unmistakably — `HR-JOINER-TEST`, not `User Sync`, because the name becomes the batch task name and the report filename prefix. Set **Synchronize Object** to the object type this feed owns, and **Records per Batch** to `2` for the test.

Leave **Active**, **Detect orphans**, **Provision to target systems?** and **Sync Frequency** empty. An inactive config cannot start, and has no batch task.
{% endstep %}

{% step %}

### Point the source at a narrow scope

Set **Synchronization Source**, then fill only what that source needs:

* `CSV` — upload the file, set **Splitter** to its real single-character delimiter.
* `RDBMS` — **JDBC Driver**, **Connection Url**, **Source login ID**, **Source password**, and a **SQL Query** returning a handful of test rows.
* `CONNECTOR_ASYNC` — the **Managed System**, a **Directory Filter** narrow enough to return only your test accounts, and a **Source attribute names** lookup script. An attribute missing from that script is missing from `columnMap`.
* `CUSTOM` — the **Custom Adapter Script** that returns the rows.
* `LOG` — nothing further; the source is OpenIAM's own audit events.

This is the blast-radius control for the whole run: a filter that returns the production population returns it on the first run too.
{% endstep %}

{% step %}

### Configure matching

In **Attribute Mapping & Rules**, set **OpenIAM field name** to how the value is looked up and **Source Attribute Name** to the source column holding it, spelled exactly as the source spells it. Use **OpenIAM Custom Attribute Name** only when the field name is `ATTRIBUTE`, and **Custom rule for matching** only when one field cannot identify the object safely.

Prove the match value is unique on the OpenIAM side before the first run against real data. Nothing in the product will do it for you, and an ambiguous value updates the wrong person without an error — [How a synchronization matches and processes a record](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md#match-before-transform).
{% endstep %}

{% step %}

### Select the scripts

**Validation Rule** and **Transformation Rule** are both required and the form will not save without them. Select your copies, not the shipped originals.
{% endstep %}

{% step %}

### Activate and run once

{% hint style="warning" %}
This is the first write. A wrong match value updates the wrong identities and reports success, so read the match field and the source field back off the config once more before you tick **Active**.
{% endhint %}

Tick **Active**, save, then **Sync now**. It tests the source connection first and refuses to start on failure. 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

Check the counts and the objects below, then run the same source again with **no changes**: the second run must update or no-op, never create the same objects twice. Only then widen the source scope.
{% endstep %}
{% endstepper %}

## Verification

**Audit tree.** `SYNCHRONIZATION` is the parent, with audit source `SYNCHRONIZATION`, the config name as its description, and the task description *"Sync process is started for `<name>`(`<id>`) at `<time>`"*. Beneath it:

| Action                                                                                 | One per                                                                                                                                                                                                           |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SYNCHRONIZATION_THREAD`                                                               | Thread                                                                                                                                                                                                            |
| `SYNCHRONIZATION_RECORD`                                                               | Record                                                                                                                                                                                                            |
| `SYNCHRONIZATION_ORPHAN`                                                               | Orphan detected                                                                                                                                                                                                   |
| `SYNCHRONIZATION_CONNECTOR_RESPONSE_PROCESSOR`                                         | Connector search response processed                                                                                                                                                                               |
| `SYNC_IDENTITY`                                                                        | Identity synchronised                                                                                                                                                                                             |
| `SYNCHRONIZATION_CONFIG_SAVED` / `SYNCHRONIZATION_CONFIG_DELETED`                      | Config change                                                                                                                                                                                                     |
| `SYNCHRONIZATION_CSV_FILE_ATTACHED`                                                    | CSV upload                                                                                                                                                                                                        |
| `IMPORT_USER`, `IMPORT_ROLE`, `IMPORT_GROUP`, `IMPORT_POLICY`, `IMPORT_MANAGED_SYSTEM` | Object actually written by the run, one per object type. These are the entries to count against the source row count — a `SYNCHRONIZATION_RECORD` means a row was read, an `IMPORT_*` means something was written |
| `SAVE_USER_IN_DB`                                                                      | Child of `IMPORT_USER`, the database write itself                                                                                                                                                                 |

Provisioning children hang off the record events: the `SYNCHRONIZATION_RECORD` entry is the parent of the resulting `PROVISIONING_MODIFY` and `PROVISIONING_SAVE`, so one record's whole fan-out is one subtree and you can read a single row's complete consequence in one place. Part-level events carry `MAX_TIME`, `MIN_TIME`, `AVERAGE_TIME`, `SUCCESS_COUNT` and `FAIL_COUNT`. Every audit retention property ships `-1`, meaning nothing is pruned — so this evidence stays until somebody sets one.

**What an uploaded CSV records.** A `SYNCHRONIZATION_CSV_FILE_ATTACHED` entry describes the uploaded file rather than containing it. The request body holds the file name, its size in bytes, how many lines were stored, and the identifiers of the synchronization configuration and the stored CSV configuration:

```json
{
  "fileName": "employees.csv",
  "sizeBytes": 41236,
  "lineCount": 802,
  "syncConfigId": "2c9180865e...",
  "csvConfigId": "2c9180865f..."
}
```

That identifies which file was uploaded and where it went, without the identity records themselves being retained in the audit store. The line count is the number of lines actually stored, so blank lines in the source file are not counted — expect it to differ from the raw line count of the file on disk. A `FAILURE` on this action means the upload was refused, either because the file was not a CSV or because it could not be stored.

**Failure reasons.** A failed record carries a response code in the report's error column and in its `SYNCHRONIZATION_RECORD` audit entry. Every code the run can produce, with what it means and what to do, is in [When it goes wrong](#when-it-goes-wrong).

**Per-record results.** The history screen at `/webconsole/synchronization-history` lists each run; expanding a run shows its `SYNCHRONIZATION_RECORD` children, one per record, each with its own result. That tree is the fastest way to answer "which records failed and why" without downloading anything. Judge a record by its child `PROVISIONING_*` entry, not by the `SYNCHRONIZATION_RECORD` entry above it: a record can be recorded as read successfully and still have failed to save.

**The run report.** Download it from the run's row on `/webconsole/synchronization-history`. The file is named `<config name>__<rootAuditLogId>.csv`. Its header column is the **OpenIAM** match field name, not the source column — so the header reads `EMPLOYEE_ID` or `EMAIL` while the data rows below it are keyed on the *source* value. Expect that mismatch; it is not a defect, and it is the commonest reason a report looks like it is keyed on the wrong thing.

**The objects.** Search each expected user and verify attributes, principal list, status, organization, groups, roles and effective dates. If **Provision to target systems?** was on, follow through to the connector reply and to the managed system itself — "user saved in OpenIAM" is not completion on the managed system.

**Deliberate negatives.** Confirm a record you excluded by filter was not read. Confirm the second unchanged run created nothing.

## When it goes wrong

| Symptom                                                                                                                                     | Cause                                                                                                                                                                                                                                                                                                                                                                                                                            | Fix                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Save rejected, `SYNCH_CONFIG_VALIDATE_FAIL`                                                                                                 | A required field is empty. `Records per Batch` ≤ 0 adds `RECORDS_NUMBER_MUST_BE_GREATER_ZERO`                                                                                                                                                                                                                                                                                                                                    | Fill Name, Synchronize Object, Source, Match field, Source Attribute, Validation Rule, Transformation Rule.                                                                                                                                                                                                                                                                                                                       |
| `Transformation Implementation` reverts to `TRANSFORMATION`                                                                                 | Every save forces it                                                                                                                                                                                                                                                                                                                                                                                                             | Not configurable. Write a transformation script.                                                                                                                                                                                                                                                                                                                                                                                  |
| Start refused, `SYNC_CONFIG_ALREADY_RUNNING`                                                                                                | `RUNNING_SYNC` positive, usually from a run that died                                                                                                                                                                                                                                                                                                                                                                            | Confirm the synchronization queues hold no live work, then open the configuration and press **Reset** on the `Synchronization is in progress.` banner above the form.                                                                                                                                                                                                                                                             |
| Start refused, `FAIL_CONNECTION`                                                                                                            | **Sync now** tests the source first; for `CONNECTOR_ASYNC` that is the managed system's heartbeat                                                                                                                                                                                                                                                                                                                                | [Verify connectivity to the target](/administration-guide/lifecycle/verify-connection.md).                                                                                                                                                                                                                                                                                                                                        |
| Records read, none created                                                                                                                  | One of the three creation gates: **Detect orphans** on with nothing matched, a transformation returning `SKIP`/`SKIP_OK`/`DELETE`, or provisioning off with new objects carrying no login                                                                                                                                                                                                                                        | Work the gates in order — [Creation on no match is gated three ways](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md#creation-on-no-match-is-gated-three-ways).                                                                                                                                                                                                            |
| Source and OpenIAM record counts do not reconcile, and the failure count is zero                                                            | The rows are missing the match column and are discarded before matching is attempted. A **null** column logs `Line object doesn't contain the match field. Object from source: %s` and the row is skipped; a **blank value** is skipped with no log at all. Either way the row never reaches a transformation script: not created, not an orphan, not in the error report, not counted. **Orphan detection does not catch this** | Grep the service log for `Line object doesn't contain the match field. Object from source:`, then reconcile by count for the blank case, which leaves no trace at all. Fix the source, or point **Source Attribute Name** at a column every record actually carries.                                                                                                                                                              |
| **The wrong existing person is updated, and the run reports success**                                                                       | The match value resolved to more than one OpenIAM object and the first candidate silently won — no error, no failed record, no report row                                                                                                                                                                                                                                                                                        | Query OpenIAM for duplicates on the match value before running, move to a field that is unique by construction (`EMPLOYEE_ID`, `USERID`), or supply a **Custom rule for matching** that stops on multiple. Then repair the overwritten identity from the audit tree. See [Match before transform](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md#match-before-transform). |
| A `USER` config set to match on `NAME` matches on login instead                                                                             | OpenIAM does not compare names for a `USER` match; it warns and falls back to the default managed system's principal                                                                                                                                                                                                                                                                                                             | Choose the field you actually meant. `NAME` is valid for `ROLE`, `GROUP`, `ORG` and `RESOURCE`, not for `USER` — [Two silent degradations](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md#two-silent-degradations).                                                                                                                                                       |
| Every new user fails with `NOT_FOUND` and `Logins collection must not be empty when provisioning is not enabled.`                           | **Provision to target systems?** is off and the transformation gave the new user no OpenIAM login                                                                                                                                                                                                                                                                                                                                | Add the login in the transformation script — the shipped JDBC and Active Directory transformations in **Administration → Groovy Manager** show the pattern — or enable provisioning deliberately.                                                                                                                                                                                                                                 |
| A record fails but the report's reason column is empty or truncated                                                                         | The transformation threw. The audit description reads `Exception during transformation script execution` and the report row is `<match value>,F,<message>`, keeping the exception's own message only when it is 50 characters or shorter                                                                                                                                                                                         | Read the `SYNCHRONIZATION_RECORD` audit entry rather than the report row.                                                                                                                                                                                                                                                                                                                                                         |
| Records are retried, log shows `User with id %s is saving in another thread. Current request will be sent back to the queue to be retried.` | Two saves collided on the same existing user id, so one was rejected with `ANOTHER_SAVE_IN_PROGRESS` and requeued                                                                                                                                                                                                                                                                                                                | Expected under load. If constant, lower **Records per Batch** or the bulk-provisioning concurrency.                                                                                                                                                                                                                                                                                                                               |
| Termination workflow starts with a null end date                                                                                            | The request's end date comes from `pUser.accessRightEndDate` and nothing else                                                                                                                                                                                                                                                                                                                                                    | Set it in the transformation, and check it on the request before trusting the feed.                                                                                                                                                                                                                                                                                                                                               |
| Imported user has no target identity                                                                                                        | The transformation added no `Login` for the target managed system, and no entitlement path granted the resource                                                                                                                                                                                                                                                                                                                  | Choose an ownership pattern deliberately.                                                                                                                                                                                                                                                                                                                                                                                         |
| Disabled accounts reactivated in bulk                                                                                                       | The transformation asserts `UserStatusEnum.ACTIVE` on every row and `IS_PROVISION = 'Y'`                                                                                                                                                                                                                                                                                                                                         | Map the source's disabled signal. Stop the schedule first.                                                                                                                                                                                                                                                                                                                                                                        |
| Custom match rule will not save                                                                                                             | `CUSTOM_MATCH_SCRIPT_ID` is only 10 characters wide                                                                                                                                                                                                                                                                                                                                                                              | Confirm the script ID fits, or use the built-in rule with a narrower match field.                                                                                                                                                                                                                                                                                                                                                 |
| Base DN truncated                                                                                                                           | `SYNCH_CONFIG.BASE_DN` is only 50 characters wide                                                                                                                                                                                                                                                                                                                                                                                | Use the managed system's `MNG_SYS_OBJECT_MATCH.SEARCH_BASE_DN`, which is 200, and leave the sync's `BASE_DN` for short values.                                                                                                                                                                                                                                                                                                    |
| Rows disappear before the transformation runs, and only `FAIL_COUNT` moves                                                                  | A validation script returned `NOT_VALID` or `SKIP`. The two are treated identically: no error row, no audit record, and a single log line — `Validation return code %d. Stop with current object` — emitted only when debug logging is on                                                                                                                                                                                        | Exclude in the source query, or return `SKIP_OK` (`11`) from the transformation instead — see [Choosing where to exclude a record](/administration-guide/reference/synchronization-script-contracts.md#choosing-where-to-exclude-a-record).                                                                                                                                                                                       |
| A record fails with `MATCH_ATTR_EMPTY`                                                                                                      | The source match value is empty. The row is audited as a failed record, written to the report as `<the whole column map>,F,MATCH_ATTR_EMPTY` and counted in `FAIL_COUNT`                                                                                                                                                                                                                                                         | Repair the source or the filter. Do not guess.                                                                                                                                                                                                                                                                                                                                                                                    |
| A record fails with `DUPLICATE_PRINCIPAL`                                                                                                   | The calculated principal already exists                                                                                                                                                                                                                                                                                                                                                                                          | Review matching and principal generation.                                                                                                                                                                                                                                                                                                                                                                                         |
| A record fails with `VALIDATION_ERROR`                                                                                                      | **Not** from a validation script. A login, first name, last name or email address failed a save-time regular expression. A validation script returning `NOT_VALID` produces no response code and no error row at all, only a `FAIL_COUNT` increment                                                                                                                                                                              | Widen the property deliberately, or normalise the value in the transformation — see [Why a leading exclamation mark fails validation](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md#why-a-leading-exclamation-mark-fails-validation).                                                                                                                                                      |
| A full HR reload queues for hours behind approvals                                                                                          | Every joiner returned `INITIATE_NEW_HIRE_WORKFLOW` and the workflow-initiate queue is capped at `org.openiam.rabbitmq.workflow.initiate.max = 5`                                                                                                                                                                                                                                                                                 | Return `NO_DELETE` for the bulk baseline and reserve `5` for genuine ongoing joiners.                                                                                                                                                                                                                                                                                                                                             |

Failures specific to one kind of feed are with that feed: the CSV file and the delimiter on [Import users from a CSV file](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md#when-it-goes-wrong), entitlement rows on [Import roles, groups, organizations and entitlement memberships](/administration-guide/lifecycle/synchronize-and-import-data/import-roles-groups-and-memberships.md#when-it-goes-wrong), and the schedule and the incremental marker on [Schedule an incremental synchronization](/administration-guide/lifecycle/synchronize-and-import-data/incremental-synchronization.md#when-it-goes-wrong). Failures inside a script — a return code that does nothing, an attribute missing from `columnMap`, a workflow that never starts — are on [Edit a synchronization transformation script](/administration-guide/automation/synchronization-scripts.md#when-it-goes-wrong).

## Rollback

Stop the schedule and untick **Active** before changing scripts or matching. Revert to the last reviewed script version — which is why the scripts must be in a customer-owned versioned path, not edited in place.

For a failed test import, remove or correct only the named test objects, and prove no downstream action occurred by checking for `PROVISIONING_*` children under the `SYNCHRONIZATION_RECORD` events. Do **not** bulk-delete identities because a count looked wrong: first distinguish newly created, matched and pre-existing objects using the audit tree.

## Do not let a synchronization reverse lifecycle state

The single most damaging shipped pattern: a transformation that assigns `UserStatusEnum.ACTIVE` to every incoming row, with `IS_PROVISION = 'Y'`. OpenIAM saves the new active status and then provisions it — back to the same directory, and onward to every other managed system. Disabled accounts come back to life in bulk.

`sync/user/openLDAP/LdapUserTransformation_1.0.groovy` does exactly this: `pUser.status = UserStatusEnum.ACTIVE`, on every record, unconditionally. So does the AD PowerShell sample pattern. They are demonstrations.

Before you enable downstream provisioning:

1. Decide which system owns status for joiner, pending-start, leave, disabled, terminated and rehire.
2. Map the source's own disabled/leave signal instead of asserting `ACTIVE`.
3. Test a currently disabled user whose source record is still present. The status must not change.
4. Keep `IS_PROVISION = 'N'` for account-inventory imports unless the feedback loop is intentional.
5. Verify a no-change import does not enable, unlock, recreate or clear the end date of any account on any managed system. Check one deliberately disabled account on the managed system itself, not in OpenIAM.

If a run has already reactivated accounts: stop the schedule and downstream provisioning, identify the run and the users from the `SYNCHRONIZATION` audit tree, restore status through the authoritative path, and prove the corrected transformation on one disposable identity before resuming.

## Related

* [How a synchronization matches and processes a record](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md) — matching, creation gates, orphans and the concurrency rules every feed obeys.
* [Synchronization configuration fields](/administration-guide/reference/synchronization-configuration-fields.md) — every field on the form, the five source types and the batch defaults.
* [Import users from a CSV file](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md), [Import roles, groups, organizations and entitlement memberships](/administration-guide/lifecycle/synchronize-and-import-data/import-roles-groups-and-memberships.md), [Schedule an incremental synchronization](/administration-guide/lifecycle/synchronize-and-import-data/incremental-synchronization.md) and [Import from two authoritative sources without overwriting](/administration-guide/lifecycle/synchronize-and-import-data/import-from-two-authoritative-sources.md) — the four feeds with their own procedures.
* [Edit a synchronization transformation script](/administration-guide/automation/synchronization-scripts.md) — the script contracts and every binding. That page owns the contract; this one owns the configuration.
* [Configure provisioning](/administration-guide/lifecycle/configure-provisioning.md) — what happens once `IS_PROVISION` is on.

*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.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.
