> 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/access/business-rules/business-rule-reference.md).

# Business rule reference

The business-rule object, every condition attribute and operator, what each of the 27 actions actually does, the Groovy contracts and six worked patterns.

## What this is, and when you need it

Business rules do far more than add a role. The engine can change identity status, add and remove role, group, resource and organization memberships, create access requests, end-date or delete a user's *entire* entitlement set, and run Groovy you supply. Several actions have a blast radius much larger than their label suggests, and several condition attributes compare something other than what you typed.

Use [Build a business rule](/administration-guide/access/business-rules.md) for the procedure. Use this page before you pick an attribute, an operator, a status or an action.

## The screens

**Access Control → Business Rules**, or `/webconsole/br`. Three tabs:

| Tab                   | What it holds                                                                                                                    |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Rules**             | The rules themselves: name, description, status, operation, condition tree, and which action group runs on match and on no match |
| **Action groups**     | Reusable named lists of actions — the targets a rule points at                                                                   |
| **Out of sync users** | The recalculation queue: users whose access no longer agrees with the rules                                                      |

`/webconsole/br/old` still serves the legacy version of the screen, which is the workaround for the two condition forms the current screen writes incorrectly. The **Edit** controls are gated by the authorization resource `BR_EDIT_BTN`: an administrator who can reach the screen but not that resource sees the rules read-only, which is a reasonable delegation for auditors. Menu resources: `BR_ROOT` at URL `/webconsole/br/`, left-menu root `BR_ROOT_LEFT` (name `BUSINESS_RULES_LM`, which is the value of `org.openiam.ui.business.rule.root.name`), search node `BR_LIST`.

Every control on those three tabs leaves a named audit trail, and that trail is the only reliable record of who changed a rule:

| What you do                                                                                   | Where                                                                         | Audit action                  |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------- |
| Save a rule, new or edited                                                                    | **Rules** → **New rule**, or the **Edit rule** dialog on a row                | `BUSINESS_RULE_SAVE`          |
| Delete a rule                                                                                 | **Rules** → **Delete rule** on a row                                          | `BUSINESS_RULE_DELETE`        |
| Save an action group                                                                          | **Action groups** → **New action group**, or the **Edit action group** dialog | `BUSINESS_RULE_TARGET_SAVE`   |
| Delete an action group                                                                        | **Action groups** → **Delete action group** on a row                          | `BUSINESS_RULE_TARGET_DELETE` |
| Add or change an action inside a group                                                        | The group's **Actions** list                                                  | `BUSINESS_RULE_ACTION_SAVE`   |
| Remove an action from a group                                                                 | The same list                                                                 | `BUSINESS_RULE_ACTION_DELETE` |
| **Preview impacted users** — evaluate every user against the current rules and fill the queue | **Out of sync users**                                                         | —                             |
| **Provision impacted users** — apply the queue                                                | **Out of sync users**                                                         | `BUSINESS_RULE_RECALCULATE`   |

Only the group's own type limits which action types you can add to it — a `PROVISIONING` group and a `SELF_REGISTRATION` group are offered different lists.

The legacy screen labels the same two buttons **Preview Affected Users** and **Provision Affected Users**. Preview warns first, and the warning is worth reading before you confirm: *"This process will evaluate all users against the updated business rules and display results in the 'Out of Sync Users' table. Users updated after this check will be removed from the table automatically."*

## The rule object

A rule is six fields plus a condition, stored in `BUSINESS_RULE`:

| Screen field                                                 | Column         | Values                                                                                                                                                                         |
| ------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name**                                                     | `NAME`         | —                                                                                                                                                                              |
| **Description**                                              | `DESCRIPTION`  | —                                                                                                                                                                              |
| **Status**                                                   | `STATUS`       | Five values, below                                                                                                                                                             |
| **Operation**                                                | `ON_OPERATION` | `NO_CHANGE`, `ADD`, `REPLACE`, `DELETE`. **Empty means evaluate on every operation**                                                                                           |
| Not on the dialog                                            | `TYPE`         | `PROVISIONING` or `SELF_REGISTRATION`. The screen only makes `PROVISIONING` rules; OpenIAM sets `SELF_REGISTRATION` on the rules the self-registration flow creates for itself |
| **Apply selected action group when conditions match**        | `ON_DO`        | The action group to run when the condition is true                                                                                                                             |
| **Apply selected action group when conditions DO NOT match** | `ON_UNDO`      | The action group to run when the condition is false                                                                                                                            |
| The condition builder                                        | `CONDITION_ID` | The root of the condition tree, `NOT NULL`                                                                                                                                     |

An action group, stored in `BR_TARGET`, has a name, a description, an **Active** flag, a type (`PROVISIONING` or `SELF_REGISTRATION`) and an ordered list of actions. A rule points at one for match and one for no match; the labels on the rule screen are **Action when conditions are met** and **Action when conditions are not met**. Groups are reusable: *"This action group is in use by one or more business rules"* is the error when you delete one that is still referenced, so a change to a shared group changes every rule using it. Each action is a type, an optional set of values — a role ID, a group ID, a script ID or a number of days — and a description. Actions run in the order they are listed, so an action that depends on an earlier one must come after it.

## Operation is an event filter, not an instruction

When **Operation** is set to anything at all, OpenIAM skips the rule on every save whose operation does not match, and says so in the service log: *"Skip Business rule '…' because it's for operation '…', but current operation is '…'"*. That log line is how you tell a rule that evaluated false from a rule that never evaluated.

So `ADD` means "only evaluate when a user is being created". `REPLACE` means "only on update". Leaving it empty means "evaluate always", which is what a birthright rule that must also react to transfers needs.

## Status is a request, not an assignment

The **Status** drop-down offers five values, and the last three are instructions to the next recalculation rather than states:

