> 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/automation/manage-groovy-scripts.md).

# Create a Groovy script

Create, compile, version, test, promote and roll back a Groovy script in Groovy Manager, and control which version each caller actually runs.

## What this is, and when you need it

Groovy Manager is the editor and version store for every script in the installation. It does **not** decide when a script runs; the referencing configuration does. So safe administration is two independent things: controlling the script *version*, and controlling the *reference* to it. Most "my change had no effect" and "my script changed by itself" tickets are reference problems, not script problems.

Open it at **Administration → Groovy Manager**, route `/webconsole/groovyManager`. The left pane is a folder tree over the script path; the right pane is a tabbed editor. The toolbar is six icons: refresh, new script, **Compile All Groovy Scripts**, **Upload Groovy Scripts (.zip)**, **Export All Groovy Scripts** and **Export Selected Groovy Scripts**.

Where a script is stored, every column a reference can live in, which services a script may inject, and the sandbox allow-list are in [Groovy script reference](/administration-guide/reference/groovy-script-reference.md).

## At a glance

|                          |                                                                                                                                                              |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **You will end up with** | A compiled, versioned script saved under a path you own, attached to one disposable caller and proven by a bounded run — with the shipped original untouched |
| **Where**                | `/webconsole/groovyManager` — **Administration → Groovy Manager**                                                                                            |
| **You need first**       | The extension point you are writing for, the shipped script for it to copy the class declaration from, and a disposable caller to attach the new version to  |
| **Takes**                | 15 minutes to create and save; a bounded test run takes as long as the caller does                                                                           |
| **Reversible**           | Yes — re-point the owning configuration at the previous version. But anything the new version already wrote to OpenIAM or to a target stays written          |

## Before you begin

* **The extension point, decided.** The base class, the bindings and the return contract differ completely between a synchronization transform, a policy-map attribute, a reconciliation command, a connector handler and a batch body.
* **The shipped script for that extension point**, open in Groovy Manager. Its class declaration is authoritative in a way no documented snippet is — see [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md).
* **A disposable caller** to attach the new version to: a synchronization config over a two-row CSV, a batch task that is saved but not enabled, a reconciliation situation whose response is `NOTHING`, a policy-map row on an inactive managed system. Never a production config.
* **A path outside the shipped tree** — for example `customer/sync/user/workday` — so an upgrade that refreshes the shipped scripts cannot collide with your file.

## Create a script

{% stepper %}
{% step %}

### Pick the contract, then the shipped example

Open the shipped script for that extension point in Groovy Manager and copy its class declaration exactly.
{% endstep %}

{% step %}

### Create the script

Click the **+** (**Create New Groovy Script**) toolbar button and paste the source into the editor.
{% endstep %}

{% step %}

### Save it with a path, name and version

The save dialog has exactly four fields — **Path**, **Name**, **Script Version** and **Description**. Use a path outside the shipped tree, for example `customer/sync/user/workday`, so an upgrade that refreshes the shipped scripts cannot collide with your file. Name is the class or purpose name **without** the `.groovy` suffix. Version defaults to `1.0` for a new script.

On an existing script, Path, Name and Version are all read-only; only **Description** and the body can change. To change a path or a name you create a new script, which means a new id — and therefore a reference every caller has to be re-pointed at.
{% endstep %}

{% step %}

### Read the compile result

Saving compiles first and stores second, so **a script that fails to compile is never stored** — there is no half-saved state to clean up, and nothing for a caller to pick up by accident.

Success is `Groovy script saved successfully` (message key `openiam.ui.webconsole.groovy.script.save.success`) and is audited as `SAVE_GROOVY`; the row's `LAST_MODIFIED` advances at the same time, which is the check that a save you were unsure about really landed. A compile failure returns HTTP 500 with `COMPILATION_ERROR` and the full stack trace in the response, shown by the warning icon on the tab (**Compilation Errors**); the equivalent service-side code is `CHECK_GROOVY_EXCEPTION`. Any other failure gives `Unable to save the Groovy script` (`openiam.ui.groovy.manager.save.error`) — which is a save problem, not a code problem, and worth distinguishing before you start editing the script.
{% endstep %}
{% endstepper %}

