SailPoint IdentityIQ ships with three extension points where you write code: rules (BeanShell scripts that compute a value), workflows (XML state machines that orchestrate multi-step processes), and tasks (scheduled jobs that operate on data in bulk). Almost every IdentityIQ customization you will ever build fits into one of those three. This guide covers what each one is for, the API you use inside them, and the failure modes that cost new IdentityIQ developers the most time.

If you are coming from a different IAM platform, the closest analogue is scripted customization in ForgeRock — see our ForgeRock AM script customization guide for a comparison of how the two platforms approach the same problem.

Choosing the Right Extension Point

Before writing anything, pick the correct mechanism. Choosing wrong is the most expensive mistake in IdentityIQ development, because migrating logic from a rule to a workflow later means rewriting it entirely.

You need to…UseRuns
Transform an attribute valueRuleSynchronously, in-process
Match an account to an identityCorrelation RuleDuring aggregation
Decide who approves a requestWorkflowAsynchronously, may pause for days
Process every identity in bulkTaskOn a schedule
Modify data on its way to a target systemProvisioning RuleDuring provisioning

The dividing line between a rule and a workflow is whether the logic can pause. A rule runs start to finish in a single thread and returns one value. If your logic needs to wait for a human, it must be a workflow.

BeanShell: The Language IdentityIQ Actually Runs

IdentityIQ rules are written in BeanShell, a scripting language that interprets Java syntax at runtime. This is the single most important thing to understand about IdentityIQ development, because BeanShell’s differences from Java cause the majority of production rule failures.

What BeanShell Does Not Support

BeanShell implements Java syntax as of roughly Java 1.4. The following will fail:

// GENERICS — not supported. This throws a parse error.
List<String> names = new ArrayList<String>();

// Correct: use raw types
List names = new ArrayList();

// LAMBDAS and streams — not supported
names.stream().filter(n -> n.startsWith("a"));

// Correct: use an explicit loop
for (int i = 0; i < names.size(); i++) {
    String n = (String) names.get(i);
    if (n.startsWith("a")) { /* ... */ }
}

// ANNOTATIONS — not supported
@Override
public String toString() { }

Loose Typing Hides Bugs Until Runtime

BeanShell lets you declare variables without a type. This is convenient and dangerous:

// Both are legal in BeanShell
String name = identity.getName();
name = identity.getName();

Because the script is interpreted, a misspelled method name compiles fine and fails only when that specific branch executes. A rule that works in your test case can fail six months later the first time an identity hits an untested code path. Two defenses matter:

  1. Always declare types explicitly. It does not make BeanShell check them at parse time, but it documents intent and catches cast errors sooner.
  2. Validate rules before deploying. The iiq console has a syntax checker — see the console section below.

Null Safety Is Entirely Your Job

IdentityIQ getters return null constantly. An identity may have no manager, a link may have no attribute, an application may not be assigned. Defensive null checks are not optional:

import sailpoint.object.Identity;

Identity manager = identity.getManager();
String managerEmail = null;

if (manager != null) {
    managerEmail = manager.getStringAttribute("email");
}

if (managerEmail == null || managerEmail.trim().length() == 0) {
    managerEmail = "[email protected]";  // fallback
}

return managerEmail;

Rules: The Most Common Extension Point

A rule is a Rule object stored in the database, containing a BeanShell script and a declared type. The type determines which arguments IdentityIQ passes in, and this is where most confusion lives — every rule type receives a different set of variables.

Anatomy of a Rule

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule language="beanshell" name="Example Manager Email Rule" type="IdentityAttribute">
  <Description>
    Returns the manager's email address, falling back to a governance mailbox.
  </Description>
  <Signature returnType="String">
    <Inputs>
      <Argument name="identity" type="sailpoint.object.Identity"/>
      <Argument name="context" type="sailpoint.api.SailPointContext"/>
    </Inputs>
  </Signature>
  <Source><![CDATA[
    import sailpoint.object.Identity;

    if (identity == null) {
        return null;
    }

    Identity manager = identity.getManager();
    if (manager == null) {
        return "[email protected]";
    }

    return manager.getStringAttribute("email");
  ]]></Source>
