Most SailPoint IdentityIQ rule tutorials stop at the three rule types you write in your first month: BuildMap, Correlation, and IdentityAttribute. Our own BeanShell Rules, Workflows, and Tasks guide covers those in depth, plus the companion repo with working templates for all four. This cookbook picks up where that guide leaves off, with two rule types that show up later in a real deployment and have far fewer working examples floating around: Provisioning rules, which reshape what actually gets sent to a target system, and Certification rules, which control what a reviewer sees during an access review.

Clone the companion repo: Working XML templates for every rule type in this article are at IAMDevBox/sailpoint-iiq-rule-cookbook.

Before and After Provisioning Rules

Every provisioning operation in IdentityIQ — a role assignment, an entitlement request, a disable — becomes a ProvisioningPlan object that IdentityIQ hands to a connector’s provision() method. Before and After Provisioning rules are your two hooks into that hand-off, and they are configured per-application on the Rules tab of the Edit Application page, not globally.

Before Provisioning: Reshaping the Plan

A Before Provisioning rule runs immediately before provision() is called. It receives four arguments and returns nothing — you mutate the plan object directly:

ArgumentTypePurpose
logorg.apache.log4j.LoggerLogger scoped to the rule
contextsailpoint.api.SailPointContextDatabase access if you need to look something up
plansailpoint.object.ProvisioningPlanThe plan about to be sent to the connector
applicationsailpoint.object.ApplicationThe application this plan targets

The canonical use case is a connector or target system that does not support an operation IdentityIQ wants to perform. Directory-style systems are the most common offender: many do not support a true “disable,” so the standard fix is converting Disable/Enable operations into a Modify that flips a status attribute instead:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule language="beanshell" name="Convert Disable to Status Flag" type="BeforeProvisioning">
  <Description>
    Some target systems have no native disable operation. Convert Disable/Enable
    account requests into a Modify that sets an ACCOUNT_STATUS attribute instead,
    so provisioning does not fail with an UnsupportedOperationException.
  </Description>
  <Signature returnType="void">
    <Inputs>
      <Argument name="log" type="org.apache.log4j.Logger"/>
      <Argument name="context" type="sailpoint.api.SailPointContext"/>
      <Argument name="plan" type="sailpoint.object.ProvisioningPlan"/>
      <Argument name="application" type="sailpoint.object.Application"/>
    </Inputs>
  </Signature>
  <Source><![CDATA[
    import sailpoint.object.ProvisioningPlan;
    import sailpoint.object.ProvisioningPlan.AccountRequest;
    import sailpoint.object.ProvisioningPlan.AttributeRequest;
    import sailpoint.object.ProvisioningPlan.ObjectOperation;
    import java.util.List;

    List requests = plan.getAccountRequests();
    if (requests == null) {
        return;
    }

    for (int i = 0; i < requests.size(); i++) {
        AccountRequest req = (AccountRequest) requests.get(i);

        if (req.getOp() == ObjectOperation.Disable) {
            log.debug("Converting Disable to status-flag Modify for " + req.getNativeIdentity());
            req.setOp(ObjectOperation.Modify);
            req.add(new AttributeRequest("ACCOUNT_STATUS", ProvisioningPlan.Operation.Set, "0"));
        } else if (req.getOp() == ObjectOperation.Enable) {
            req.setOp(ObjectOperation.Modify);
            req.add(new AttributeRequest("ACCOUNT_STATUS", ProvisioningPlan.Operation.Set, "1"));
        }
    }
  ]]></Source>
</Rule>

Two details that cost people real debugging time:

  • This rule mutates in place. There is no return value to wire up — <Signature returnType="void"> is correct, and IdentityIQ uses whatever state plan is in when the rule finishes, not anything you return.
  • Attach it per application. Before/After Provisioning rules are set on the Edit Application page’s Rules tab, one application at a time. If several applications need the same conversion logic, either attach the rule to each one or branch inside the rule on application.getName().

After Provisioning: Reacting to the Result

An After Provisioning rule runs once provision() returns — but only when the ProvisioningResult status is Committed or Queued, so a rejected or failed request does not trigger it. It receives everything the Before rule does, plus result:

Argument name="result" type="sailpoint.object.ProvisioningResult"

The most common use is notification: telling a user their account was created, or alerting an owner when a privileged entitlement is granted. Because failures never reach an After Provisioning rule, error notifications need a separate mechanism — typically a workflow-level failure handler, not this rule type.

<Rule language="beanshell" name="Notify Manager on Privileged Grant" type="AfterProvisioning">
  <Signature returnType="void">
    <Inputs>
      <Argument name="log" type="org.apache.log4j.Logger"/>
      <Argument name="context" type="sailpoint.api.SailPointContext"/>
      <Argument name="plan" type="sailpoint.object.ProvisioningPlan"/>
      <Argument name="application" type="sailpoint.object.Application"/>
      <Argument name="result" type="sailpoint.object.ProvisioningResult"/>
    </Inputs>
  </Signature>
  <Source><![CDATA[
    import sailpoint.object.Identity;
    import sailpoint.object.EmailTemplate;
    import sailpoint.object.EmailOptions;
    import java.util.HashMap;

    if (result == null || !result.isCommitted()) {
        return;
    }

    String identityName = plan.getIdentity();
    if (identityName == null) {
        return;
    }

    Identity identity = context.getObjectByName(Identity.class, identityName);
    if (identity == null) {
        return;
    }

    Identity manager = identity.getManager();
    if (manager == null || manager.getEmail() == null) {
        return;
    }

    EmailTemplate template = context.getObjectByName(EmailTemplate.class, "Privileged Grant Notification");
    if (template == null) {
        log.warn("Privileged Grant Notification email template not found");
        return;
    }

    HashMap vars = new HashMap();
    vars.put("identityName", identity.getDisplayableName());
    vars.put("applicationName", application.getName());

    EmailOptions options = new EmailOptions(manager.getEmail(), vars);
    context.sendEmailNotification(template, options);
  ]]></Source>