The portable name for a script version is `<path>/<name>_<version>.groovy`, for example `customer/sync/user/workday/WorkerTransform_1.0.groovy`. That is the form used in an export archive and in a path-style reference, so it is the string to quote when you are talking to anyone else about a specific version.

## How a caller finds your script

A reference to a script is resolved at run time from up to four things: an **id**, a **path**, a **name** and a **version**. Which of those the owning configuration actually stored is the whole version-pinning story, and it is not visible on the Groovy Manager screen — you find it by opening the owning object, not the script.

An id wins whenever one is stored, and it identifies exactly one version. With no id, a path and name without a version resolve to the **highest** version under that path and name. A path, name and explicit version resolve to that version — but only in the back-end services: a reference resolved by a UI service ignores the version it was given and takes the highest anyway, so a version pinned that way is not pinned at all.

Almost everything stores an id: synchronization configurations, reconciliation configurations, batch tasks, policy-map rows, questionnaires, authentication providers and business rules. The exception is the workflow service, which accepts either form and decides by inspecting the value — a 32-character id is treated as an id, anything else is read as `<path>/<name>_<version>.groovy`. That is why the eleven `Workflow`-category script properties hold paths like `bpm/CustomNotificationDelegate.groovy` while everything else holds an id. Read the stored value on the owning object and you know which behaviour you have.

### Version pinning: what each reference form does

| Reference stored by the caller               | Runtime selection                   | Administrative consequence                                                                                                                                                                                                                                      |
| -------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Script **ID** (a UUID in `GROOVY_SCRIPT_ID`) | Exactly that row                    | **Pinned.** Batch tasks, sync configs, recon configs, policy-map rows, questionnaires, auth providers and business rules all store an ID. Creating a higher version does not move the reference — you must reopen the owning object and select the new version. |
| Path + name + explicit version               | Exactly that path, name and version | Pinned when a back-end service resolves it. **Not pinned when a UI service resolves it** — the version is ignored and the highest wins.                                                                                                                         |
| Path + name, no version                      | Highest version for that path/name  | **Floating.** A higher saved version becomes active after the cache expires, with no change to the owning configuration.                                                                                                                                        |

`org.openiam.groovy.cache.ttl.ms` (**60000**) is how long a resolved body stays cached, so a floating reference switches versions up to a minute after you save.

{% hint style="warning" %}
A script that "changed by itself" after an upgrade is almost always a floating path reference. A script change that "had no effect" is almost always a pinned ID reference. Decide which you want per attachment point, and record it.
{% endhint %}

## Version with Branch

**Branch off of this version** (the tree icon) is how you make a new version. It offers the highest existing version for that path and name plus `0.1`, and opens a new tab with **Script Version** editable above that floor — so you can jump to `2.0` for a substantial change, but you cannot reuse or go below a version that already exists.

Three buttons are conditionally disabled, and the conditions are worth knowing before you wonder why one is greyed out:

| Button | Disabled when                                       | Tooltip you see instead                                                                                         |
| ------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Save   | the open tab is not the latest version              | *You cannot save an older version of a script. Please create a new script or branch off of this version.*       |
| Branch | not the latest version, or the script is unsaved    | —                                                                                                               |
| Delete | not the latest version, or **any reference exists** | *This Groovy script is referenced by one or more objects. Resolve that dependency before deleting this script.* |

Before you branch, open **Show References** and decide whether every caller moves together. Because almost every caller pins an ID, saving `1.1` changes nothing until you reopen each owning object and select it. That is the property that makes rollback trivial — and the property that makes people think their change did not deploy.

Do not delete the older version until every reference has moved, the observation period has passed, and rollback no longer needs it.

## Compile the whole catalog

**Compile All Groovy Scripts** compiles the **latest version of every script, plus every version that is referenced by something**, each one in whichever service would actually run it. Success is `All Groovy scripts compiled successfully`; failures come back as a per-script stack trace under **Compilation Errors**.