</Rule>

Three details matter here:

  • The <Source> must be wrapped in CDATA. Without it, any <, >, or & in your code breaks the XML parse.
  • The type attribute is not cosmetic. It controls the input arguments and where the rule appears in the UI dropdowns.
  • <Signature> is documentation, not enforcement. BeanShell does not validate arguments against it. Declaring an argument that IdentityIQ does not actually pass yields null at runtime, not an error.

Rule Types You Will Actually Write

IdentityIQ defines dozens of rule types. In practice, a small handful cover most work:

BuildMap — Runs once per row of incoming data during aggregation, converting a raw record into a Map of attributes. Required by the JDBC connector, and used heavily with delimited-file connectors. The record variable holds the incoming data:

import java.util.HashMap;

HashMap resultMap = new HashMap();

for (int i = 0; i < cols.size(); i++) {
    String colName = (String) cols.get(i);
    Object value = record.get(colName);
    resultMap.put(colName, value);
}

// Derive a value that does not exist in the source
String status = (String) resultMap.get("EMP_STATUS");
resultMap.put("isActive", "1".equals(status) ? "true" : "false");

return resultMap;

Correlation — Decides which identity an account belongs to when a simple attribute match is not enough. Returns a Map naming the identity attribute to match on:

import java.util.HashMap;

HashMap result = new HashMap();
String employeeId = (String) account.getAttribute("employeeNumber");

if (employeeId != null && employeeId.length() > 0) {
    // Strip a legacy prefix before matching
    if (employeeId.startsWith("E-")) {
        employeeId = employeeId.substring(2);
    }
    result.put("identityAttributeName", "employeeId");
    result.put("identityAttributeValue", employeeId);
}

return result;

IdentityAttribute — Computes a value for an identity attribute during the Identity Refresh task. Receives identity and, for some configurations, link.

Provisioning / BeforeProvisioning / AfterProvisioning — Modify a ProvisioningPlan on its way to a target system. The canonical use case is translating IdentityIQ’s values into whatever encoding the target expects — for instance converting "Full" to the numeric code 1.

Certification — Filter or pre-decide certification items, typically to auto-approve low-risk entitlements so reviewers only see what matters.

Find the exact arguments for any rule type in IdentityIQ_HOME/WEB-INF/config/examplerules.xml. This file contains a working example of every rule type with its real input arguments, and it is more reliable than the documentation for this specific question.

Rule Libraries Prevent Copy-Paste Sprawl

Do not duplicate helper logic across twenty rules. Put shared functions in a rule of type null and reference it:

<Rule language="beanshell" name="Example Rule Library" type="null">
  <Source><![CDATA[
    public static String normalizeDepartment(String raw) {
        if (raw == null) return "UNKNOWN";
        return raw.trim().toUpperCase().replaceAll("[^A-Z0-9]", "_");
    }
  ]]></Source>
</Rule>

Then in any consuming rule:

<Rule language="beanshell" name="Department Attribute Rule" type="IdentityAttribute">
  <ReferencedRules>
    <Reference class="sailpoint.object.Rule" name="Example Rule Library"/>
  </ReferencedRules>
  <Source><![CDATA[
    return normalizeDepartment(identity.getStringAttribute("dept"));
  ]]></Source>
</Rule>

The SailPointContext API

SailPointContext is your handle to the database. Nearly every rule receives it as context. Four operations cover most usage:

import sailpoint.object.Identity;
import sailpoint.object.Application;
import sailpoint.object.Filter;
import sailpoint.object.QueryOptions;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

// 1. Fetch a single object by name
Identity user = context.getObjectByName(Identity.class, "jdoe");

// 2. Fetch by ID
Application app = context.getObjectById(Application.class, appId);

