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

# Build a business rule

Build a business rule end to end, with three complete worked rules you can adapt and the recalculation that ships disabled.

## What this is, and when you need it

A business rule watches user data and changes access. It has one condition tree, one **action group** to run when the condition is true, and one to run when it is false.

This page builds three complete rules you can adapt: [a birthright grant](#rule-1-a-birthright-grant), [a conditional grant with an exception](#rule-2-a-conditional-grant-with-an-exception) and [a rule that removes access](#rule-3-a-rule-that-removes-access). It then answers the question those three raise: [what happens when two of them disagree](#when-two-rules-disagree). Every condition attribute, every operator, every action's exact blast radius and the two worked Groovy extensions are in [Business rule reference](/administration-guide/access/business-rules/business-rule-reference.md).

## At a glance

|                          |                                                                                                                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **You will end up with** | An active rule granting a named role to everyone matching a condition, tagged `BUSINESS_RULE_ASSIGNMENT` on each user, with the population swept once and removal armed deliberately |
| **Where**                | `/webconsole/br` — **Access Control → Business Rules**, tabs **Rules**, **Action groups**, **Out of sync users**                                                                     |
| **You need first**       | The role's ID and the ID of whatever the condition tests; one disposable user whose Employee ID you know                                                                             |
| **Takes**                | 30 to 60 minutes to build and pin to one user; the sweep depends on how many users change                                                                                            |
| **Reversible**           | Partly — the rule is, its effects are not. A no-match sweep that has removed memberships and deprovisioned target accounts cannot be undone                                          |

## Before you begin

* **The entity IDs.** Read the role's from the read-only **Role ID** field on `/webconsole/editRole`. Most condition attributes are compared as **IDs**, not display values, and a value matching nobody raises no error — see [What a condition can test](/administration-guide/access/business-rules/business-rule-reference.md#what-a-condition-can-test).
* **Four disposable users:** one you can save repeatedly and whose Employee ID you know, one who matches, one who does not, and one who will stop matching.
* **The decision that this rule is the entitlement's only source**, and the event that removes it. A rule with no no-match target grants forever; two mechanisms owning one role produce [an entitlement that flips on every save](#when-two-rules-disagree).

{% hint style="warning" %}
**Nothing recalculates business rules on a schedule out of the box.** `PERFORM_BR_REPROVISION`, cron `0 0 0 * * ?`, ships **disabled** — it arrived enabled in 4.2.1.0 and was switched off in 4.2.1.5. A saved rule reaches new and modified users immediately and **nobody else** until someone sweeps. Read its **Is Active** state and last execution time under **Administration → Batch Tasks** before you start.
{% endhint %}

## Rule 1: a birthright grant

Employees whose job code is `AP-200` and who are affiliated to the Finance-US organization should hold the `Accounts Payable Specialist` role, with no request, and should lose it when they stop qualifying.

|                     |                                                                                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Condition**       | `AND` of **Job code** *Equals* `AP-200`, and **Organizations** *Any match* Finance-US                                                           |
| **On match**        | Action group `AP Specialist grant`: one **Add user to role** action, value = the role ID                                                        |
| **On no match**     | Action group `AP Specialist revoke`: one **Remove user from role** action, same role ID — added last, in step 8, and only after the check there |
| **No-match effect** | The role is removed from every user who does not match, however they got it                                                                     |
| **Evidence**        | Membership tag `BUSINESS_RULE_ASSIGNMENT`; `BUSINESS_RULE_EXPLANATION` on the provisioning audit event                                          |

{% stepper %}
{% step %}

### Collect the real IDs

You need three IDs, not three names: the job code, the Finance-US organization and the `Accounts Payable Specialist` role. The condition's pickers resolve the first two for you, but you still have to recognise the right entry, and you need the role ID by hand for the action group — read it from the read-only **Role ID** field on `/webconsole/editRole`.

Open one qualifying user and read the values off their record rather than transcribing from a spreadsheet. Note that user's **Employee ID** as well; step 3 uses it.
{% endstep %}

{% step %}

### Create the on-match action group

**Access Control → Business Rules → Action groups → New action group**. Name it for the rule, not for the action — `AP Specialist grant`. Add one action, **Add user to role**, and set its value to the role ID. Tick **Active**. Save; the audit records `BUSINESS_RULE_TARGET_SAVE`, and the action itself `BUSINESS_RULE_ACTION_SAVE`.

Do **not** create the revoke group yet. A no-match action group is not a per-user setting: it runs for every user in the system who does not match, from the moment the rule is live.
{% endstep %}

{% step %}

### Build the rule, pinned to one user, and save it Inactive

**Rules → New rule**. Name and describe it — the description is what the next administrator reads at 2am. Leave **Operation** on *All operations - including both user creation and updating* so the rule evaluates on create **and** update; choose *Add - triggered only upon user creation* only if this is genuinely create-only.

Build the condition as a **Group 'AND'** with three **Expression** children:

| Field name    | Operation | Field value                           |
| ------------- | --------- | ------------------------------------- |
| Job code      | Equals    | `AP-200`, from the picker             |
| Organizations | Any match | Finance-US, from the picker           |
| Employee ID   | Equals    | the employee ID of your one test user |

The third row is a temporary **pin**. While it is present, nobody but that user can satisfy the condition, so a live rule can only affect one person. Set **Apply selected action group when conditions match** to `AP Specialist grant`, leave **Apply selected action group when conditions DO NOT match** empty, set **Status** to Inactive and save. The audit records `BUSINESS_RULE_SAVE`, and a rule created as Inactive is stored `INACTIVE` and evaluated by nothing.

{% hint style="info" %}
If `org.openiam.automatic.organization.hierarchy.assignment.enabled` is `true` — it ships `true` — a user affiliated to a department under Finance-US is also affiliated to Finance-US, automatically. Your *Any match* on Finance-US will therefore match users nobody explicitly put there. That is usually what you want. What is not obvious is the timing: the ancestor affiliations are written when the user is saved, **after** the rules have run, so on the save that first puts someone into a child department the condition is still false. They match from their next save onwards. See [Resources and organizations](/administration-guide/access/resources-and-organizations.md#automatic-hierarchy-assignment-the-shipped-default-is-true).
{% endhint %}
{% endstep %}

{% step %}

### Activate, and save the pinned user

Reopen the rule, set **Status** to Active and save. Two things happen. The rule is stored as *Pending modifications*, which is what the Status column will show from now on and is normal — only a completed recalculation moves it to Active, and the [full state table](/administration-guide/access/business-rules/business-rule-reference.md#status-is-a-request-not-an-assignment) is on the reference page. And the rule is now evaluated on every user save anywhere in the system; because of the pin, only one user can match it.

Open the pinned test user and save them. Then check, in this order:

1. The role appears on the user, tagged `BUSINESS_RULE_ASSIGNMENT`.
2. The provisioning audit event carries `BUSINESS_RULE_EXPLANATION`, and inside it the line `JOB_CODE_ID: 'AP-200' EQUALS 'AP-200' == true.`, then *"All of child expression are TRUE. AND is TRUE"*, then *"The ADD\_ROLE action has been performed with the following results: ADD with Id=…"*.
3. Save the same user a second time. The action is now reported as *"Action skipped for Id=… as user is already entitled to this entity"* — that is the idempotence you want.
   {% endstep %}

{% step %}

### Test the boundaries, still pinned

With the pin in place these are all safe, because no other user can match and there is no no-match group to run:

* Change the pinned user's job code to something else and save. Expected: the explanation says *"Because of ('JOB\_CODE\_ID' EQUALS 'AP-200') return 'false' for AND operation, expression is FALSE"*, and **the role stays**, because nothing is configured to remove it. That is the correct behaviour of a grant-only rule, and it is the behaviour rule 2 relies on.
* Put the value back, move the user to a department outside Finance-US, save, and read the `AFFILIATIONS` line to confirm the organization IDs the engine actually saw.
* Clear the job code entirely and save. Expected: false. *Equals* against a blank field is never true; only *Is empty* tests emptiness.
  {% endstep %}

{% step %}

### Remove the pin, and preview the population

Delete the **Employee ID** expression and save with Status Active. The rule is live for the real population on every user save from this moment — it has not yet touched anyone who is not saved.

Go to the **Out of sync users** tab and press **Preview impacted users**. That runs the same evaluation against every user in batches of 50 and records one entry per user the rule *would* change, with the full explanation behind **View details**. Compare the count against your estimate before going any further. A birthright rule whose preview returns ten times the expected number is almost always an organization picking up its children, or a free-text value that matches more broadly than you meant.

{% hint style="warning" %}
The preview is not a dry run of an inactive rule. It only reports on rules in a pending status — `ON_MODIFY`, `ON_DEACTIVATE`, `ON_DELETE` — which means the rule has to be live for per-save evaluation before you can preview it. It also covers **every** pending rule on the system, not only yours, and it clears its previous contents each time it runs, so two administrators previewing at once read each other's results. Agree who is driving before you press it.
{% endhint %}
{% endstep %}

{% step %}

### Sweep the population deliberately

Nothing has happened to the users who have not been saved. `PERFORM_BR_REPROVISION` ships disabled, so unless someone has enabled it on your system there is no overnight sweep.

When the preview count is what you expect, press **Provision impacted users** on the same tab. Every user the evaluation changes is put through a real provisioning save, reaching every managed system they are entitled to. Audit records `BUSINESS_RULE_RECALCULATE`.

When it finishes, the rule's status settles from *Pending modifications* to **Active** and the out-of-sync list is empty. That settling is one-way: an `ACTIVE` rule is not swept again, so if you need a second sweep you have to re-save the rule to put it back into a pending status.
{% endstep %}

{% step %}

### Decide about removal, and arm it as a separate act

If this rule is to own the role — meaning nothing else may grant it — create `AP Specialist revoke` with one **Remove user from role** action for the same role ID, attach it as **Apply selected action group when conditions DO NOT match**, and save.

{% hint style="danger" %}
Arming a no-match target is a population-wide act with no undo. The removal matches the membership by role ID alone: it does not check the `BUSINESS_RULE_ASSIGNMENT` tag the rule wrote, it does not check which rule wrote it, and it will take the role away from someone who was granted it by request or by an administrator.

Immediately after saving, press **Preview impacted users** and read the entries. The grant population having just been swept, they are the users who hold the role without qualifying for it — the set the rule will strip. If that is not what you intended, open `AP Specialist revoke`, untick **Active** and save; an inactive target is skipped with *"Target AP Specialist revoke is inactive, ignore"*. There is a real exposure window between the two actions, because any user saved in between is evaluated against the live rule. The only true dry run is a non-production copy.
{% endhint %}

Then verify removal end to end on the pinned test user: change their job code so they stop matching, save, and confirm the explanation reports *"The REMOVE\_ROLE action has been performed with the following results: DELETE with Id=…"*, that the membership is gone, and that the account changed in the managed system.
{% endstep %}

{% step %}

### Prove it reached the far end

An entitlement in OpenIAM is not an account in a target. For at least one user, follow the chain: the role on the user, then the provisioning request, then the connector reply, then the account in the managed system itself. A rule that fires correctly and a target that never changed is a provisioning problem, not a rule problem — see [Configure provisioning](/administration-guide/lifecycle/configure-provisioning.md).
{% endstep %}
{% endstepper %}

### Reversing rule 1

Removing the rule does not remove what it granted. Work in this order.

To stop the rule granting anything more without disturbing existing holders: untick **Active** on `AP Specialist grant`, or set the rule Inactive **after** first making the no-match group inactive. Selecting Inactive on a live rule stores *Pending deactivation*, and the next recalculation runs the no-match group across the population — which, if you left the revoke group armed, revokes the role from everyone who ever held it.

To take the grants back as well: leave the revoke group armed and set the rule to *Pending deactivation* deliberately, preview, then run **Provision impacted users**. The rule then settles to `INACTIVE`.

To remove the rule entirely and keep the grants: make the no-match group inactive first, then delete the rule. With no active no-match group the delete is immediate and nothing is unwound. With one, the delete is converted into *Pending deletion* and becomes a mass revocation at the next recalculation.

Memberships already written stay until something removes them, and no target-side change is undone by any of this.

## Rule 2: a conditional grant with an exception

Everyone in the Finance-US organization whose employee type is *PERMANENT FULL TIME* should hold the `Finance Reporting Readers` group. A handful of people outside that population have been approved for the same access by exception, and must keep it when their department or employee type changes.

This is the case where the obvious construction destroys the exceptions. A rule with the two conditions ANDed and **Remove user from group** on no match will strip the group from every exception holder the next time they are saved, because a no-match target does not know who granted what.

The construction that works puts the exception **inside** the condition, so an exception holder keeps matching:

|                                   |                                                                                                                                                                                                                                             |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Condition**                     | A **Group 'OR'** with two children: a **Group 'AND'** of **Organizations** *Any match* Finance-US and **Employee type ID** *Equals* `PERM FULL TIME`; and an **Expression** of **Roles** *Any match* the `Finance Reporting Exception` role |
| **On match**                      | **Add user to group**, value = the `Finance Reporting Readers` group ID                                                                                                                                                                     |
| **On no match**                   | **Remove user from group**, same group ID — now safe, because exception holders match                                                                                                                                                       |
| **How an exception is granted**   | By requesting the `Finance Reporting Exception` role through the catalog, with its own approval and its own certification                                                                                                                   |
| **How an exception is withdrawn** | Remove that role. The next save fails the condition and the group is removed                                                                                                                                                                |

Four things to get right, all of which are cheap to get wrong:

**Group the conditions explicitly.** Three expressions in one flat `OR` grants the group to everyone in Finance-US, whatever their employee type. Build the `AND` as a child of the `OR`, and read the tree back before saving. The explanation states which branch decided it: *"Because of ('ROLES' ANY\_MATCH '…') returns 'true' for OR operation, expression is TRUE"*.

**The employee type is an ID.** The drop-down entry reads **PERMANENT FULL TIME**; the value stored and compared is `PERM FULL TIME`. Pick it, do not type it.

**Prefer a role to a flag for the exception.** A user attribute would work in principle — *User attributes*, name `financeReportingException`, value `true` — but the current screen writes user attributes as an unquoted pair while the engine requires the literal quoted form `"name":"value"`, and a rejected condition makes the **whole rule** unevaluable, not just that branch. The rule then resolves to `false` for everybody and the no-match target strips the group from the entire population. An exception expressed as a role uses a picker, is visible on the user's entitlements screen, is requestable, and can be certified. See [Two condition forms the current screen writes incorrectly](/administration-guide/access/business-rules/business-rule-reference.md#two-condition-forms-the-current-screen-writes-incorrectly).

**An expired exception still counts.** The collection comparison looks only at the entity IDs the user holds, ignoring start and end dates; the add and remove actions honour dates. So an exception role that expired last month still satisfies the condition and keeps the group. If exceptions must be time-bound, remove the role rather than end-dating it, and re-certify the exception role on its own schedule.

Test plan: one user matching each branch, one matching both, one matching neither, one who loses the exception role, and one whose department changes to a child of Finance-US — that last one does not match until their second save. The same pattern with its full six-user test plan is worked through in [Grant birthright access](/administration-guide/lifecycle/birthright-access.md#worked-example-2-two-attributes-and-access-that-must-survive).

## Rule 3: a rule that removes access

Users on leave of absence must lose remote access for the duration. Nothing should be restored automatically when they come back.

|                         |                                                                 |
| ----------------------- | --------------------------------------------------------------- |
| **Condition**           | One **Expression**: **User status** *Equals* `LEAVE_OF_ABSENCE` |
| **On match**            | **Remove user from group**, value = the `VPN Users` group ID    |
| **On no match**         | *(empty)*                                                       |
| **A non-matching user** | Nothing happens. An empty no-match target is skipped silently   |
| **On return**           | Nothing. Re-granting is a separate, deliberate act              |

An empty no-match target is the point of this rule, not an omission. Pointing it at **Add user to group** would turn a removal rule into a birthright rule granting VPN to every user in the system who is not on leave.

**User status** is compared by the status's own stored value, and the picker lists the ones defined on your system — the shipped set is `ACTIVE`, `INACTIVE`, `PENDING_START_DATE`, `PENDING_APPROVAL`, `APPROVAL_DECLINED`, `PENDING_USER_VALIDATION`, `PENDING_INITIAL_LOGIN`, `TERMINATED`, `RETIRED`, `LEAVE`, `LEAVE_OF_ABSENCE`, `LEAVE_WITH_PAY`, `DECEASED` and `DELETED`. *Equals* and *Is empty* are the only operators offered, which matches what the engine can do with a status.

One entry in that list is a trap: **DELETED** is offered by the picker but is not a status a user can actually hold, so a condition on it is false for everybody — and if the rule has a no-match target, false for everybody means the no-match target runs for everybody. Confirm any status value the cheap way before you rely on it: set one disposable user to that status, save, and read the actual value back out of the explanation.

Removal is unconditional on the entity ID: this rule takes `VPN Users` away from a matching user however they got it. That is what you want for a suspension, and it is the reason it must be paired with a documented restoration path rather than an automatic one.

Test plan: set a disposable user to *Leave of absence* and save — the explanation should read `STATUS: 'LEAVE_OF_ABSENCE' EQUALS 'LEAVE_OF_ABSENCE' == true.` followed by *"The REMOVE\_GROUP action has been performed with the following results: DELETE with Id=…"*. Set them back to *Active* and save — the explanation should show the rule evaluating `false` and no action, and the group should still be absent. Then run **Preview impacted users** to see how many users are on leave today and still hold the group; those are the ones the first sweep will affect.

## When two rules disagree

There is no priority contract. Rules are evaluated in whatever order storage returns them, actions run in their stored order within a group, and no screen exposes an ordering or a tie-break.

Take a rule built like rule 1 but granting the `VPN Users` group — it adds the group to everyone in Finance-US and removes it from everyone else through its no-match target — and rule 3, which removes the same group from anyone on leave. For a Finance-US user on leave the two disagree, and what happens is this:

* **A user who already holds the entitlement loses it, whichever order the rules run in.** If the add runs first it is reported as *"Action skipped for Id=… as user is already entitled to this entity"* and then the remove marks the membership for deletion; if the remove runs first, the add finds the membership present and skips. The remove wins.
* **A user who does not yet hold it depends on the order.** Add-then-remove reports *"Entity with id = … came with status ADD, but based on business rules it should be deleted"* and they end up without it. Remove-then-add reports *"Action skipped for Id=… as user is not entitled to this entity"* and they end up with it.

Put those together and you get the symptom that brings people to this page: **the entitlement is granted on one save and revoked on the next, indefinitely**, with a provisioning operation to the managed system on every flip. The audit trail shows both rules acting on the same ID in the same explanation, which is how you identify it.

The same flap happens between a rule and a universal assignment. `ASSIGN_TO_ALL` on a role or group is applied *after* the rules have run, on every add and update, and it re-adds anything that is not already on the user — so a rule that removes a universal role loses it back on the following save. See [Grant birthright access](/administration-guide/lifecycle/birthright-access.md#universal-assignment-assign_to_all).

### How to not build one

**One authoritative rule per object.** Decide, per role or group, which single mechanism owns it, and write that down somewhere the next administrator will look — the rule's own **Description** field is the best place. Before you save any rule, list every other path to that entitlement: another rule, `ASSIGN_TO_ALL`, an organization entitlement, a role that contains it, the request catalog, a synchronization.

**Fold the exceptions into the owner's condition** rather than adding a second rule to correct the first. The leave case above belongs in the owning rule as an `AND` of the qualification and a negated **User status** *Equals* `LEAVE_OF_ABSENCE` — tick the negation box on that expression, and the explanation records *"Invert answer due to negation flag. Result is …"* so you can see it worked. One rule, one truth, no ordering question.

**Give a genuinely separate policy its own object.** If suspension really is a different concern owned by a different team, let it act on a different group — `VPN Users (suspended)` — rather than fighting over the same one.

**Look for the conflict before you activate.** Rule 1's step 6 preview lists the users a rule would change. Two rules that disagree show up there as users whose explanation contains both an add and a remove for the same ID.

## Verification

| What to check                             | Where                                                                                                                                                                                                  |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The rule saved                            | Audit `BUSINESS_RULE_SAVE`; reopen and confirm status, operation and both action groups                                                                                                                |
| Why a rule did or did not fire for a user | The rule explanation: rule ID and name, per-condition text naming attribute, actual value, operator and expected values, the boolean result, which target ran, the actions performed, and elapsed time |
| Which users are pending                   | The **Out of sync users** tab                                                                                                                                                                          |
| A grant came from the rule                | The membership's tag is `BUSINESS_RULE_ASSIGNMENT`                                                                                                                                                     |
| Recalculation ran                         | Audit `BUSINESS_RULE_RECALCULATE`; the batch task's last-execution time                                                                                                                                |
| The population is settled                 | The rule's Status column reads **Active** rather than *Pending modifications*. Only a completed recalculation moves it                                                                                 |
| A no-match target is disarmed             | The explanation says *"Target … is inactive, ignore"*                                                                                                                                                  |
| A condition could not be evaluated        | The explanation contains a line beginning *"Can't evaluate condition:"* and one beginning *"Error:"* — and the no-match target ran                                                                     |
| The managed system changed                | The provisioning request and connector reply — a rule changing OpenIAM state is not the same as an account being disabled                                                                              |

The out-of-sync view and the preview are investigation aids. Neither proves that downstream provisioning will succeed.

## When it goes wrong

| Symptom                                                                                        | Cause                                                                                                                                            | Fix                                                                                                                                                                                                                    |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rule matches nobody                                                                            | A name was entered where an ID is compared                                                                                                       | Read the explanation: it prints the user's actual value next to your expected values. [Which attributes compare IDs](/administration-guide/access/business-rules/business-rule-reference.md#what-a-condition-can-test) |
| Rule matches far more users than expected                                                      | **Organizations** picked up ancestor organizations added by automatic hierarchy assignment                                                       | Condition on the specific child organization, or set `org.openiam.automatic.organization.hierarchy.assignment.enabled=false`                                                                                           |
| Everyone lost access after a condition change                                                  | The condition could not be evaluated, so its result resolved to `false` and the no-match group ran                                               | Check the explanation for `Error:` or *"out of Business Rule logic"*; never condition on **Addresses**, **Phone Numbers** or **Suffix**                                                                                |
| Explanation says "Attribute … is out of Business Rule logic"                                   | You selected **Addresses**, **Phone Numbers** or **Suffix**, none of which OpenIAM can compare                                                   | Choose an implemented attribute; there is no workaround short of a Groovy condition                                                                                                                                    |
| A blank field does not match `EQUALS ''`                                                       | String comparison requires a non-blank value on both sides                                                                                       | Use `IS_NULL`                                                                                                                                                                                                          |
| Rule never evaluates                                                                           | **Operation** is set to an operation that is not happening                                                                                       | Clear the operation, or match it to the event; the skip is logged verbatim                                                                                                                                             |
| A Groovy condition silently returns false                                                      | The script threw, and the exception was swallowed, leaving the result undefined                                                                  | Search the service log for *"Error invoking Groovy method"*; make the script exception-free                                                                                                                            |
| A rule change took effect without anyone running anything                                      | An active rule evaluates on every user save, and `PERFORM_BR_REPROVISION` may have been enabled on this system                                   | Expected for the per-save path. Keep rules Inactive while building, and check the task's enabled state                                                                                                                 |
| Manual grants keep disappearing                                                                | The rule's no-match target removes the entitlement regardless of who granted it                                                                  | Pick one authoritative mechanism per entitlement                                                                                                                                                                       |
| Deleting a rule revoked access everywhere                                                      | The rule had an active no-match group, so the delete was converted into *Pending deletion* and the no-match target ran for every user it matched | Make the no-match group inactive **before** deleting. With no active no-match group the delete is immediate and unwinds nothing                                                                                        |
| The rule says *Pending modifications* and never becomes Active                                 | Only a completed recalculation settles the status, and `PERFORM_BR_REPROVISION` ships disabled                                                   | Expected. The rule is still evaluated on every user save. Press **Provision impacted users** when you want the population swept                                                                                        |
| Setting a rule Inactive revoked access from everyone                                           | Selecting Inactive on a live rule stores *Pending deactivation*, which runs the no-match target at the next recalculation                        | Untick **Active** on the no-match action group first, then set the rule Inactive                                                                                                                                       |
| A **User attributes** condition matches nobody, and the no-match target fires                  | The current screen stores the pair unquoted; the engine needs `"name":"value"`                                                                   | Explanation shows *"Values for attribe wrongly formatter"* — the product's spelling. Build that condition on `/webconsole/br/old`, or condition on a role instead                                                      |
| A date condition never matches                                                                 | The current screen's date picker stores an ISO 8601 timestamp; the engine parses the value as epoch milliseconds                                 | Explanation shows *"Error is For input string: …"*. Build date conditions on `/webconsole/br/old`                                                                                                                      |
| An entitlement is granted on one save and revoked on the next, for ever                        | Two live mechanisms disagree about the same object                                                                                               | [When two rules disagree](#when-two-rules-disagree). Make one mechanism authoritative                                                                                                                                  |
| The preview shows users and rules you did not touch                                            | The preview evaluates **every** pending rule on the system and clears its previous results each run                                              | Coordinate. Check whose rules are in a pending status before previewing                                                                                                                                                |
| A newly transferred user does not match an **Organizations** condition until their second save | Ancestor organizations are written after the rules have run                                                                                      | Expected with automatic hierarchy assignment on. Condition on the child organization, or accept the one-save delay                                                                                                     |

## Rollback

Stop the rule acting on anyone else before anything else, and do it in the order that does not trigger a second population-wide change. **Untick Active on the no-match action group first**, then set the rule Inactive. Doing it the other way round stores the rule as *Pending deactivation* with its no-match target still armed, and the next recalculation runs that target across the population. There is no way to select a stored `INACTIVE` status directly on a rule that is currently live.

Then restore the previous condition, action groups and script IDs, press **Preview impacted users** and compare the count against the numbers you recorded before the change, and only then run **Provision impacted users**.

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

## Related

* [Business rule reference](/administration-guide/access/business-rules/business-rule-reference.md) — the condition attributes, the operators, every action's exact scope, the Groovy contracts and six verified patterns
* [Grant birthright access](/administration-guide/lifecycle/birthright-access.md) — two more worked birthright rules, and the other three mechanisms that grant access without a request
* [Manage roles and groups](/administration-guide/access/manage-roles-and-groups.md) — the memberships a rule writes
* [Shipped batch tasks](/administration-guide/reference/shipped-batch-tasks.md) — `PERFORM_BR_REPROVISION` and the rest

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