> 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/reconciliation-scripts.md).

# Reconciliation scripts

The real Groovy contracts for reconciliation — population, query, match, custom command and pre/post-processor — with the signatures and packages that actually compile.

## What this is, and when you need it

A reconciliation compares a managed system's accounts against OpenIAM's identities and acts on each difference, and the interesting half of that work cannot be expressed as a setting. Which of two attributes identifies the same person when the obvious one is blank; what a target record has to become before it is a usable identity; what must be recorded when a run starts and finishes. Those are the questions the Groovy hooks answer, and needing to answer one of them is the reason to write a script.

When not to write one: the decision to create, delete or leave an account alone belongs in the situation response, because that is the part of a reconciliation a colleague or an auditor has to be able to read off the screen — see [Configure reconciliation](/administration-guide/lifecycle/configure-reconciliation.md). A plain attribute correspondence belongs on an attribute-map row for the same reason.

A reconciliation configuration is mostly a set of Groovy hooks. The population script decides what each record becomes; the query scripts decide what is compared; the pre/post-processors bracket the run. This page gives the exact base class, package and method signature for each, as they are in this release.

{% hint style="danger" %}
**Documentation and forum posts predating this release publish a population-script signature that does not compile.** They show a two-argument method taking string values:

`public int execute(Map<String, String> line, ProvisionUser pUser)`

The real interface is three arguments, and the values are `Attribute` objects, not `String`:

`int execute(Map<String, Attribute> line, T pUser, Map<String, Object> dataMap)`

The same older material names four more classes that do not ship. Fix the `import` lines in your script to the names in the third column — a script that imports a name from the first column will not compile, and the Groovy manager will tell you so on save.

| Named in old docs                                                                                   | Ships                                                                                      | Import instead                                                                      |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `org.openiam.provision.service.AbstractPrePostExecutor`                                             | ✗                                                                                          | `org.openiam.recon.service.impl.service.AbstractPrePostExecutor`                    |
| `org.openiam.idm.srvc.recon.service.AbstractPopulationScript`                                       | ✗                                                                                          | `org.openiam.recon.service.impl.service.AbstractPopulationScript`                   |
| `org.openiam.idm.srvc.recon.dto.ReconciliationConfig`                                               | ✗                                                                                          | `org.openiam.dto.ReconciliationConfig`                                              |
| `org.openiam.idm.srvc.res.dto.Resource`                                                             | ✗                                                                                          | `org.openiam.dto.Resource`                                                          |
| `ReconciliationServiceImpl.RECONCILIATION_CONFIG` as a binding key                                  | ✗ — no such class ships, and nothing puts a `RECONCILIATION_CONFIG` key in any binding map | The binding key is the literal string `"config"`                                    |
| `org.openiam.idm.srvc.msg.service.MailService` and `mailService.sendEmails(from, String[] to, ...)` | ✗                                                                                          | `org.openiam.common.beans.mq.MailRabbitMQService`, method `sendEmailAsynchronously` |