// 3. Query with filters
QueryOptions qo = new QueryOptions();
qo.addFilter(Filter.eq("inactive", new Boolean(false)));
qo.addFilter(Filter.like("department", "Engineering", Filter.MatchMode.START));
List identities = context.getObjects(Identity.class, qo);

// 4. Save changes — BOTH calls are required
user.setAttribute("riskTier", "HIGH");
context.saveObject(user);
context.commitTransaction();

Use Projection Queries for Bulk Reads

context.getObjects() hydrates every full object into memory. Against a large identity cube this will exhaust the heap. When you only need a few fields, use a projection query, which returns an iterator over Object[] rows and streams results:

import sailpoint.object.QueryOptions;
import sailpoint.object.Identity;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;

QueryOptions qo = new QueryOptions();
qo.addFilter(Filter.eq("inactive", new Boolean(false)));

List props = new ArrayList();
props.add("id");
props.add("name");

Iterator it = context.search(Identity.class, qo, props);
int count = 0;
while (it.hasNext()) {
    Object[] row = (Object[]) it.next();
    String id = (String) row[0];
    String name = (String) row[1];
    count++;
}
return "Processed " + count + " identities";

For long-running loops, call context.decache() periodically to clear the Hibernate session, or memory will grow until the task fails.

Workflows: Orchestrating Processes That Pause

A workflow is an XML state machine. It exists because rules cannot wait. When a user requests access and a manager must approve it, the process may sit idle for days — the workflow persists to a WorkflowCase row and resumes when the approval arrives, surviving application restarts.

Steps and Transitions

<Workflow name="Example Access Request Approval" type="LCMProvisioning">
  <Variable name="identityName" input="true"/>
  <Variable name="plan" input="true"/>
  <Variable name="approvalDecision"/>

  <Step name="Start" icon="Start">
    <Transition to="Evaluate Risk"/>
  </Step>

  <Step name="Evaluate Risk" resultVariable="riskLevel">
    <Script><![CDATA[
      import sailpoint.object.Identity;
      Identity id = context.getObjectByName(Identity.class, identityName);
      if (id != null && id.getScore() != null && id.getScore().intValue() > 500) {
          return "HIGH";
      }
      return "LOW";
    ]]></Script>
    <Transition to="Manager Approval" when='riskLevel == "HIGH"'/>
    <Transition to="Auto Approve"/>
  </Step>

  <Step name="Manager Approval">
    <Approval mode="serial" owner="script:...">
      <Arg name="workItemDescription" value="Approve access request"/>
    </Approval>
    <Transition to="Provision"/>
  </Step>

  <Step name="Auto Approve">
    <Transition to="Provision"/>
  </Step>

  <Step name="Provision" action="call:provisionProject">
    <Transition to="Stop"/>
  </Step>

  <Step name="Stop" icon="Stop"/>
</Workflow>

Key mechanics:

  • <Transition> order matters. They are evaluated top to bottom and the first matching when wins. Always place a bare <Transition> last as the default branch, or the workflow will dead-end.
  • resultVariable captures a step’s return value into a workflow variable usable by later steps and transition conditions.
  • Variables marked input="true" are supplied by the caller. Everything else starts null.

Debugging Workflows

Workflows fail silently more often than rules do, because a failed transition simply stops the case. Two techniques:

  1. Enable workflow trace. Add <Arg name="trace" value="true"/> to the workflow, and step-by-step execution prints to stdout — usually catalina.out on Tomcat.
  2. Inspect the stuck case. In the iiq console: list WorkflowCase then checkout WorkflowCase "<name>" /tmp/case.xml to see exactly which step it halted on and the state of every variable.

Tasks: Scheduled Bulk Operations

Tasks are TaskDefinition objects run on a schedule. The built-ins cover most needs — Account Aggregation pulls accounts from a source, Identity Refresh recalculates attributes, roles, and risk scores across the identity cube.

When you need behaviour the built-ins do not provide, write a custom task executor in Java (not BeanShell) by implementing TaskExecutor:

package com.example.iiq.task;