Run it after every import and before every release. Then understand its limit, because it is easy to read more into a green result than is there: it proves syntax and sandbox compliance, and nothing else. It does **not** prove the class implements the contract its caller expects — that shows up only at run time, as `CANNOT_INSTANTIATE_GROOVY_CLASS`. It also does not prove a connector-bound script will compile where it runs, for the reason in [Which services a script can inject](/administration-guide/reference/groovy-script-reference.md#which-services-a-script-can-inject-by-script-type).

## Test a script

There is no test harness in the Webconsole. Testing means attaching the version to one bounded caller and running it.

{% stepper %}
{% step %}

### Attach it to a disposable caller

A synchronization config over a two-row CSV; a batch task that is saved but not enabled; a reconciliation situation whose response is `NOTHING`; a policy-map row on an inactive managed system. Never a production config.
{% endstep %}

{% step %}

### Wait out or force the cache

`org.openiam.groovy.cache.ttl.ms` is **60000**, and the class cache is keyed on the script URL, so a freshly saved body can take up to a minute to be picked up by a service that has already run it. `org.openiam.groovy.script.file.local.cache.sweep` is **30000** for the body-to-filesystem map in groovy-manager. If a test shows the old behaviour, wait a minute before you change anything.
{% endstep %}

{% step %}

### Run one record and read the far end

Run the caller once and check the *outcome*, not the log line: the target object, the audit event, the synchronization or reconciliation report. See [Diagnose a failed operation](/administration-guide/operations/diagnose-a-failed-operation.md).
{% endstep %}

{% step %}

### Run the negative cases

A missing attribute, a null, a value that must be rejected, and the same input twice. The negative test is the one that finds the mistake; the repeat test is the one that finds the non-idempotency.
{% endstep %}
{% endstepper %}

## The failure you will actually see

When a script exists and compiles but cannot be created as the type its caller expects — wrong base class, no no-argument constructor, an exception thrown while constructing — OpenIAM logs the problem and the caller reports `CANNOT_INSTANTIATE_GROOVY_CLASS`. Every kind of caller behaves the same way, so the response code tells you nothing about which one failed; the log line does.

The log lines to search for name the script you asked for, so they carry either its id or its path:

* `Access problems when instantiating class <token>`
* `Instantiation problems when instantiating class <token>`
* `Target invocation problems when instantiating class <token>`
* `Script name :<token>` — the catch-all for any other throwable

Note the asymmetry, because it decides where you look. A **compile** failure is `CHECK_GROOVY_EXCEPTION` and you see it at save time, on screen. An **instantiate** failure is `CANNOT_INSTANTIATE_GROOVY_CLASS` and appears only at run time, in the log of whichever service ran the script. Compile All cannot catch the second, so a bounded test run is the only check that does.

## Import, export, promote

Promotion is a ZIP round-trip. Do not retype code in each environment.

**Export.** **Export All Groovy Scripts** streams `groovy-scripts.zip` containing the **latest version only** of every script — so it is not a complete backup of your version history, and it is not what you want as a rollback artifact for a specific version. **Export Selected Groovy Scripts** streams `groovy-scripts-selected.zip` at exactly the versions you selected in the tree, which is. Entries in both are named `<path>/<name>_<version>.groovy`. Both are audited as `EXPORT_GROOVY_SCRIPTS`, and export-all records `Exported <n> groovy scripts` — check that count against what you expected before you treat the file as complete.

**Import.** Upload one `.zip`; anything else is rejected with `INVALID_FILE_EXTENSION` and HTTP 400. The import is audited as `IMPORT_GROOVY_SCRIPTS`. Two rules decide whether an entry lands at all:

1. The filename must match `^(.+)_(\d+(?:\.\d+)?)\.groovy$`. Anything else is skipped and reported in the audit warning as `Invalid filename format (expected name_version.groovy): …`.
2. The entry must be **inside a folder**. A root-level `.groovy` entry is skipped with `Cannot create scripts at root level (must be inside a folder): …`.

Each incoming script is matched to an existing path, name and version, and reuses that script's id where all three match — so an import **updates** a matching script in place rather than creating a duplicate, and every existing reference keeps working. Change any of the three and you get a new script with a new id instead, which nothing points at. Scripts absent from the ZIP are never deleted.

{% hint style="warning" %}
A partial import returns HTTP 200 with the success message `Import completed. Some files were skipped. Please check audit logs for details.` (`openiam.ui.webconsole.groovy.script.import.partial.success`). It looks like a success. The list of skipped files exists **only** as a warning on the `IMPORT_GROOVY_SCRIPTS` audit record — read it every time.
{% endhint %}

Promotion gate, in order:

1. Export the change set from the source environment as a ZIP; keep it as the promotion artifact.
2. Export the **target's** current state first — that is your rollback artifact.
3. Import, and read the audit warning for skipped files.
4. Run **Compile All Groovy Scripts** in the target.
5. Confirm each owning configuration points at the intended version — the import does not retarget any reference, because references are IDs and IDs are preserved.
6. Run one bounded test per changed contract, including a negative case.

## Roll back

{% stepper %}
{% step %}

### Stop further execution

Disable the owning schedule or set the owning configuration inactive. A rollback that races a running synchronization is not a rollback.
{% endstep %}

{% step %}

### Re-point the reference, do not edit the script

Reopen the owning object and select the previous version, or restore the previous configuration export. Because references are pinned IDs, re-pointing is instant and complete.
{% endstep %}

{% step %}

### Run one bounded verification

Same input as the failing case, on a disposable object.
{% endstep %}

{% step %}

### Decide whether data must be remediated

The failed version may already have written to OpenIAM or to a target. Deleting the script does not undo that. Check the audit trail for what it did before you close the incident.
{% endstep %}
{% endstepper %}

Deleting the new script is not a rollback: the UI blocks deletion while any reference exists, and if the script already ran, its effects persist.

## Verification

| What you changed      | Evidence                                                                                         |
| --------------------- | ------------------------------------------------------------------------------------------------ |
| Saved a script        | `SAVE_GROOVY`; success token `GROOVY_SCRIPT_SAVED`; the row's `LAST_MODIFIED` advanced           |
| Imported a ZIP        | `IMPORT_GROOVY_SCRIPTS`, **including its warning list**                                          |
| Exported              | `EXPORT_GROOVY_SCRIPTS` with `Exported <n> groovy scripts`                                       |
| Compiled the catalog  | Success token `GROOVY_COMPILATION_SUCCESSFUL`                                                    |
| The migration ran     | `MIGRATE_GROOVY_SCRIPTS`; log line `Is groovy migrated? …`                                       |
| A workflow script ran | `ACTIVITI_GROOVY_SCRIPT`                                                                         |
| A batch script ran    | `BATCH_TASK_EXECUTE`, with the audit record's URL set to `groovy://<scriptId>`                   |
| The right version ran | Reopen the owning configuration and read back the selected script; do not infer it from the tree |

All five `*_GROOVY*` audit actions ship with retention `-1` (never prune) like everything else — see [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md).

## When it goes wrong

| Symptom                                                                                                                  | Cause to check                                                                                                               | Fix                                                                                                                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Save fails with a stack trace                                                                                            | Sandbox or syntax; `COMPILATION_ERROR` on screen, `CHECK_GROOVY_EXCEPTION` service-side                                      | Compare imports and the class declaration against the shipped script for the same contract                                                                                                                                                            |
| Save fails with `Method calls not allowed on` or `You tried to call a method which is not allowed`                       | The sandbox rejected a package. Those two strings are what a sandbox rejection always says                                   | Get the narrowest prefix added to `org.openiam.groovy.extra.whitelist.package` and the services restarted — deployment work. [The allow-list](/administration-guide/reference/groovy-script-reference.md#adding-a-package-to-the-sandbox)             |
| `unable to resolve class org.openiam.common.beans.mq.GroupRabbitMQService` — after the script saved and compiled cleanly | The script runs in the connector, which has no MQ client layer available to it                                               | Move the lookup to a policy-map or synchronization pre-processor script — [Which services a script can inject](/administration-guide/reference/groovy-script-reference.md#which-services-a-script-can-inject-by-script-type)                          |
| Compiles, then fails at run time                                                                                         | The class does not satisfy the contract its caller expects; the caller reports `CANNOT_INSTANTIATE_GROOVY_CLASS`             | Find the log line naming the script id or path, then fix the base class or the constructor                                                                                                                                                            |
| Compiles, then `ClassNotFoundException` in one service only                                                              | Which processes actually have the library, not just which have the allow-list entry                                          | Allow-listing a package does not install it. Every process that runs the script needs the library, and each replica counts — deployment work, [Installation Guide](/installation/installation.md)                                                     |
| New version has no effect                                                                                                | The caller pins an ID                                                                                                        | Reopen the owning configuration and select the new version                                                                                                                                                                                            |
| New version became active without being selected                                                                         | The caller stores path and name only, or a UI service resolved the reference and ignored the pinned version                  | Pin an id, and re-test after `org.openiam.groovy.cache.ttl.ms` (60 s)                                                                                                                                                                                 |
| Change had no effect for about a minute                                                                                  | `org.openiam.groovy.cache.ttl.ms` = 60000                                                                                    | Wait it out; do not save again                                                                                                                                                                                                                        |
| Save button greyed out                                                                                                   | The open tab is not the latest version                                                                                       | Branch, then save                                                                                                                                                                                                                                     |
| Delete button greyed out                                                                                                 | A reference exists, or the tab is not the latest version                                                                     | Move every caller off it; **Show References** lists them                                                                                                                                                                                              |
| **Show References** empty but the script is clearly in use                                                               | It is referenced by path from a `MANAGED_SYS.*_HNDLR` column or a `Workflow`-category property, and neither form is searched | Read the handler columns on the Groovy-connector managed systems, and the `Workflow` property values, before deleting — [the two invisible families](/administration-guide/reference/groovy-script-reference.md#every-place-a-script-can-be-attached) |
| Import reported success, script missing                                                                                  | Filename did not match `^(.+)_(\d+(?:\.\d+)?)\.groovy$`, or the entry was at the ZIP root                                    | Read the `IMPORT_GROOVY_SCRIPTS` audit warning; re-zip inside a folder with a `_version` suffix                                                                                                                                                       |
| Import created a duplicate                                                                                               | Path, name or version differs from the existing script, so nothing matched                                                   | Match all three exactly, or accept the new version and re-point every reference                                                                                                                                                                       |
| Batch task's script reference is null after upgrade                                                                      | The upgrade's path-to-id conversion found no `1.0` for that path and name, and wrote null                                    | Look for `Could not find groovy script:` in the service log, then re-select the script on the task                                                                                                                                                    |
| Script body correct in the editor, stale at run time                                                                     | The file and the row disagree, or the 30-second file sweep has not run                                                       | Re-save through the Webconsole; never edit a script file by hand                                                                                                                                                                                      |
| Scripts vanished after a restore                                                                                         | Only the `openiam` schema was restored — not the `groovy` schema, and not the script files                                   | Both have to be restored together, at the same point in time — [where a script lives](/administration-guide/reference/groovy-script-reference.md#where-a-script-actually-lives), then the [Installation Guide](/installation/installation.md)         |

## Related

* [Groovy script reference](/administration-guide/reference/groovy-script-reference.md) — where a script is stored, every column a reference can live in, which services each script family may inject, and the sandbox allow-list
* [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md) — all 745, by extension point
* [Edit a synchronization transformation script](/administration-guide/automation/synchronization-scripts.md) — the commonest reason to be here
* [Create a batch task](/administration-guide/automation/batch-tasks.md) — the script-versus-Spring-bean choice and the bindings
* [Promote configuration between environments](/administration-guide/platform/import-export-and-promotion.md) — moving the rest of the configuration with the scripts
* [Change configuration safely](/administration-guide/start/change-safely.md) — the change, test and rollback standard

*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/automation/manage-groovy-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.
