> 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/developer-guide/policy-map-scripts.md).

# Policy map scripts

Control whether an attribute is written to a managed system and what value it carries — isPerform, perform, the diff object, and an annotated shipped example.

A policy map script decides **whether** an attribute is written to a managed system, and **what value** it carries. One script per attribute, per managed system.

**This is the type you will write most.** Across the scripts shipped with the platform, `AbstractIPolicyMapGroovy` accounts for more than five times the next most common base class. If you learn one type properly, learn this one.

## What this builds, and how long it takes

|                |                                                                                   |
| -------------- | --------------------------------------------------------------------------------- |
| **You get**    | A script controlling one attribute on one managed system                          |
| **Time**       | An hour for a first script, including two provisioning runs                       |
| **You need**   | A deployment, a configured managed system, and an account you can provision to it |
| **Reversible** | Yes — increment the version, or unbind the script from the policy map             |

## Before you start

* [*Set up your environment*](/developer-guide/set-up-your-environment.md) and [*How scripting works*](/developer-guide/how-scripting-works.md).
* **A test managed system.** Provisioning a wrong value to a production directory is not something a script version helps with.
* **A decision about who owns this attribute after the account is created** — OpenIAM, or the target. That decision is what the script expresses.

***

## Build it

### 1. Understand the two methods

```groovy
abstract boolean isPerform(DIFF diffObject)
abstract void    perform(ConnectorAttribute attribute, DIFF diffObject)
```

**`isPerform` is a gate.** Return `false` and the attribute is not written at all. "Only on create", "only when the department changed" and "never for contractors" live here.

**`perform` supplies the value.** It is only called when `isPerform` returned `true`.

**Splitting them is the point.** Not writing an attribute and writing it empty are different things to a target system — one leaves the existing value alone, the other clears it.

### 2. Know what the diff object tells you

The type parameter is the diff:

| Type                       | For                |
| -------------------------- | ------------------ |
| `ProvisionUserObjectDiff`  | User provisioning  |
| `ProvisionGroupObjectDiff` | Group provisioning |

| Member                      | Gives you                                                             |
| --------------------------- | --------------------------------------------------------------------- |
| `getStatus()`               | An `AttributeOperationEnum` — `ADD`, `REPLACE`, `DELETE`, `NO_CHANGE` |
| `getLeft()`                 | The object being provisioned                                          |
| `userRoleDiffs` and similar | Per-membership diffs, each answering `isDirty()`                      |

**`getStatus()` is how you distinguish a create from an update.** `AttributeOperationEnum.ADD` is a new account.

### 3. Use what the base class injects

`AbstractIPolicyMapGroovy` provides these as protected fields — no `@Autowired` needed:

| Field                    | Is                                      |
| ------------------------ | --------------------------------------- |
| `ctx`                    | The Spring `ApplicationContext`         |
| `managedSysId`           | The managed system being provisioned to |
| `loginMQService`         | Login lookups                           |
| `userMQService`          | User lookups                            |
| `propertyValueService`   | Cached property values                  |
| `counterRabbitMQService` | Counters, for generated values          |
| `mapper`                 | A configured Jackson mapper             |

### 4. Write it

{% code overflow="wrap" %}

```groovy
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.openiam.api.connector.groovy.AbstractIPolicyMapGroovy
import org.openiam.api.connector.model.ConnectorAttribute
import org.openiam.api.connector.model.StringOperationalConnectorValue
import org.openiam.base.AttributeOperationEnum
import org.openiam.idm.provisioning.diff.model.user.ProvisionUserObjectDiff

class Title extends AbstractIPolicyMapGroovy<ProvisionUserObjectDiff> {

    private static final Log log = LogFactory.getLog("Title")

    @Override
    boolean isPerform(ProvisionUserObjectDiff diffObject) {
        return diffObject.getLeft()?.title
    }

    @Override
    void perform(ConnectorAttribute attribute, ProvisionUserObjectDiff diffObject) {
        attribute.addValue(
            new StringOperationalConnectorValue(diffObject.getLeft().getTitle(), diffObject.getStatus()))
    }
}
```

{% endcode %}

**`StringOperationalConnectorValue` carries the operation as well as the value.** Passing `diffObject.getStatus()` is what tells the connector whether this is an add or a replace. Get it wrong and a value that should be replaced is appended instead.