| Stored value    | Webconsole label                                        | Lifecycle                                                         |
| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
| `ACTIVE`        | Active                                                  | Evaluated normally                                                |
| `INACTIVE`      | Inactive                                                | Not evaluated                                                     |
| `ON_MODIFY`     | Pending modifications (Will change status to 'Active')  | Rule changed; recalculate the population, then return to `ACTIVE` |
| `ON_DEACTIVATE` | Pending deactivation (Will change status to 'Inactive') | Recalculate through the *no-match* path, then go `INACTIVE`       |
| `ON_DELETE`     | Pending deletion (Will be physically deleted)           | Recalculate through the *no-match* path, then delete              |

The value you pick is not always the value that gets stored. OpenIAM rewrites it on the way in, and the rule you reopen may show something you did not select:

| You are                    | You select    | The rule is stored as                          |
| -------------------------- | ------------- | ---------------------------------------------- |
| Creating a rule            | Active        | `ON_MODIFY` — *Pending modifications*          |
| Creating a rule            | Anything else | `INACTIVE`                                     |
| Editing an `INACTIVE` rule | Inactive      | `INACTIVE`, unchanged                          |
| Editing an `INACTIVE` rule | Active        | `ON_MODIFY`                                    |
| Editing an `ACTIVE` rule   | Active        | `ON_MODIFY` — even if you changed nothing else |
| Editing an `ACTIVE` rule   | Inactive      | `ON_DEACTIVATE` — *Pending deactivation*       |

Four consequences, all of which surprise people:

**A rule sits in&#x20;*****Pending modifications*****&#x20;until a recalculation completes.** Only a recalculation moves `ON_MODIFY` to `ACTIVE`. Since `PERFORM_BR_REPROVISION` ships disabled, a correct, working rule on a system where nobody has run a recalculation shows **Pending modifications** for ever. That is not a fault and it does not mean the rule is dormant — `ACTIVE` and `ON_MODIFY` rules are both evaluated on every user save. The status tells you whether the *population* is settled, not whether the rule is live.

**Every save of a live rule re-queues the whole population.** Correcting a typo in the description of an active rule puts it back into `ON_MODIFY`, so the next recalculation sweeps every user against it again. Edit deliberately.

**You cannot park a live rule without arming its no-match target.** Selecting Inactive on a live rule stores *Pending deactivation*, and a recalculation in that state runs the **no-match** action group across the population. To stop a rule without unwinding its grants, open its no-match action group first, untick **Active** and save it — an inactive target is skipped and the explanation says *"Target … is inactive, ignore"* — and only then set the rule Inactive.

**Delete behaves the same way, conditionally.** Deleting a rule whose no-match action group is absent or inactive removes it immediately, audited `BUSINESS_RULE_DELETE`. Deleting a rule with an **active** no-match group deletes nothing now: it stores the rule as *Pending deletion*, and the rule disappears only after a recalculation has run that no-match group against every user it matched.

The current screen labels these from the message keys `openiam.ui.webconsole.br.page.tab.rules.dialog.field.status.value.*`; the legacy screen at `/webconsole/br/old` uses `org.openiam.ui.user.status.enum.*`, whose wording for the same three pending states differs slightly. If two administrators describe the same status differently, that is why.

The two pending-terminal states have a specific and useful behaviour: when the condition is **true** but the status is `ON_DEACTIVATE` or `ON_DELETE`, OpenIAM runs the **no-match** action group instead of the match one, and the rule explanation says so: *"The user account meets the requirements, but the rule is going to be Deleted/Disabled based on status …, so undo will be performed if possible"*. Read that line in the explanation before you accept the result of any recalculation on a rule in a pending state.

{% hint style="danger" %}
That is how you retire a rule cleanly — and it is also a mass revocation. Setting *Pending deletion* on a rule whose no-match target contains **Remove user from role** revokes that role from every user the rule ever matched, at the next recalculation. Do not use deletion as a tidy-up; use Inactive, with the no-match group disarmed first, if you want the grants to stay.
{% endhint %}

## Evaluation model

For each active rule, in the order storage supplies them, OpenIAM does the following:

1. **Filter by operation.** If **Operation** is set and differs from the operation actually taking place, skip and log *"Skip Business rule '…' because it's for operation '…', but current operation is '…'"*. An empty **Operation** never skips.
2. **Evaluate the condition tree** against the *updated* user.
3. **If true**: run the on-match group — unless the rule's status is `ON_DEACTIVATE` or `ON_DELETE`, in which case run the no-match group instead.
4. **If false**: run the no-match group.
5. **Record an explanation** per rule: rule ID and name, the condition text, the boolean result, which target ran, each action performed, and elapsed milliseconds.
6. **Collect** provisioning changes and access-request models for later processing.

Two properties of this loop matter for design.

**A condition that errors is treated as false.** Any failure while evaluating a condition is swallowed, the result is set to `false`, and the explanation gains an `"Error: …"` line. Because a `false` result runs the no-match group, an unevaluable condition **executes the no-match group**. If that group removes access, a broken condition revokes access. Design no-match groups on the assumption that they can fire for the wrong reason, and read the rule explanation for `Error:` after any change to a condition.

**There is no priority contract.** Rules are evaluated in whatever order they come back from storage, and actions run in their stored order within a group. Nothing on any screen exposes an administrator-settable priority or tie-break, and there is no way to observe or fix the ordering.