import sailpoint.api.SailPointContext;
import sailpoint.object.Attributes;
import sailpoint.object.TaskResult;
import sailpoint.object.TaskSchedule;
import sailpoint.task.AbstractTaskExecutor;

public class DormantAccountTask extends AbstractTaskExecutor {

    private boolean terminated = false;

    public void execute(SailPointContext context, TaskSchedule schedule,
                        TaskResult result, Attributes args) throws Exception {

        int threshold = args.getInt("dormantDays", 90);
        int processed = 0;

        // ... query and process identities, checking terminated each iteration

        result.setAttribute("identitiesProcessed", new Integer(processed));
        result.setAttribute("dormantThreshold", new Integer(threshold));
    }

    public boolean terminate() {
        this.terminated = true;
        return true;
    }
}

Compile this into a JAR, drop it in IdentityIQ_HOME/WEB-INF/lib/, restart the application server, and register it with a TaskDefinition XML pointing at the class name.

Always honour terminate(). A task that ignores it cannot be stopped from the UI, and an administrator’s only remaining option is restarting the application server.

The iiq Console

The console is where you deploy, inspect, and debug. Launch it from IdentityIQ_HOME/WEB-INF/bin:

./iiq console          # Linux/macOS
iiq.bat console        # Windows

It requires the System Administrator capability and authenticates as spadmin by default. The commands you will use constantly:

>>>>>>iclgrwmhieuapestlroctenrkItoRd"uueM/tlnypetaRiRtutuhlyl/eetj"o"d/MoryeulReusl.ex"ml/tmp/r.xml######deepesexnrxhppuieolomncworetuytrtraaeeaftncnoeaeronoobrtbrbjujejelweveceacictrtetinwsanistnooegrfXrsMamaLcitgcirlvaaetslisyon

checkout plus import is the migration path between environments. Export from dev, commit the XML to version control, import into test.

Logging and Debugging

Configure logging in IdentityIQ_HOME/WEB-INF/classes/log4j2.properties. IdentityIQ picks up changes to this file automatically within roughly 60 seconds — no application restart required, which is the single biggest time-saver in IdentityIQ debugging.

# Namespace your rule logging so you can raise it without drowning in output
logger.customrules.name = com.example.iiq
logger.customrules.level = debug

# Useful built-in loggers
logger.connector.name = sailpoint.connector
logger.connector.level = debug

logger.workflow.name = sailpoint.workflow
logger.workflow.level = debug

Inside a rule:

import org.apache.log4j.Logger;

Logger log = Logger.getLogger("com.example.iiq.correlation");
log.debug("Correlating account: " + account.getNativeIdentity());

Never use System.out.println() in production rules. It writes to the container log with no level control, no namespace, and no way to disable it without a code change and restart.

Deployment Practices That Prevent Outages

Version-control the XML, not the database. IdentityIQ objects live in the database, which makes them invisible to Git by default. Export every custom rule, workflow, and task definition with checkout and commit the XML. Without this, a database refresh silently destroys work.

Never edit rules in production through the UI. The debug pages allow direct object editing, which creates changes that exist in exactly one environment and are lost on the next deployment.

Test correlation rules against real edge cases — accounts with null employee IDs, duplicate IDs, service accounts that should match nothing. A correlation rule that throws an exception aborts the entire aggregation run, not just the one account.

Keep rules short. A rule doing substantial work belongs in a compiled Java class in WEB-INF/lib/, called from a thin BeanShell wrapper. You get compile-time type checking, real unit tests, and a debugger.

Where This Fits in Broader Identity Governance

Rules, workflows, and tasks are the mechanics. What you build with them is governance — access certification, joiner-mover-leaver automation, separation-of-duties enforcement. For the strategic layer above this code, see our guide to identity governance in the Zero Trust era, and for where the platform is heading, SailPoint’s extension of governance to AI agents.

The infrastructure underneath — the Java runtime, the MySQL schema your queries hit, and the shell scripts that automate deployment — is covered in the companion article on Java, MySQL, and shell scripting for SailPoint IdentityIQ.