</Rule>

result.isCommitted() is the guard that matters most here — skipping it means the rule fires on partial or queued results where the target system has not actually confirmed the change yet.

Certification Exclusion Rules

Certification campaigns get noisy fast: a manager reviewing forty direct reports does not need to re-approve entitlements a role already justifies, or accounts that were disabled the day the campaign launched. An Exclusion Rule, configured under Certifications → Certification Schedule → Advanced Settings, filters those items out before the reviewer ever sees them.

An exclusion rule receives the identity under review and the list of certifiable items being considered for that identity, and returns the subset that should be removed:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule language="beanshell" name="Exclude Disabled Accounts and Role-Justified Entitlements" type="CertificationExclusion">
  <Description>
    Removes certifiable items for accounts that are currently disabled, and
    entitlements that are already granted indirectly through an assigned role
    (they get reviewed as part of the role certification instead).
  </Description>
  <Signature returnType="java.util.List">
    <Inputs>
      <Argument name="context" type="sailpoint.api.SailPointContext"/>
      <Argument name="identity" type="sailpoint.object.Identity"/>
      <Argument name="certifiableEntities" type="java.util.List"/>
    </Inputs>
  </Signature>
  <Source><![CDATA[
    import sailpoint.object.AbstractCertifiableEntity;
    import sailpoint.object.Certifiable;
    import sailpoint.object.EntitlementCertifiable;
    import sailpoint.object.EntitlementSnapshot;
    import java.util.ArrayList;
    import java.util.List;

    List excluded = new ArrayList();

    if (certifiableEntities == null) {
        return excluded;
    }

    for (int i = 0; i < certifiableEntities.size(); i++) {
        Object entry = certifiableEntities.get(i);

        // Only EntitlementCertifiable items carry role/link detail we can inspect;
        // other Certifiable subtypes (e.g. BundleCertifiable) pass through untouched.
        if (!(entry instanceof EntitlementCertifiable)) {
            continue;
        }

        EntitlementCertifiable ec = (EntitlementCertifiable) entry;
        EntitlementSnapshot snap = ec.getEntitlements();

        if (snap == null) {
            continue;
        }

        // Skip entitlements already covered by an assigned business role —
        // they get reviewed once, at the role level, not once per entitlement.
        if (snap.getApplication() != null && identity.getAssignedRoles() != null) {
            boolean coveredByRole = false;
            for (int r = 0; r < identity.getAssignedRoles().size(); r++) {
                if (identity.getAssignedRoles().get(r).getName().equals(snap.getSourceRole())) {
                    coveredByRole = true;
                    break;
                }
            }
            if (coveredByRole) {
                excluded.add(entry);
            }
        }
    }

    return excluded;
  ]]></Source>
</Rule>

Two things worth knowing before writing your own:

  • Return only what to remove, never rebuild the full list. The rule contract is subtractive — it returns items to drop, not the surviving set. Returning null or an empty list means nothing gets excluded.
  • certifiableEntities mixes types. Depending on the certification type (manager, application-owner, role, entitlement-owner), the list can contain EntitlementCertifiable, BundleCertifiable, or PolicyViolationCertifiable objects. Guard with instanceof before casting, exactly as the example does — a blind cast on the wrong subtype throws a ClassCastException that aborts certification generation for the entire campaign, not just one identity.

Staging an Exclusion Rule Before It Runs Against a Live Campaign

An exclusion rule fails quietly in the worst way: if it over-excludes, reviewers simply never see the missing items, and nothing in the UI tells them something was filtered. There is no error, no warning banner — just an access review that looks complete but was not. Two habits keep this from reaching production:

  • Run the campaign as a “Staged” certification first. Staged certifications generate the certification objects without activating them or notifying reviewers, so you can inspect CertificationEntity and CertificationItem counts against a version of the same campaign generated without the exclusion rule attached, and diff the two.
  • Log every exclusion, not just the final count. Add a log.info line inside the loop before adding to excluded, including the entitlement or account identifier. A campaign that unexpectedly excludes zero items is often the same bug as one that excludes everything — usually a null check that always short-circuits — and the log is the only way to tell which failure mode you are looking at without re-running the whole campaign generation.

Finding the Argument List for Any Other Rule Type

IdentityIQ ships more than 70 rule types, and this cookbook plus the BeanShell Rules, Workflows, and Tasks guide only cover the ones developers hit most often. For anything else — Refresh rules, Populate rules on forms, Managed Attribute customization — the fastest path is still IdentityIQ_HOME/WEB-INF/config/examplerules.xml on your own instance. It contains a working example of every type with the real, current argument list for your installed version, which matters because argument sets do change between major releases. Cross-check anything you find against the <Signature> block, not just the <Source> body — the signature is what tells you which arguments are guaranteed non-null.

Once you have a rule drafted, the iiq console lets you import and test it without a UI round-trip:

> import /path/to/my-rule.xml
> rule "Convert Disable to Status Flag" {plan=..., application=...}

Building the test arguments interactively is easier from the BeanShell console than the iiq console rule command for anything beyond a trivial signature — see the Debugging Rules section of the BeanShell guide for the full workflow, including log4j2 debug logging that does not require an application server restart.

For the operational side of running these rules in production — diagnosing why an aggregation using a BuildMap or Correlation rule stalled — see SailPoint IdentityIQ Aggregation Troubleshooting, and for the underlying Java/MySQL/shell tooling this whole series builds on, Java, MySQL, and Shell Scripting for SailPoint IdentityIQ.