That does not make the outcome random, and the shape of it produces a specific symptom: an entitlement granted on one save and revoked on the next, indefinitely. Make rules mutually exclusive, or make exactly one rule authoritative per entitlement — the mechanics and the way out are [When two rules disagree](/administration-guide/access/business-rules.md#when-two-rules-disagree).

## The condition tree

A condition node is one of four types:

| Type         | Label         | Contains                                                 |
| ------------ | ------------- | -------------------------------------------------------- |
| `AND`        | And           | Children, all of which must be true                      |
| `OR`         | Or            | Children, one of which must be true                      |
| `EXPRESSION` | Expression    | An attribute, an operator and one or more values         |
| `GROOVY`     | Groovy Script | One or more script IDs implementing `IConditionExecutor` |

Every node also has a **negation** flag, applied after the node's own result: the rule explanation records it as *"Invert answer due to negation flag"*, so you can see in the explanation whether negation was what flipped the outcome. Rule validation rejects an `AND` or `OR` with fewer than two children — `BUSINESS_RULE_OR_AND_INVALID`, message key `openiam.ui.business.rules.condition.error.or.and` → "An Or/And condition must have at least two child conditions".

### What a condition can test

The **Attribute** drop-down on an expression node offers 46 entries. The **Webconsole label** column below is what the drop-down shows, from the message keys `org.openiam.ui.business.rule.expression.attribute.*`; the first column is the value stored on the condition, which is what you will see in a rule export and in the rule explanation.

| Stored value             | Webconsole label             | Compared as                          |
| ------------------------ | ---------------------------- | ------------------------------------ |
| `FIRST_NAME`             | First Name                   | string                               |
| `LAST_NAME`              | Last Name                    | string                               |
| `MIDDLE_INIT`            | Middle Initial               | string                               |
| `PREFIX`                 | Prefix                       | string                               |
| `NICKNAME`               | Nickname                     | string                               |
| `MAIDEN_NAME`            | Maiden Name                  | string                               |
| `PARTNER_NAME`           | Partner Name                 | string                               |
| `PREFIX_LAST_NAME`       | Last Name Prefix             | string                               |
| `PREFIX_PARTNER_NAME`    | Partner Name Prefix          | string                               |
| `TITLE`                  | Title                        | string                               |
| `SEX`                    | Gender                       | string                               |
| `EMPLOYEE_ID`            | Employee ID                  | string                               |
| `EMPLOYEE_TYPE_ID`       | Employee Type ID             | **ID**                               |
| `JOB_CODE_ID`            | Job Code                     | **ID**                               |
| `COST_CENTER`            | Cost Center                  | string                               |
| `COMPANY_OWNER_ID`       | Company Owner ID             | **ID**                               |
| `CLASSIFICATION`         | Classification               | **ID**                               |
| `USER_TYPE_IND`          | User Type                    | **ID**                               |
| `USER_SUB_TYPE_ID`       | User Subtype                 | **ID**                               |
| `TYPE_ID`                | Metadata Type                | **ID** — the user's metadata type ID |
| `LOCATION_CD`            | Location Code                | **code**                             |
| `LOCATION_NAME`          | Location Name                | string                               |
| `MAIL_CODE`              | Mail Code                    | **code**                             |
| `ALTERNATE_CONTACT_ID`   | Alternate Contact            | **ID**                               |
| `STATUS`                 | User Status                  | a user status value, by name         |
| `SECONDARY_STATUS`       | Account Status               | a user status value, by name         |
| `BIRTHDATE`              | Birth Date                   | date                                 |
| `CREATE_DATE`            | Created Date                 | date                                 |
| `LAST_UPDATE`            | Last Updated                 | date                                 |
| `START_DATE`             | Start Date                   | date                                 |
| `LAST_DATE`              | End Date                     | date                                 |
| `CLAIM_DATE`             | Claim Date                   | date                                 |
| `ALTERNATIVE_START_DATE` | Alternate Contact Start Date | date                                 |
| `ALTERNATIVE_END_DATE`   | Alternate Contact End Date   | date                                 |
| `CREATED_BY`             | Created By                   | string                               |
| `LAST_UPDATED_BY`        | Last Updated By              | string                               |
| `EMAIL_ADDRESSES`        | Email Addresses              | any address matches                  |
| `PRINCIPAL_LIST`         | Logins                       | login collection                     |
| `ROLES`                  | Roles                        | **role IDs**                         |
| `GROUPS`                 | Groups                       | **group IDs**                        |
| `RESOURCES`              | Resources                    | **resource IDs**                     |
| `AFFILIATIONS`           | Organizations                | **organization IDs**                 |
| `USER_ATTRIBUTES`        | User Attributes              | one `"name":"value"` pair            |
| `ADDRESSES`              | Addresses                    | **not implemented**                  |
| `PHONES`                 | Phone Numbers                | **not implemented**                  |
| `SUFFIX`                 | Suffix                       | **not implemented**                  |

{% hint style="danger" %}
**What you type is not what is compared.** For **Roles**, **Groups**, **Resources** and **Organizations**, OpenIAM compares the entity **ID** of every membership and never the object's name. The same is true of **Job code**, **Employee type ID**, **User status**, **Account status** and **Metadata type**.

The current screen protects you for all of those: each offers a picker that stores the ID for you, and the Roles and Groups pickers put a **Managed system** box above them to narrow the list. What is stored is still the ID, which is what you will see in a rule export and in the rule explanation.

The trap survives in two places. The first is the free-text **Field value** box, which is what you get for **User type**, **User subtype**, **Cost center**, **Location code**, **Title**, **Employee ID** and the other string attributes. Nothing validates those, and a value that matches nobody produces no error and no warning — just a rule that grants nothing and, if it has a no-match target, revokes from everyone.

The second is inside the pickers, because a metadata entry's displayed name and its stored ID are often different. Among the shipped employee types, the entry displayed as **CONTRACTOR** stores `CONTRACTOR TYPE`, and **PERMANENT FULL TIME** stores `PERM FULL TIME`. Pick from the list; never transcribe what you read on screen into a Groovy script or a free-text box.

Prove which form an attribute wants in one pass: build the condition, run **Preview impacted users**, and read the explanation for a user you know qualifies. It prints the user's actual value beside your expected values — `JOB_CODE_ID: 'AP-200' EQUALS 'AP-200' == true.` — so a mismatch is visible at a glance.
{% endhint %}

### Two condition forms the current screen writes incorrectly

{% hint style="danger" %}
Both fail identically and silently: the condition cannot be evaluated, the rule's result resolves to `false`, and the **no-match action group runs**.

**User attributes.** The engine requires the value in the literal form `"name":"value"` — each half in double quotes, separated by a colon, nothing else. The **Attribute name** and **Attribute value** boxes on the current screen store the pair *without* the quotes. The explanation then carries *"Values for attribe wrongly formatter. Got invalid value:…"* — the product's spelling, and how you will recognise it.

**Dates.** **Birth date**, **Created date**, **Start date**, **End date**, **Claim date**, **Last updated** and the two alternate-contact dates are compared as epoch milliseconds. The date picker on the current screen stores an ISO 8601 timestamp instead, and the comparison fails with *"Can't evaluate condition: … Error is For input string: …"*.

The legacy screen at `/webconsole/br/old` writes both forms correctly — quoted pairs for user attributes, milliseconds for dates — so it is the workaround for either condition until this is fixed on your release. Do not take either behaviour on trust: build the condition, run **Preview impacted users**, and read the explanation for a user you know qualifies. A working date condition reads `LAST_DATE: '1767225600000' LESS_THAN '1769817600000' == true.`; a broken one carries a line beginning *"Can't evaluate condition:"* followed by a line beginning *"Error:"*.
{% endhint %}

### Three attributes that cannot be evaluated

{% hint style="warning" %}
**Addresses**, **Phone Numbers** and **Suffix** appear in the drop-down and cannot be evaluated. They are labelled, selectable and saveable, and OpenIAM has no comparison for any of them. Selecting one puts *"Attribute … is out of Business Rule logic."* in the rule explanation and leaves the condition's result undefined — and an undefined condition result is resolved as **`false`**, which means the **no-match action group runs**.

So a condition on one of those three does not fail safe. If the no-match group removes access, it removes access from everyone the rule reaches. Do not select them. Confirm the behaviour, if you need to, with a rule whose no-match group is empty: build the condition, preview it, and read the explanation for the *"out of Business Rule logic"* text.
{% endhint %}

### Operators

The **Operation** drop-down on an expression node offers eleven values. Which ones are legal depends on how the attribute is compared:

| Stored value           | Label                | Valid for                                                     |
| ---------------------- | -------------------- | ------------------------------------------------------------- |
| `EQUALS`               | Equals               | strings, statuses, user attributes                            |
| `EQUALS_IGNORE_CASE`   | Equals ignore case   | strings and user attributes — **not** statuses                |
| `START_WITH`           | Starts with          | strings, user attributes                                      |
| `ENDS_WITH`            | Ends with            | strings, user attributes                                      |
| `CONTAINS`             | Contains             | strings, user attributes                                      |
| `CONTAINS_IGNORE_CASE` | Contains ignore case | strings, user attributes                                      |
| `GREATER_THAN`         | Greater than         | dates — true when the user's date is *later* than the value   |
| `LESS_THAN`            | Less than            | dates — true when the user's date is *earlier* than the value |
| `ANY_MATCH`            | Any match            | collections — roles, groups, resources, organizations, logins |
| `ALL_MATCH`            | All match            | collections — true when the user holds **every** listed ID    |
| `IS_NULL`              | Is empty             | everything                                                    |

The current screen offers only the operators the attribute supports, so most of these mistakes are no longer reachable from the drop-down: **User status**, **Account status**, **Employee type ID**, **Job code** and **Metadata type** offer only *Equals* and *Is empty*; the four collection attributes offer only *Any match*, *All match* and *Is empty*; the date attributes offer only *Greater than*, *Less than* and *Is empty*. A rule imported or built on the legacy screen can still hold an unsupported pairing.

Choosing an operator the attribute does not support puts *"Operation … is not supported for user Entitlement values."* in the rule explanation and lands you in the same undefined-result-becomes-false path as above. The screen's own guard, `openiam.ui.business.rules.condition.error.operation` → "Invalid operation for the selected attribute", catches most of these at save time — but not all, which is why the explanation is worth reading even after a clean save.

Four behaviours to know:

* **`EQUALS` on an empty field is false, not true.** A string comparison requires a non-blank value on both sides, so `EQUALS` with an empty expected value never matches. `IS_NULL` is the only way to test emptiness.
* **Collection operators ignore pending deletions.** Memberships being removed in the same save are dropped before the condition looks at them, so a role that is on its way out is already invisible to `ANY_MATCH`. A rule that should react to a removal must therefore be driven by what is left, not by what is going.
* **User attributes takes exactly one value, in a literal quoted form.** The stored value is `"costCentreOverride":"AP-200"` — attribute name and value each in double quotes, separated by a colon, nothing else. More than one value gives *"'Values' for 'User Attributes' attribute could not have more than 1 value."*; a malformed value gives *"Values for attribe wrongly formatter"* — the product's spelling, which is how you will recognise it, and which is what the current screen produces. See the warning above.
* **An absent attribute is not an error.** If the user has no attribute of that name the condition is simply false, or true when the operator is *Is empty*. Only a malformed value breaks the rule.

## The action catalogue

There are 27 action types. Twenty-four are provisioning actions and appear on a `PROVISIONING` action group; three — `TRANSITION`, `TRIGGER_CREATE_WITH_APPROVAL` and `TRIGGER_CREATE_WITHOUT_APPROVAL` — are self-registration actions and appear only on a `SELF_REGISTRATION` group. The action group's type therefore decides which actions the drop-down offers; if an action you expect is missing, check the group's type first.

### Identity-state actions

| Stored value | Webconsole label | Exact effect                                           |
| ------------ | ---------------- | ------------------------------------------------------ |
| `ACTIVATE`   | Activate user    | Sets **User Status** to `ACTIVE`, if it is not already |
| `DEACTIVATE` | Deactivate user  | Sets **User Status** to `DEACTIVATED`                  |
| `ENABLE`     | Activate user    | Clears **Account Status**, if it is set                |
| `DISABLE`    | Deactivate user  | Sets **Account Status** to `DISABLED`                  |
| `LOCK`       | Lock user        | Sets **Account Status** to `LOCKED`                    |

{% hint style="warning" %}
`ENABLE` and `ACTIVATE` render the *same* label, and so do `DISABLE` and `DEACTIVATE`. Both label sets in the product spell them the same way, so they are indistinguishable in the drop-down — and they are not equivalent: the pair ending in `-ATE` writes the **User Status**, the other pair writes the **Account Status**. After selecting one, reopen the action and confirm the stored value; then run the rule against a disposable user and see which of the two status fields moved.
{% endhint %}

Each action is a no-op when the user is already in the target state, which is why these actions are idempotent across repeated recalculations. And an OpenIAM status change is not a target-system change: prove the deprovisioning path separately for every managed system.

### Direct membership actions

| Stored value      | Webconsole label              | What it changes on the user          |
| ----------------- | ----------------------------- | ------------------------------------ |
| `ADD_ROLE`        | Add user to role              | The user's roles                     |
| `REMOVE_ROLE`     | Remove user from role         | The user's roles                     |
| `ADD_GROUP`       | Add user to group             | The user's groups                    |
| `REMOVE_GROUP`    | Remove user from group        | The user's groups                    |
| `ADD_RESOURCE`    | Grant resource to user        | The user's resources                 |
| `REMOVE_RESOURCE` | Revoke access from resource   | The user's resources                 |
| `ADD_ORG`         | Add user to organization      | The user's organization affiliations |
| `REMOVE_ORG`      | Remove user from organization | The user's organization affiliations |

All eight behave the same way, and the details matter:

* The action's **values are entity IDs**, and an action with no values fails with *"action %s doesn't have any values"*.
* **Adds are idempotent.** If a valid membership already exists, the action is skipped with *"Action skipped for Id=… as user is already entitled to this entity"*.
* **New memberships are tagged.** Every membership a rule creates carries the membership tag `BUSINESS_RULE_ASSIGNMENT`. Read the tag on the user's role row to tell a rule grant from a request grant or an administrator's.
* **A pending delete is reversed.** If the incoming save marks a membership `DELETE` but the rule says add, the operation is flipped back to `ADD`: *"Entity with id = … came with status DELETE, but based on business rules it should be added"*.
* **"Already entitled" means currently in force.** A membership counts only if its start date is empty or past **and** its end date is empty or future. A future-dated or already-expired membership is invisible to the add and remove logic, so an add action creates a *second* row alongside an expired one. You will see both rows on the user's entitlements screen; that is the check.

{% hint style="danger" %}
**Removal is not scoped to the rule's own grants.** A remove action matches the membership by entity ID alone. It does not check the `BUSINESS_RULE_ASSIGNMENT` tag the rule wrote on the way in, and it does not check which rule wrote it. A no-match **Remove user from role** action therefore removes that role however it was granted — by request, by synchronization, by another rule, or by an administrator. Prove it on a disposable user: grant the role by hand, make the user fail the rule's condition, recalculate, and watch the hand-made grant disappear.

Do not put a remove action in a no-match target for an entitlement that any other mechanism can grant, unless that precedence decision has been made explicitly and tested.
{% endhint %}

### Workflow-based access actions

| Stored value                  | Webconsole label                          |
| ----------------------------- | ----------------------------------------- |
| `ADD_ROLE_ACCESS_REQUEST`     | Initiate workflow: Add user to role       |
| `REMOVE_ROLE_ACCESS_REQUEST`  | Initiate workflow: Revoke user from role  |
| `ADD_GROUP_ACCESS_REQUEST`    | Initiate workflow: Add user to group      |
| `REMOVE_GROUP_ACCESS_REQUEST` | Initiate workflow: Revoke user from group |

These four raise an access request instead of writing a membership, so the grant arrives through the approval path with its own evidence trail. A revoke raised this way carries the comment *"Revoke access was initiated based on business rule evaluation results."* and the system user as requestor — which is how you recognise one in the request list.

Use them where approval evidence is required. Verify request creation, approval routing, the resulting membership, provisioning, and — critically — what happens on repeated evaluation while a request is still pending. OpenIAM reads the user's already-requested entitlements before evaluating, specifically to avoid raising a duplicate request each time the rule runs. Confirm that holds on your release the cheap way: leave one request unapproved, run the recalculation again, and count the open requests for that user.

Audit action `INITIATE_ACCESS_REQUEST_BASED_ON_BR` exists for this path.

### Broad access actions

These four act on the user's **entire** entitlement collections — roles, groups, resources *and* organization affiliations. Not the ones this rule granted. All of them.

| Stored value            | Webconsole label                                                              | Exact scope                                                                                         |
| ----------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `END_DATE_ALL_ACCESS`   | Terminate access to all entitlements by setting end date to now               | Sets the end date to now on every membership not already being deleted, across all four collections |
| `RESTORE_ALL_ACCESS`    | Resume access and clear membership end dates                                  | Clears the end date on every membership not already being deleted, across all four collections      |
| `PROLONG_ALL_ACCESS`    | Resume access and extend end date by the specified number of days from now    | Sets the end date to today plus N days on every membership not already being deleted                |
| `DROP_ALL_ENTITLEMENTS` | Remove all entitlements (roles, groups, organizations, resources) immediately | Marks every membership in all four collections for deletion                                         |

Three details that change how you use them:

* **`PROLONG_ALL_ACCESS` sets an absolute date, it does not add to the existing one.** The new end date is today plus N days, regardless of what the membership's end date was. An entitlement that was due to expire in six months will be *shortened* to N days if N is small.
* **`PROLONG_ALL_ACCESS` with a bad or missing value silently means zero days.** A value that is absent or not a whole number is treated as `0`, and today plus zero days end-dates everything as of now — which is `END_DATE_ALL_ACCESS` by accident. Reopen the action after saving and confirm the value is there and is an integer; the save will not warn you.
* **`RESTORE_ALL_ACCESS` destroys legitimate expiry dates.** Clearing the end date on every membership removes the expiry from time-bound and separately approved access as well, and nothing records what the dates used to be.

All four also mark every membership they touch as changed, so they generate a write for every membership the user holds. On a user with a hundred entitlements that is a hundred updates and potentially a hundred provisioning operations — count the user's entitlements before putting one of these in a rule that can match a population.

### The Groovy action

| Stored value | Webconsole label   |
| ------------ | ------------------ |
| `GROOVY`     | Call Groovy script |

The action's values are Groovy Manager script IDs. Each script must implement `IActionExecutor`, declaring `void perform(User user)`. OpenIAM runs each in turn and reports per script, in the rule explanation and the service log, either *"Actions for groovy script file %s has been successfully performed."* or *"Could not initialize groovy class from file: %s."*. If no script initialised, the whole action is reported as failed.

Use Groovy for a genuinely missing action — stamping an attribute, computing a derived value. Do not use it to re-implement **Add user to role**; the built-in action has clearer audit and explanation text. Working examples, and the reason a condition script declares `perfrom` while an action script declares `perform`, are under [Groovy extension points](#groovy-extension-points).

### The two actions that cannot run

| Stored value       | Webconsole label | Status           |
| ------------------ | ---------------- | ---------------- |
| `ADD_ATTRIBUTE`    | Add attribute    | **Does nothing** |
| `REMOVE_ATTRIBUTE` | Remove attribute | **Does nothing** |

Both are labelled and both appear in the action drop-down of a provisioning action group. Neither is implemented, so both take OpenIAM's fallback path for an unrecognised action type and report *"Could not perform action with type ADD\_ATTRIBUTE"* — or `REMOVE_ATTRIBUTE` — every time the group runs.

Confirm it in three minutes rather than taking this on trust: add an **Add attribute** action to a group on a disposable rule, run the rule against a test user, and read the rule explanation. The failure line is there, and the user has no new attribute.

{% hint style="danger" %}
Selecting **Add attribute** or **Remove attribute** produces an action that fails every time it runs. The rule still saves, the group still "runs", and the failure appears only in the explanation — which is easy to miss if nobody reads explanations. Nothing is written and nothing is raised to the administrator.

Use a Groovy action instead. The worked example under [A complete action script](#a-complete-action-script) sets a user attribute correctly, including the attribute operation OpenIAM requires before it will persist one.
{% endhint %}

### Self-registration actions

| Stored value                      | Action group type   | Consumed by                |
| --------------------------------- | ------------------- | -------------------------- |
| `TRANSITION`                      | `SELF_REGISTRATION` | The self-registration flow |
| `TRIGGER_CREATE_WITH_APPROVAL`    | `SELF_REGISTRATION` | The self-registration flow |
| `TRIGGER_CREATE_WITHOUT_APPROVAL` | `SELF_REGISTRATION` | The self-registration flow |

Unlike **Add attribute**, these three are implemented — just not by the provisioning rule engine. They are acted on by the self-registration flow, which is why they appear only on an action group typed `SELF_REGISTRATION` and never on a provisioning group. Put one on a provisioning rule and it will not be offered; find it on a rule that is not a self-registration rule and the action group's type is wrong. See [Self-registration](/administration-guide/identities/self-registration.md).

## Groovy extension points

A rule can call Groovy in two places: as a `GROOVY` condition node, and as a **Call Groovy script** action. Each expects a script implementing one interface. Use them only where the expression tree genuinely cannot express the policy — a built-in action has clearer semantics, better audit and no maintenance cost.

### The condition interface spells the method `perfrom`

A condition script must declare exactly this method, and the spelling is not a mistake in this guide:

{% code overflow="wrap" %}

```groovy
boolean perfrom(User u)
```

{% endcode %}

**`perfrom`, not `perform`.** The interface your script implements, `IConditionExecutor`, spells it that way, so your script must too. Do not "fix" it: a script declaring `perform` compiles and then fails validation with `BUSINESS_RULE_WRONG_GROOVY` when you attach it to a condition, which is how you will discover the typo if you forget it.

An **action** script spells it correctly:

{% code overflow="wrap" %}

```groovy
void perform(User user)
```

{% endcode %}

So in the same feature, a condition script declares `perfrom` and an action script declares `perform`. The typo is carried consistently through the recalculation path as well, so seeing `perfrom` in a log line is not evidence of a problem.

### How a script is bound

When you attach a script, OpenIAM looks it up by ID in the Groovy Manager, instantiates it, and checks that it implements the interface the slot requires; a script that fails any of those three steps is treated as absent. At run time the instance is wrapped so that **every exception it throws is swallowed and the call returns nothing**.

That has a consequence worth stating plainly: a condition script that throws does not surface as an error. It returns nothing, the condition's result is undefined, and an undefined result resolves to `false` — so the **no-match action group runs**. Write conditions that cannot throw. The only trace is the service log: search it for *"Error invoking Groovy method"* after any recalculation you did not fully expect, because neither the rule explanation nor the out-of-sync queue will tell you a script failed.

Manage the scripts themselves at **Administration → Groovy Manager** — see [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md). The shipped templates are `business-rules/CustomConditionExecutor_1.0.groovy` and `business-rules/CustomActionExecutor_1.0.groovy`.

### A complete condition script

Adapted from the shipped template, keeping its declaration and signature. This is the condition the expression tree cannot express: *contractor whose engagement ends within the next 30 days and who still holds a privileged role.*

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

```groovy
import org.openiam.br.groovy.IConditionExecutor
import org.openiam.dto.User
import org.openiam.base.AttributeOperationEnum
import org.openiam.idm.srvc.user.dto.UserStatusEnum
import org.openiam.idm.srvc.membership.dto.MembershipXref

/**
 * True when the user is an active contractor whose end date falls within the next
 * 30 days AND who still holds at least one of the privileged roles listed below.
 *
 * The interface method is spelled 'perfrom' in the product. That is not a mistake
 * in this file - IConditionExecutor declares it that way.
 *
 * No database or network calls: this runs once per user during a full
 * recalculation, and every exception is swallowed into a 'false' result, which
 * runs the rule's no-match target.
 */
class ContractorExpiringWithPrivilegedAccess implements IConditionExecutor {

    /**
     * The stored ID of the employee type, not the label. The shipped entry displayed
     * as CONTRACTOR stores 'CONTRACTOR TYPE'. Read yours from a representative user.
     */
    static final String CONTRACTOR_EMPLOYEE_TYPE_ID = 'CONTRACTOR TYPE'

    /** Role IDs, not role names. Replace with the IDs from your catalogue. */
    static final Set<String> PRIVILEGED_ROLE_IDS = ['<PRIVILEGED_ROLE_ID_1>',
                                                    '<PRIVILEGED_ROLE_ID_2>'] as Set

    static final long THIRTY_DAYS_MS = 30L * 24L * 60L * 60L * 1000L

    @Override
    boolean perfrom(User u) {
        if (u == null) {
            return false
        }
        if (CONTRACTOR_EMPLOYEE_TYPE_ID != u.employeeTypeId) {
            return false
        }
        if (UserStatusEnum.ACTIVE != u.status) {
            return false
        }

        Date lastDate = u.lastDate
        if (lastDate == null) {
            return false
        }
        long now = System.currentTimeMillis()
        boolean expiringSoon = lastDate.time > now && lastDate.time <= (now + THIRTY_DAYS_MS)
        if (!expiringSoon) {
            return false
        }

        Set<MembershipXref> roles = u.roles
        if (roles == null || roles.isEmpty()) {
            return false
        }
        return roles.any { MembershipXref xref ->
            AttributeOperationEnum.DELETE != xref.operation &&
                PRIVILEGED_ROLE_IDS.contains(xref.entityId)
        }
    }
}
```

{% endcode %}

Attach it by adding a `GROOVY` node to the condition tree and selecting the script ID. OpenIAM instantiates the script at save time and refuses the save with `BUSINESS_RULE_WRONG_GROOVY` if it does not compile or does not implement the interface — so a clean save proves the shape of the script, and nothing else about it.

### A complete action script

An action script receives the same user object and mutates it; OpenIAM persists the result. The built-in actions cover roles, groups, resources, organizations and status, so a Groovy action is for what they do not — here, stamping a user attribute so a downstream report can find these users, which no built-in action can do.

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

```groovy
import org.openiam.br.groovy.IActionExecutor
import org.openiam.base.AttributeOperationEnum
import org.openiam.dto.User
import org.openiam.dto.UserAttribute

/**
 * Records that the contractor-expiry rule fired, by setting a user attribute
 * that reports and downstream rules can read. Idempotent: re-running it on an
 * already-stamped user changes nothing.
 *
 * Note the method here IS spelled 'perform' - IActionExecutor differs from
 * IConditionExecutor on this point.
 *
 * The operation MUST be set. OpenIAM drops every attribute whose operation is
 * null or NO_CHANGE before persisting; only ADD and REPLACE are written.
 */
class StampContractorExpiryReview implements IActionExecutor {

    static final String ATTR_NAME = 'contractorExpiryReview'
    static final String ATTR_VALUE = 'PENDING'

    @Override
    void perform(User user) {
        if (user == null) {
            return
        }
        if (user.userAttributes == null) {
            user.userAttributes = new HashMap<String, UserAttribute>()
        }

        UserAttribute existing = user.getAttribute(ATTR_NAME)
        if (existing != null && ATTR_VALUE == existing.firstValue) {
            return
        }

        UserAttribute attribute = new UserAttribute()
        attribute.name = ATTR_NAME
        attribute.values = [ATTR_VALUE]
        attribute.userId = user.id
        attribute.operation = (existing == null) ? AttributeOperationEnum.ADD
                                                 : AttributeOperationEnum.REPLACE
        user.userAttributes.put(ATTR_NAME, attribute)
    }
}
```

{% endcode %}

Attach it as an action of type **Call Groovy script** in an action group, with the script ID as the value. OpenIAM reports the outcome per script in the rule explanation and the service log, as either *"Actions for groovy script file %s has been successfully performed."* or *"Could not initialize groovy class from file: %s."*. The first message means the script ran, not that it achieved anything — verify the attribute on the user.

A condition testing this stamp afterwards uses attribute **User Attributes** with value `"contractorExpiryReview":"PENDING"` — quotes included, which on the current screen means building that condition on `/webconsole/br/old`.

## Read the explanation

The explanation is the only place the engine tells you what it decided and why. For each rule it records:

* the rule ID and name;
* per condition node, the attribute, the user's actual value, the operator, the expected values and the result — e.g. `JOB_CODE_ID: 'AP-200' EQUALS 'AP-200' == true.`;
* for `AND`/`OR`, which child decided it — *"Because of ('JOB\_CODE\_ID' EQUALS 'AP-200') returns 'true' for OR operation, expression is TRUE"* / *"All of child expression are TRUE. AND is TRUE"*;
* negation, when applied — *"Invert answer due to negation flag. Result is false"*;
* whether the rule was skipped on operation, and with which operations;
* which target ran, on-match or no-match;
* per action, the outcome text — *"The … action has been performed with the following results: ADD with Id=…"*, *"Action skipped for Id=… as user is already entitled to this entity"*, or *"Could not perform action with type …"*;
* elapsed milliseconds per rule.

Read it to answer: was the rule skipped on operation? Which expression decided the result? Was the target inactive or empty? Was an add skipped because the membership already existed? Did an action create a direct change or request work? Did another rule target the same object?

The **Out of sync users** tab and its preview are an investigation aid. Neither proves downstream provisioning will succeed.

## Before you recalculate a population

`PERFORM_BR_REPROVISION` ships disabled, so "before you recalculate" means "before you activate" — an active rule reaches every user who is saved, whether or not a sweep ever runs. Note also that a recalculation only evaluates rules in a **pending** status: a rule that has already settled to `ACTIVE` is not swept again until it is re-saved. Work through this list first:

* export or version the rule and both action groups;
* list every action in either group that opposes an action in the other on the same object;
* record the expected matching, non-matching and changing counts, from preview;
* identify direct, requested, synchronized and inherited duplicate paths to each affected entitlement;
* test one true, one false, one transition, one null-attribute, one repeated and one protected user;
* estimate the request and connector load — `END_DATE_ALL_ACCESS` and friends generate one write per membership per user;
* confirm the no-match target is as carefully designed as the match target, remembering that an errored condition runs it;
* define a stop threshold and who can stop it.

## Pattern 1: deterministic birthright role

**Problem:** active employees in Finance-US with job code `AP-200` require the `Accounts Payable Specialist` role.

Condition: `AND` of `JOB_CODE_ID EQUALS 'AP-200'`, `AFFILIATIONS ANY_MATCH '<ORG_ID>'` and `STATUS EQUALS 'ACTIVE'` — *Any match* for the affiliation, because *Equals* is not supported on a collection. Leave **Operation** empty so creation and later changes are both evaluated. On match, **Add user to role** with the role **ID**; on no match, **Remove user from role** *only* if this rule is the sole source of that role, because it cannot distinguish its own grants. If optional or exception access can grant the same role, either give the exception its own contained role or route all qualification and removal through this one mechanism.

The full build, including the seven test cases and the population preview, is [Rule 1](/administration-guide/access/business-rules.md#rule-1-a-birthright-grant).

## Pattern 2: transfer between jobs

**Problem:** a user leaving AP must lose the AP role and gain the new job's role.

Use **one authoritative rule per role**, never one rule that does both:

* AP rule: match AP qualification, add AP; no match, remove AP.
* New-job rule: match new qualification, add new role; no match, remove new role.

Because there is no priority contract, do not rely on evaluation order to sequence the two. Make the populations explicit and review the overlap. If both roles are legitimately held during a transition window, model that with effective dates or an approved transition attribute — and remember that `AUTO_REVOCATION` ships disabled, so an end date alone will not close the window.

Run SoD analysis for the overlap, and check that technical access shared by both roles is not deprovisioned and reprovisioned unnecessarily.

## Pattern 3: temporary leave

**Problem:** some access should pause during leave and return afterwards.

Prefer targeted removal or end-dating of a purpose-built `Leave-suspended` role containing only the access that should pause, preserving requested, regulated and retained access according to policy. On return, recalculate current qualification rather than clearing every end date, and compare pre-leave and post-return effective access *and* target access. Use `END_DATE_ALL_ACCESS` and `RESTORE_ALL_ACCESS` only if every membership the user holds is governed as a single bundle and no independent expiry needs to survive, which is rare.

The rule that does the removal is [Rule 3](/administration-guide/access/business-rules.md#rule-3-a-rule-that-removes-access).

## Pattern 4: contractor extension

**Problem:** extend approved contractor access by 30 days.

Do not use `PROLONG_ALL_ACCESS` unless policy genuinely extends *every* role, group, resource and organization membership to exactly 30 days from today. It sets an absolute date; it does not add 30 days to each existing end date; and a non-integer value silently means zero days, which end-dates everything immediately.

For normal contractor governance: update the authoritative contract end date on the user, let qualification recalculate, and update only the memberships that engagement governs. Keep separately approved access inside its own maximum duration.

## Pattern 5: high-risk access with approval

**Problem:** a deterministic condition identifies *eligibility*, but a human must approve.

Use `ADD_ROLE_ACCESS_REQUEST` or `ADD_GROUP_ACCESS_REQUEST` instead of a direct add. Keep the condition narrow enough that it establishes eligibility and not approval. Configure the entitlement's approval flow, duration, risk, SoD checks and reviewer. Then test repeated evaluation while a request is pending, so the rule does not create duplicate requests.

## Pattern 6: termination

**Problem:** revoke access on an authoritative leaver event.

Use the full leaver process in [Grant birthright access](/administration-guide/lifecycle/birthright-access.md). A business rule can participate, and `LEAVER_PROCESS` ships **enabled** at `0 0 17 * * ?` driving the script named by `org.openiam.idm.leaver.script`, shipped `batch/LeaverProcess.groovy`.

`DROP_ALL_ENTITLEMENTS` is a destructive population-wide action: it marks every membership in all four collections for deletion for every matching user. Before using it, test service accounts, protected identities, legal-retention requirements, manually fulfilled systems, connector outages and retry behaviour.

## Rollback

Untick **Active** on the no-match action group first, then set the rule Inactive. In that order the rule stops acting and nothing is unwound; in the other order the rule is stored as `ON_DEACTIVATE` and the next recalculation runs the no-match target for the whole population. Restore the previous condition, action groups and script references. Press **Preview impacted users** and compare against the counts you recorded. Then run a bounded corrective recalculation.

Disabling or deleting a rule does not restore memberships already removed and does not undo external side effects. Recover through the authoritative grant path and verify every managed system.

## Related

* [Build a business rule](/administration-guide/access/business-rules.md) — the procedure, and three complete worked rules
* [Manage roles and groups](/administration-guide/access/manage-roles-and-groups.md) — the memberships these actions write
* [Membership tags](/administration-guide/governance/certification/membership-tags.md) — what `BUSINESS_RULE_ASSIGNMENT` is used for
* [Enumerated values](/administration-guide/reference/enums.md) — all 32 enum sets

*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/access/business-rules/business-rule-reference.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.