### 5. Read a shipped example

`conf/iamscripts/provision/ad/group/Description_1.0.groovy`, complete:

{% code overflow="wrap" %}

```groovy
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.openiam.api.connector.groovy.AbstractIPolicyMapGroovy
import org.openiam.api.connector.model.ConnectorAttribute
import org.openiam.api.connector.model.StringOperationalConnectorValue
import org.openiam.base.AttributeOperationEnum
import org.openiam.idm.provisioning.diff.model.group.ProvisionGroupObjectDiff

class Description extends AbstractIPolicyMapGroovy<ProvisionGroupObjectDiff> {

    private static final Log log = LogFactory.getLog("Description")

    @Override
    boolean isPerform(ProvisionGroupObjectDiff diffObject) {
        return AttributeOperationEnum.ADD.equals(diffObject.getStatus())
    }

    @Override
    void perform(ConnectorAttribute attribute, ProvisionGroupObjectDiff diffObject) {
        attribute.addValue(
            new StringOperationalConnectorValue(diffObject.getLeft().getDescription(), diffObject.getStatus()))
    }
}
```

{% endcode %}

**Twenty-three lines, and every one earns its place.** Read `isPerform`: this script writes the description **only when the group is created**. On an update it returns `false`, so whatever the directory holds is left alone. That is a deliberate decision — the directory is treated as authoritative for the description after creation — and it is expressed in one line.

**That is the pattern to copy.** Decide who owns each attribute after creation, and say so in `isPerform`.

### 6. Write only when something changed

For an attribute that should follow OpenIAM on every change, gate on the diff rather than the operation:

```groovy
@Override
boolean isPerform(ProvisionUserObjectDiff diffObject) {
    if (AttributeOperationEnum.ADD.equals(diffObject.getStatus())) {
        return true
    }
    return diffObject.userRoleDiffs.any({ it.isDirty() })
}
```

From `conf/iamscripts/provision/test-h2/memberOf_1.0.groovy`. **Always write on create; on update, write only if something in the relevant collection actually moved.** Without that second line, every provisioning event rewrites the whole membership set.

### 7. Deploy it

1. Groovy Manager → the folder under `provision/` for your managed system.
2. **New Script**, named for the attribute it maps — the shipped scripts use the target's attribute name: `cn`, `memberOf`, `userPrincipalName`.
3. **Save.** It compiles on save.
4. Reference it from the managed system's policy map, against the attribute.

***

## Verify it

**Provision one account to a test managed system, then change one attribute and provision again.** The two runs together tell you what one cannot.

| Check                       | Expect                                                   |
| --------------------------- | -------------------------------------------------------- |
| Create                      | The attribute present in the target with the right value |
| Update, attribute unchanged | `isPerform` returned `false` — nothing written           |
| Update, attribute changed   | The value **replaced**, not appended                     |
| The connector log           | The attribute and operation your script produced         |

## If it goes wrong

| What you see                                 | What it means                              | What to do                                                           |
| -------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------- |
| The attribute is never written               | `isPerform` returns `false`                | Log `diffObject.getStatus()` at the top of it                        |
| The attribute is written on every event      | `isPerform` returns `true` unconditionally | Gate on the status, or on `isDirty()`                                |
| Values accumulate instead of replacing       | The operation is not passed with the value | Pass `diffObject.getStatus()` into `StringOperationalConnectorValue` |
| `NullPointerException` in `perform`          | The source field is null                   | Guard in `isPerform` — that is what it is for                        |
| The value is right but the target rejects it | Format the target expects                  | Compare against the shipped script for that target                   |
| A membership set is rewritten every time     | No `isDirty()` check                       | Add the diff gate in step 6                                          |

## What this procedure does not cover

Configuring the managed system and its policy map, which is the [Administration Guide](/administration-guide/admin.md)'s subject. Connector behaviour, and what a specific target does with a value once it receives it.

## Related

* [*Validation and attribute scripts*](/developer-guide/validation-and-attribute-scripts.md) — principal generators use this same shape
* [*How scripting works*](/developer-guide/how-scripting-works.md)

*Applies to OpenIAM 2026.8.1.*


---

# 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/developer-guide/policy-map-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.