The shipped sample is affected too: `recon/PrePostExecutorSample_1.0.groovy` compiles against the current classes, but it still reads `bindingMap.get("RECONCILIATION_CONFIG")`, which is always `null` on the live pre/post path — so it throws a `NullPointerException` on the next line. Change the key to `"config"` before using it, and see [what the live path actually injects](#what-a-processor-actually-receives).

`recon/GroupProcessor_1.0.groovy` does not compile at all — it imports `org.openiam.idm.srvc.recon.command.*` (package removed), `ManagedSysDozerConverter` and `ConnectorService`, none of which exist. Do not start from it.
{% endhint %}

## Which script goes in which field

Eight fields on the reconciliation form hold a script, and each one is an autocomplete over scripts already registered in the Groovy manager. Which column each field writes, and what reads it, is in [Reconciliation situations and responses](/administration-guide/reference/reconciliation-situations-and-responses.md#every-script-column). Two of them decide whether a run works at all: the target search query goes in **Custom Processor Script**, not in **Target system search query script**, and the population script goes on each situation row.

## How bindings reach a script

Two different mechanisms, and the difference matters.

**Class instantiation** — used for population, OpenIAM search, match rule, custom command and pre/post scripts. OpenIAM parses the script into a class, calls its no-arg constructor, then sets each binding as a **property** on the instance. A binding therefore only reaches your script if the script — or a base class it extends — declares a field with that **exact name**. A binding with no matching field raises `MissingFieldException`, which is caught, logged at debug level and then ignored: the run carries on with your field left null. Nothing tells you at runtime that you misspelled a field name, so when a field you declared is unexpectedly null, check the spelling against the binding table below before you look anywhere else. To see the swallowed failure, raise the reconciliation service log to debug — see the [Installation Guide](/installation/installation.md) for how to change a logger level.

The Spring `ApplicationContext` is always injected as `context`, whatever the binding map contains. Use `context.getBean(SomeService.class)` for anything not in the binding map; that is what every shipped script does.

**Script-body evaluation** — used for the target search query script only. OpenIAM evaluates the script body with the bindings as script variables, and the script's value is its last expression. The shipped convention `output = "..."` works because an assignment in Groovy evaluates to the assigned value — there is no magic in the name `output`.

## Population scripts

The one method:

{% code overflow="wrap" %}

```java
// import org.openiam.recon.service.impl.service.PopulationScript
public interface PopulationScript<T> extends ApplicationContextAware {
    int execute(Map<String, Attribute> line, T pUser, Map<String, Object> dataMap);
}
```

{% endcode %}

`AbstractPopulationScript<T>` implements it and adds `context` plus two helpers you can call — `addGroupAttribute(Group, String name, String value)` and `addUserMemberByLogin(ProvisionGroup, String login, Date startDate, Date endDate, String description, Set<String> accessRightIds)`.

The three arguments:

* **`line`** — the record from the managed system, `Map<String, Attribute>`. Keys are the attribute names the connector returned; for the target → OpenIAM pass this is `LineObject.getColumnMap()`, and for the OpenIAM → target pass it is built from the per-principal lookup response.
* **`pUser`** — the `ProvisionUser` (or `ProvisionGroup`) to fill in. For a matched record it is pre-loaded from OpenIAM, so you can compare old and new. For an unmatched target account it is a fresh object carrying only a `Login` for the managed system.
* **`dataMap`** — a scratch map shared by every record in the run, and **not cleared between runs**: it belongs to the processor, which lives for as long as the service does. It is handed to the post-processor as the `DATA_MAP` binding. Convenient for a counter that the post-processor reports; dangerous as per-run state, because values survive into the next run of any config on that instance.

**The return value is a per-record veto.** Every command checks it, including one you write yourself by way of the inherited `executeScript`:

| Return | What the command does                                                                                             |
| ------ | ----------------------------------------------------------------------------------------------------------------- |
| `0`    | Proceeds — saves, deletes or calls the connector as the response dictates.                                        |
| `> 0`  | Logs `Incorrect return code from population groovy script. User <principal> not processed` and does nothing else. |
| `< 0`  | Throws `Couldn't transform ProvisionUser. User <principal> not processed`; the record's child audit entry fails.  |

Returning `1` from a branch is the cleanest way to exclude records — safer than relying on the search filter, because it is per record and it is visible in the log.

### Reading a value out of an `Attribute`

`Attribute` — import `org.openiam.provision.type.Attribute` — carries `name`, `value`, `valueList`, `multiValued`, `type` and `columnNbr`, all readable as Groovy properties: `attr.value`, `attr.valueList`, `attr.multiValued`.

Which field holds the data depends on how many values the connector returned. A single value goes in `value` and leaves `valueList` null; two or more values go in `valueList` and set `multiValued` to true, leaving `value` null. So `attr.value` is null for genuinely multi-valued attributes, and `attr.valueList` is null for single-valued ones. Read both:

{% code overflow="wrap" expandable="true" %}

```groovy
import org.openiam.provision.type.Attribute

/** First (or only) value of a target attribute, or null. */
static String firstValue(Attribute a) {
    if (a == null) return null
    return a.multiValued ? (a.valueList ? a.valueList.get(0) : null) : a.value
}

/** Every value of a target attribute, never null. */
static List<String> allValues(Attribute a) {
    if (a == null) return []
    if (a.multiValued) return a.valueList ?: []
    return a.value != null ? [a.value] : []
}
```

{% endcode %}

The shipped scripts use the shorter `line.get("SamAccountName")?.value`. That is correct for single-valued attributes and silently returns null for multi-valued ones — which is exactly the bug behind "the target has three values for `memberOf` and my script sees nothing".

### A complete population script

Adapted from `recon/Ad_Powershell/PowershellADPopulationScript_1.0.groovy`, keeping its real class declaration and signature. Three deliberate changes from the shipped file, each explained below the code.

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
import org.openiam.base.AttributeOperationEnum
import org.openiam.base.ws.MatchType
import org.openiam.base.ws.SearchParam
import org.openiam.common.beans.mq.LoginMQService
import org.openiam.common.beans.mq.UserRabbitMQService
import org.openiam.dto.Address
import org.openiam.dto.Login
import org.openiam.dto.ReconciliationConfig
import org.openiam.dto.User
import org.openiam.dto.UserAttribute
import org.openiam.idm.searchbeans.LoginSearchBean
import org.openiam.idm.searchbeans.UserSearchBean
import org.openiam.idm.srvc.user.dto.UserStatusEnum
import org.openiam.mq.util.UnwrapUtils
import org.openiam.provision.dto.ProvisionUser
import org.openiam.provision.type.Attribute
import org.openiam.recon.service.impl.service.AbstractPopulationScript
import org.apache.commons.collections4.CollectionUtils

import java.text.SimpleDateFormat

class PowershellADPopulationScript extends AbstractPopulationScript<ProvisionUser> {

    /** Injected by name from the reconciliation binding map. */
    ReconciliationConfig config

    UserRabbitMQService userMng
    LoginMQService loginMQService

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd")

    @Override
    int execute(Map<String, Attribute> line, ProvisionUser pUser, Map<String, Object> dataMap) {
        int retval = 1

        if (userMng == null) userMng = context.getBean(UserRabbitMQService.class)
        if (loginMQService == null) loginMQService = context.getBean(LoginMQService.class)

        // AbstractPopulationScript.managedSysId is NOT populated on the reconciliation
        // path - take it from the injected config instead.
        String msysId = config?.managedSysId

        for (String key : line.keySet()) {
            // One declaration for the whole switch: Groovy, like Java, gives every
            // case in a switch the same variable scope.
            String v = firstValue(line.get(key))

            switch (key) {
                case "SamAccountName":
                    addAttribute(pUser, "sAMAccountName", v)
                    retval = 0
                    break
                case "EmployeeID":
                    if (pUser.employeeId != v) {
                        pUser.employeeId = v
                        retval = 0
                    }
                    addAttribute(pUser, "employeeID", v)
                    break
                case "GivenName":
                    if (pUser.firstName != v) {
                        pUser.firstName = v
                        retval = 0
                    }
                    addAttribute(pUser, "givenName", v)
                    break
                case "Surname":
                    if (pUser.lastName != v) {
                        pUser.lastName = v
                        retval = 0
                    }
                    addAttribute(pUser, "sn", v)
                    break
                case "Title":
                    if (pUser.title != v) {
                        pUser.title = v
                        retval = 0
                    }
                    addAttribute(pUser, "title", v)
                    break
                case "City":
                    insertPrimaryAddressItem(pUser, 'city', v)
                    addAttribute(pUser, "l", v)
                    break
                case "State":
                    insertPrimaryAddressItem(pUser, 'state', v)
                    addAttribute(pUser, "st", v)
                    break
                case "UserPrincipalName":
                    addAttribute(pUser, "userPrincipalName", v)
                    break
                case "DistinguishedName":
                    addAttribute(pUser, "distinguishedName", v)
                    break
                case "AccountExpires":
                    if (v) {
                        // Windows FILETIME: 100ns ticks since 1601-01-01
                        GregorianCalendar win32Epoch = new GregorianCalendar(1601, Calendar.JANUARY, 1)
                        long ms = ((v as Long) / 10000L as long) + win32Epoch.getTimeInMillis()
                        Date accountExpires = new Date(ms)
                        if (!accountExpires.after(dateFormat.parse('9999-12-31'))
                                && !pUser.lastDate?.equals(accountExpires)) {
                            pUser.lastDate = accountExpires
                        }
                    }
                    addAttribute(pUser, "accountExpires", v)
                    break
                case "Managers":
                    if (v && msysId) {
                        LoginSearchBean lsb = new LoginSearchBean()
                        lsb.setManagedSysId(msysId)
                        lsb.setLoginMatchToken(new SearchParam(v, MatchType.EXACT))
                        List<Login> logins = UnwrapUtils.unwrap(loginMQService.findBeans(lsb, 0, 1))
                        if (!CollectionUtils.isEmpty(logins)) {
                            Login managerLogin = logins.get(0)
                            if (!pUser.getSupervisors()?.find { it.getEntityId() == managerLogin.getUserId() }) {
                                pUser.getSupervisors()?.each { it.setOperation(AttributeOperationEnum.DELETE) }
                                UserSearchBean usb = new UserSearchBean()
                                usb.addKey(managerLogin.getUserId())
                                List<User> found = UnwrapUtils.unwrap(userMng.findBeans(usb, null, 0, 1))
                                if (!CollectionUtils.isEmpty(found)) {
                                    User superior = found.get(0)
                                    superior.operation = AttributeOperationEnum.ADD
                                    pUser.addSupervisor(superior, null)
                                }
                            }
                        }
                    }
                    addAttribute(pUser, "managers", v)
                    break
            }
        }

        // New users only: give them a status and a metadata type, or the save is rejected.
        if (!pUser.id) {
            pUser.status = UserStatusEnum.PENDING_INITIAL_LOGIN
        }
        if (!pUser.mdTypeId) {
            pUser.mdTypeId = "DEFAULT_USER"
        }
        return retval
    }

    static String firstValue(Attribute a) {
        if (a == null) return null
        return a.multiValued ? (a.valueList ? a.valueList.get(0) : null) : a.value
    }

    boolean insertPrimaryAddressItem(ProvisionUser pUser, String item, String value) {
        Address address = pUser.addresses?.find { Address a -> a.mdTypeId == 'PRIMARY_LOCATION' }
        def isNew = false
        if (!address) {
            isNew = true
            address = new Address()
            address.mdTypeId = 'PRIMARY_LOCATION'
            pUser.addresses.add(address)
        }
        if (address[item] != value) {
            if (address.operation == AttributeOperationEnum.NO_CHANGE) {
                address.operation = isNew ? AttributeOperationEnum.ADD : AttributeOperationEnum.REPLACE
            }
            address[item] = value
            return true
        }
        return false
    }

    def addAttribute(ProvisionUser pUser, String attributeName, String attributeValue) {
        UserAttribute existing = pUser.userAttributes.get(attributeName)
        if (existing == null) {
            UserAttribute userAttr = new UserAttribute(attributeName, attributeValue)
            userAttr.operation = AttributeOperationEnum.ADD
            pUser.userAttributes.put(attributeName, userAttr)
        } else if (existing.getFirstValue() != attributeValue) {
            existing.overwrite(attributeValue)
            existing.operation = AttributeOperationEnum.REPLACE
            pUser.userAttributes.put(attributeName, existing)
        }
    }
}
```

{% endcode %}

Three changes from the shipped file, all of them fixes:

1. **`managedSysId` comes from `config`, not from the base class.** `AbstractPopulationScript` declares a `managedSysId` field, but no `managedSysId` key is put in the population script's binding map, so on the reconciliation path that field is always null. The shipped `Managers` branch therefore searches logins with a null managed system id and finds nothing. Declaring a `ReconciliationConfig config` field picks up the `config` binding and gives you `config.managedSysId`.
2. **`addAttribute` no longer branches on the return value of `addValue`.** The shipped helper reads `if (userAttr.addValue(attributeValue))`, but `AbstractAttributeDTO.addValue` returns `void` — in Groovy that is `null`, so the `REPLACE` branch never executed and an already-present attribute was never updated. The version above compares `getFirstValue()` and calls `overwrite(value)`.
3. **Every read goes through `firstValue`,** so a multi-valued target attribute does not silently read as null.

The shipped `println(">>>>>>>>>>> " + key + " = " + line.get(key))` debug line is removed — it writes every attribute value of every reconciled account to stdout, including anything sensitive the policy map requests.

## Target search query scripts

Not a class — a script body evaluated with `ScriptIntegration.execute`. Its value must be a non-empty `String` or the run fails at stage 1 with `BasicDataServiceException(NOT_FOUND)`, "Search query not defined for this reconciliation config".

Bindings available to the target search query script:

| Binding        | Type                 | Value                                                                 |
| -------------- | -------------------- | --------------------------------------------------------------------- |
| `managedSysId` | `String`             | The config's managed system. The key is the literal `"managedSysId"`. |
| `searchFilter` | `String`             | `RECONCILIATION_CONFIG.TARGET_SYS_SEARCH_FILTER`                      |
| `lastExecTime` | `Date`               | `RECONCILIATION_CONFIG.LAST_EXEC_TIME`                                |
| `updatedSince` | `Date`               | `RECONCILIATION_CONFIG.UPDATED_SINCE`                                 |
| `context`      | `ApplicationContext` | always injected                                                       |

Because it is a script body, a binding that has not been set is *absent* rather than null, so guard with `binding.hasVariable(...)` as the shipped PowerShell example does:

{% code overflow="wrap" %}

```groovy
// recon/Ad_Powershell/PowershellADSearchQuery_1.0.groovy
def sf = binding.hasVariable("searchFilter") ? searchFilter : "{Name -like '*'}"
def us = binding.hasVariable("updatedSince") ? updatedSince : ""

output = "Get-ADUser -LDAPFilter '${sf}' -Properties *" as String
```

{% endcode %}

The simplest shipped form is one line — `recon/Ad_Powershell/AdUserSearchQuery_1.0.groovy` is just `output = searchFilter`, which passes the config's filter straight through. `AppTableSearchQuery_1.0.groovy` is `output = "*"` and `PeopleSoftSearchQuery_1.0.groovy` is `output = "WHERE OPRID LIKE '%'"` — the string is connector-specific, and OpenIAM does not inspect it.

Both shapes above stay valid under the sandbox that 2026.8.1 applies to the execution path: testing a binding, defaulting it, interpolating it and casting it are all fine. What no longer compiles is an untyped operation directly on the binding, because `searchFilter` reaches the script as `Object` — cast it, as the shipped examples do with `as String`. See [The sandbox on the execution path](/administration-guide/reference/groovy-script-reference.md#the-sandbox-on-the-execution-path).

{% hint style="warning" %}
This script goes in **Custom Processor Script**, not **Target system search query script** — see [the caution in Configure reconciliation](/administration-guide/lifecycle/configure-reconciliation.md#point-the-target-search-at-a-narrow-scope). A script placed in that field that also happens to declare a class implementing `ReconciliationProcessor` would replace the whole processor; a plain body like the ones above does not, and if you put one in **Custom Processor Script** expecting it to be treated as a query, the run fails with `The ReconciliationProcessor script '<id>' wasn't found` — a body with no class in it cannot be instantiated as a processor.
{% endhint %}

## OpenIAM search query scripts

Builds the search bean for the OpenIAM-side pass. Extend `AbstractIDMSearchScript`, which supplies the injected fields and no-op defaults:

{% code overflow="wrap" %}

```java
// import org.openiam.recon.service.impl.service.IDMSearchScript
public interface IDMSearchScript {
    UserSearchBean createUserSearchBean(Map<String, Object> bindingMap);
    GroupSearchBean createGroupSearchBean(Map<String, Object> bindingMap);
}
```

{% endcode %}

Injected fields declared on `AbstractIDMSearchScript`: `searchFilter` (from `RECONCILIATION_CONFIG.SEARCH_FILTER`), `managedSysId`, `updatedSince`, `context`. Read those, not the `bindingMap` argument — on a **group** config OpenIAM calls `createGroupSearchBean` with a fresh empty map, so anything you expect to find in `bindingMap` will not be there.

`recon/UserSearchScript_1.0.groovy` and `GroupSearchScript_1.0.groovy` parse `searchFilter` as JSON and set matching properties on the bean, which is why the shipped configs store values like `{"employeeId":"not_found"}` and `{"nameToken":{"value":"TEST","matchType":"STARTS_WITH"}}`. `recon/Ad_Powershell/IdmSearchScript_1.0.groovy` is the fullest example — it maps `employeeId`, `email`, `*Set` collections, `managedSysId` and `principal` keys onto a `UserSearchBean` with the right `SearchParam`/`MatchType`.

If the bean you return has a `principal` set, OpenIAM resolves it to logins first and searches on the resulting user ids instead, because a principal-only `UserSearchBean` is rejected for performance reasons. You do not have to do anything about this, but it explains why a run whose filter names a principal shows a user-id search in the reconciliation service log rather than the principal you set.

## Custom command scripts

Set on a situation row, in **Custom Command**. When present it replaces the built-in command for that situation and **the Response you selected is ignored**: OpenIAM checks the row for a custom command first and never consults the Response. Confirm which one ran from the child audit entry for the record — a custom command's effects appear there, and the effect the Response names does not.

Extend `BaseReconciliationUserCommand` or `BaseReconciliationGroupCommand` and implement:

{% code overflow="wrap" %}

```java
// import org.openiam.recon.service.intf.command.ReconciliationObjectCommand
void execute(AbstractPopulationScript<T> populationScript, String principal, String managedSysID,
             T object, Map<String, Attribute> attributes, String parentAuditLogId,
             Map<String, Object> dataMap) throws Exception;
```

{% endcode %}

The base class gives you `protected int executeScript(AbstractPopulationScript<T>, Map<String, Attribute>, T, Map<String, Object>)` so your command still runs the situation's population script and honours its return code.

Bindings: only `managedSysId` (put in by the factory) and `context`. Neither `BaseReconciliationUserCommand` nor `BaseReconciliationGroupCommand` declares a `context` field, so declare `protected ApplicationContext context` in your script — the four shipped `CreateIdmGroupCommand`/`CreateIdmGroupCustomCommand` samples all do, and `DoNothingGroupCommand_1.0.groovy` does not, which is why it never calls `context.getBean`.

Shipped examples, all `extends BaseReconciliationGroupCommand`: `recon/DoNothingGroupCommand_1.0.groovy`, `CreateIdmGroupCommand/CreateIdmGroupCommand_1.0.groovy`, `CreateIdmGroupCommand/UpdateIdmGroupCommand_1.0.groovy`, `CreateIdmGroupCustomCommand/CreateIdmGroupCustomCommand_1.0.groovy`, `CreateIdmGroupCustomCommand/UpdateIdmGroupCustomCommand_1.0.groovy`.

A custom command is the supported way to get the four responses group reconciliation does not implement — see [group reconciliation supports only seven responses](/administration-guide/reference/reconciliation-situations-and-responses.md#group-reconciliation-supports-only-seven-of-the-eleven-responses).

## Custom match rule scripts

Set in **Custom rule for matching** (`CUSTOM_IDENT_MATCH_SCRIPT_ID`). When present, OpenIAM instantiates your script as an implementation of `MatchObjectRule` — import `org.openiam.match.MatchObjectRule` — and uses it instead of the built-in rule for the config's `RECON_TYPE`. Four methods: `lookup(MatchConfig, List<LineObject>)`, `getMatchRegex()`, `getApi()`, `getDataResponseClassName()`.

It is instantiated with a **null** binding map, so `context` is the only injected value: everything else has to come from `context.getBean(...)`. No shipped example exists for this hook, and the built-in rules it replaces are not published as Groovy, so there is nothing to clone — this is the one reconciliation hook you write from the interface alone. If you need to see how the built-in user rule behaves before replacing it, raise a support request rather than guessing.

## Pre-processor and post-processor scripts

The real interface and base class:

{% code overflow="wrap" %}

```java
// import org.openiam.recon.service.intf.PrePostExecutor
public interface PrePostExecutor extends ApplicationContextAware {
    int execute(Map<String, Object> bindingMap);
}

// import org.openiam.recon.service.impl.service.AbstractPrePostExecutor
public abstract class AbstractPrePostExecutor implements PrePostExecutor {
    protected ApplicationContext context;
}
```

{% endcode %}

The pre-processor runs once when the run starts, before the target search request is sent. The post-processor runs once after the last part is processed and before the report file is written. In both cases OpenIAM instantiates the script and calls `execute(bindingMap)` with the same map it used for instantiation, and **discards the return value** — a non-zero return from a pre- or post-processor changes nothing, unlike the return from a population script.

**A pre- or post-processor that fails to compile is easy to miss.** If loading the script raises an `IOException`, a warning goes on the parent audit entry — `Script <id> wasn't not executed due to error <e>` — and the run continues. Any other failure to instantiate is swallowed: OpenIAM skips the script and puts **nothing on the audit entry at all**, so the run looks clean. The only trace is the reconciliation service log, where the line beginning `Script name :` is followed by the compilation failure. So after you attach a pre- or post-processor for the first time, do not treat a successful run as proof it ran — make the script write something you can see, such as a mail or a counter in `dataMap` reported by the post-processor, and confirm that instead.

An exception thrown *inside* `execute` behaves differently, and only for the pre-processor: it fails the whole run, and `EXEC_STATUS` on the config becomes `failed`. A post-processor that throws leaves the run's status alone.

### What a processor actually receives

{% hint style="warning" %}
Two binding maps exist and only one of them is live for pre- and post-processors.

The rich map documented below — the caches, the ESB services, the pre-populated managed-system maps — is **not** what a pre- or post-processor receives in this build, even though older material and the shipped sample assume it is. What the live path passes is:

* **pre-processor:** `new HashMap<>()` — completely empty.
* **post-processor:** a single key, `DATA_MAP`, holding the processor's `dataMap` .

So a pre/post script in this release should take everything from `context.getBean(...)` and, for the post-processor, from `bindingMap.get("DATA_MAP")`. Do not rely on any other key. There is no screen that shows you which keys arrived; the cheapest check is a first version of the script that logs `bindingMap.keySet()` through a mail or an exception you can see, run once, then removed.
{% endhint %}

The rich map **is** live for population scripts. Every key it contains:

| Binding                          | Type                                                           | Purpose                                                                                                                                            |
| -------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config`                         | `org.openiam.dto.ReconciliationConfig`                         | The configuration being run — the only reliable source of `managedSysId`, `processRule`, the filters and `notificationEmailAddress`.               |
| `reconConfigId`                  | `String`                                                       | `config.getId()`, for logging and for keying anything you store.                                                                                   |
| `rolesByName`                    | `HashMap<String, Role>`                                        | Empty on entry. A per-run cache for your script to fill, so a lookup happens once rather than per record.                                          |
| `groupsByName`                   | `HashMap<String, Group>`                                       | Empty cache, same purpose.                                                                                                                         |
| `resourcesByName`                | `HashMap<String, Resource>`                                    | Empty cache.                                                                                                                                       |
| `usersByLogin`                   | `HashMap<String, User>`                                        | Empty cache.                                                                                                                                       |
| `organizationByName`             | `HashMap<String, Organization>`                                | Empty cache.                                                                                                                                       |
| `metadataTypesByGroupingAndName` | `EnumMap<MetadataTypeGrouping, HashMap<String, MetadataType>>` | Empty cache for metadata types, keyed by grouping then name.                                                                                       |
| `rolesById`                      | `HashMap<String, Role>`                                        | Empty cache.                                                                                                                                       |
| `groupsById`                     | `HashMap<String, Group>`                                       | Empty cache.                                                                                                                                       |
| `resourcesById`                  | `HashMap<String, Resource>`                                    | Empty cache.                                                                                                                                       |
| `usersById`                      | `HashMap<String, User>`                                        | Empty cache.                                                                                                                                       |
| `organizationById`               | `HashMap<String, Organization>`                                | Empty cache.                                                                                                                                       |
| `roleRabbitMQService`            | `RoleRabbitMQService`                                          | Role lookups and saves over the ESB.                                                                                                               |
| `groupRabbitMQService`           | `GroupRabbitMQService`                                         | Group lookups and saves.                                                                                                                           |
| `userRabbitMQService`            | `UserRabbitMQService`                                          | User lookups and saves. Matches the field of the same name on `AbstractPopulationScript`, so it arrives without you declaring anything.            |
| `resourceRabbitMQService`        | `ResourceRabbitMQService`                                      | Resource lookups. Note the name — `AbstractPopulationScript`'s field is `resourceMQService`, which does not match and is therefore never injected. |
| `resourceTypeMQService`          | `ResourceTypeMQService`                                        | Resource type lookups.                                                                                                                             |
| `organizationRabbitMQService`    | `OrganizationRabbitMQService`                                  | Organization lookups.                                                                                                                              |
| `mailRabbitMQService`            | `MailRabbitMQService`                                          | Sending mail. This is the replacement for the `MailService` the old docs name.                                                                     |
| `counterRabbitMQService`         | `CounterRabbitMQService`                                       | Sequence/counter values, for generating principals.                                                                                                |
| `metadataTypeRabbitMQService`    | `MetadataTypeRabbitMQService`                                  | Metadata type lookups.                                                                                                                             |
| `managedSystemByName`            | `Map<String, ManagedSysDto>`                                   | **Pre-populated** with every managed system, keyed by `getName()`.                                                                                 |
| `managedSystemById`              | `Map<String, ManagedSysDto>`                                   | Pre-populated, keyed by id.                                                                                                                        |
| `resourceTypeByName`             | `Map<String, ResourceType>`                                    | Pre-populated — but keyed by `getDescription()`, not by name.                                                                                      |
| `resourceTypeById`               | `Map<String, ResourceType>`                                    | Pre-populated, keyed by id.                                                                                                                        |
| `context`                        | `ApplicationContext`                                           | Always injected, whatever else the map holds.                                                                                                      |

Remember the rule from [how bindings reach a script](#how-bindings-reach-a-script): each of these only arrives if your script declares a field with that exact name. The empty caches and the two pre-populated managed-system maps are the ones worth declaring — building a `managedSystemByName` lookup yourself costs one ESB round trip per record.

### A working post-processor

Corrected from `recon/PrePostExecutorSample_1.0.groovy`: real base class, real mail service, and the `DATA_MAP` key that the live path actually provides.

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
import org.openiam.common.beans.mq.MailRabbitMQService
import org.openiam.recon.service.impl.service.AbstractPrePostExecutor

class ReconCompletionNotifier extends AbstractPrePostExecutor {

    @Override
    int execute(Map<String, Object> bindingMap) {
        Map<String, Object> dataMap = (Map<String, Object>) bindingMap.get("DATA_MAP")

        StringBuilder message = new StringBuilder()
        message.append("Reconciliation finished.\n")
        if (dataMap != null) {
            dataMap.each { k, v -> message.append("${k} = ${v}\n") }
        }

        MailRabbitMQService mailService = context.getBean(MailRabbitMQService.class)
        mailService.sendEmailAsynchronously("<RECON_NOTIFY_ADDRESS>",
                "Reconciliation completed", message.toString(), false)
        return 0
    }
}
```

{% endcode %}

`sendEmailAsynchronously(String emailAddress, String subject, String text, boolean format)` is one of four `sendEmailAsynchronously` overloads on `MailRabbitMQService`; the six-argument `sendEmailAsynchronously(List<String> emailAddresses, String subject, String text, boolean format, List<String> attachments, String parentAuditLogId)` is the one OpenIAM itself uses to send the completion report with the CSV attached.

You rarely need this script at all: filling **Email Address for Completion Notification** on the config emails the report CSV without any Groovy. Use a post-processor for something the built-in mail does not do — writing to an external ticket system, or reporting counters your population scripts accumulated in `dataMap`.

## Everything shipped under `recon/`

41 scripts in the root plus nine per-target directories. The full inventory is in [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md).

**Root — generic examples:**

| Script                             | Kind                                                                                                                                                                                                                                                     |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BlankSituationScript_1.0.groovy`  | Population script skeleton — `extends AbstractPopulationScript<ProvisionUser>`, returns `1`, so it is a no-op that never lets the command act. The shortest correct statement of the signature, and the right file to clone when you start from nothing. |
| `UserSearchScript_1.0.groovy`      | OpenIAM search query script, `extends AbstractIDMSearchScript`, JSON `searchFilter` → `UserSearchBean`                                                                                                                                                   |
| `GroupSearchScript_1.0.groovy`     | The same for `GroupSearchBean`, and it honours `updatedSince`                                                                                                                                                                                            |
| `AppTableSearchQuery_1.0.groovy`   | Target query script: `output = "*"`                                                                                                                                                                                                                      |
| `PeopleSoftSearchQuery_1.0.groovy` | Target query script: `output = "WHERE OPRID LIKE '%'"`                                                                                                                                                                                                   |
| `DoNothingGroupCommand_1.0.groovy` | Custom command, `extends BaseReconciliationGroupCommand`                                                                                                                                                                                                 |
| `PrePostExecutorSample_1.0.groovy` | Pre/post-processor, `extends AbstractPrePostExecutor` — correct base class, wrong binding key (see the danger note above)                                                                                                                                |
| `GroupProcessor_1.0.groovy`        | **Does not compile.** Imports removed packages.                                                                                                                                                                                                          |

The nine per-target directories — `AD/`, `Ad_Powershell/`, `ldap/`, `Linux/`, `Oracle/`, `Salesforce/`, `google/`, `CreateIdmGroupCommand/` and `CreateIdmGroupCustomCommand/` — each hold a population script and one or more query scripts for that target; the file names are listed in [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md).

Every population script in every one of those directories uses the three-argument `int execute(Map<String, Attribute> line, <ProvisionUser|ProvisionGroup> p, Map<String, Object> dataMap)` signature. If a script you are given uses two arguments, it predates this release.

## Verification

A script change is visible in three places:

* **Compilation.** The Groovy manager compiles on save; a failure there is the cheapest one to find. See [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md).
* **The per-record audit entry.** Each reconciled record gets a `RECONCILE_TARGET_WITH_IDM` or `RECONCILE_IDM_WITH_TARGET` child entry with the situation name in `INFO` and the target attribute map as the request body. A script that threw shows the exception on that entry: a population script failure fails that one record's child entry and lets the run continue, so a run that finishes successfully can still have failed on every record. Read the child entries, not the parent status.
* **The report CSV.** Run with all four situations on `NOTHING` and read the `TARGET_VALUE_*` versus `IDM_VALUE_*` columns — that is your script's mapping, without any writes. See [the worked example](/administration-guide/lifecycle/configure-reconciliation.md#worked-example-a-first-reconciliation-against-a-test-ou).

An instantiation failure is quieter than a compilation failure. It is logged in the reconciliation service log on a line beginning `Script name :`, and nowhere else. For a population script the symptom on screen is "Population script wasn't set for situation `<SITUATION>`" on every record, which is misleading — the script *is* set, it just could not be instantiated. Search the reconciliation service log for the script id to see the real cause.

## Related

* [Which log answers which question](/administration-guide/operations/which-log-to-read.md#raising-the-detail-level) — the logger categories that unlock a diagnostic line, and which log it lands in
* [Configure reconciliation](/administration-guide/lifecycle/configure-reconciliation.md) — the situation matrix, the response semantics, and which field each of these scripts belongs in.
* [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md) — the full inventory, and the difference between a path reference and an ID reference on upgrade.
* [Build a policy map](/administration-guide/lifecycle/policy-map.md) — the attribute rows that decide which keys appear in `line`.
* [Enumerated values](/administration-guide/reference/enums.md#reconciliation-responses) — the response constants a custom command replaces.
* [Change configuration safely](/administration-guide/start/change-safely.md) — test a script change with `NOTHING` before it can write.

*Applies to OpenIAM 2026.8.1. Last reviewed 28 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/reconciliation-scripts.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.
