Understanding Uniqueness, Data Flow, and Real-Time Limitations in SailPoint Identity Security Cloud
Introduction
When provisioning new accounts in SailPoint Identity Security Cloud (ISC), the platform does not query the target system in real time to determine whether a generated value—such as a sAMAccountName, userPrincipalName, or email address—is unique. Instead, ISC relies on its own internal account index, which is a cached representation of the accounts that have been aggregated from each connected source.
This distinction matters. The Account Profile Attribute Generator rule (commonly called the Attribute Generator rule, or AttributeGenerator rule type) is the cloud-executed rule SailPoint provides for generating these complex provisioning values. Because it runs in the SailPoint cloud and has read-only access to the ISC data model, every uniqueness check it performs reads from the index, not from the live target system.
The Attribute Generator rule is the right place to perform the uniqueness check. SailPoint’s documentation on the Identity Attribute Rule explicitly discourages embedding uniqueness logic inside identity attribute calculations, and gives two reasons: identity attributes are recalculated whenever any input changes, so a value that was unique at creation can be regenerated and collide later; and the calculation process is multi-threaded, so the uniqueness check is not even guaranteed to be accurate when it runs. SailPoint’s recommendation is to perform the uniqueness check during account generation for the target source, what the Attribute Generator rule is designed to do.
The flow diagram and discussion below, walk through how this works in practice, where the gotchas are, and what to consider when designing a generator that scales beyond a handful of joiners per day.
How the Attribute Generator Rule Works
The Attribute Generator rule is invoked by ISC during account creation when the provisioning policy for a source has a field configured to use a rule transform. The rule receives several inputs from the platform:
• idn — a reference to sailpoint.server.IdnRuleUtil, which exposes read-only helper methods (e.g., accountExistsByDisplayName, countAccounts, attrSearchCountAccounts) that query the ISC internal account index.
• identity — the Identity object being provisioned, used for source attributes like firstname and lastname.
• application — a read-only reference to the Application/Source object the account is being created on.
• field — the Field object describing the attribute being generated.
The rule’s job is straightforward: produce a candidate value, ask the index whether that value already exists, and either return it or iterate. Behind that simple loop, however, sit several design considerations that determine whether your rule will behave correctly under load.
The Flow
At a high level, the data path looks like this:
1. A provisioning event fires (joiner, role assignment, manual request) and ISC begins building the account create payload for the target source.
2. For each provisioning policy field configured with a generator rule, the platform invokes the Attribute Generator rule, passing in the inputs above.
3. Inside the rule, the developer constructs a candidate value (e.g., firstname.lastname) and calls an IdnRuleUtil method to check uniqueness.
4. IdnRuleUtil queries the ISC internal account index for the source — not the live target.
5. If the value is unique in the index, the rule returns it. If not, the rule iterates, modifying the value (typically by appending a counter) and re-checking until it finds a unique candidate or hits a max-iteration safeguard.
6. The returned value is included in the provisioning request sent to the target connector.
7. The target system either accepts the create or rejects it (e.g., Active Directory returns “already exists”).
The critical point: between step 4 (index query) and step 7 (target system create), the index can be stale. The rule’s view of the world is only as current as the last aggregation.
Before You Write a Rule
Not every uniqueness scenario needs a cloud rule. SailPoint provides a usernameGenerator transform on the creation profile that handles the same general pattern — generate a value, check uniqueness, iterate with a counter — entirely through declarative JSON, with no BeanShell to write and no SailPoint review cycle to wait through. For straightforward cases it is the lighter, faster, more maintainable option, and it is worth checking before reaching for a rule.
The transform takes a patterns array — an ordered list of templates with placeholders like $fn, $ln, and ${uniqueCounter} — and tries each pattern in sequence until it finds a unique result. A sourceCheck boolean controls whether uniqueness is verified against the ISC index alone (the default) or against the live target via a getObject call to the connector when the source supports it. That last capability is genuinely interesting: it sidesteps the index-staleness problem the rest of this post is about, but only for connectors that can answer single-account lookups synchronously, and only at the cost of a per-provisioning round-trip to the target.
Use a transform when your username logic is simple and predictable. Transforms are best when you can describe what you want with just a fill-in-the-blanks pattern — something like “first name dot last name, and add a 1, 2, 3 at the end if it’s already taken.” If all the pieces you need (first name, last name, employee ID, etc.) already live on the identity in SailPoint, and the same rule applies to everyone the same way, a transform will do the job. There’s another nice perk: transforms are configuration, not code, so you can deploy them yourself without waiting for SailPoint to review and install anything.
Use a rule when your username logic must make decisions or handle real complications. If the right answer depends on the situation — “people in Europe get one format, people in the US get another,” or “if the last name is longer than 15 characters, chop it down to fit,” or “first check if this person already has an account on a different system before generating a new one” — that’s the kind of thinking a transform can’t really do. Rules are written in actual code (Java/BeanShell), so they can branch, loop, look up other accounts, handle errors, and reach into data the transform language can’t get to. The trade-off is that rules must be reviewed and installed by SailPoint before they go live, which adds time and process.
Example: Generating a Unique sAMAccountName
Below are examples of Attribute Generator rules that produces a unique sAMAccountName for an Active Directory source. It follows the same shape as the “Generate Username” example in the SailPoint developer documentation but adds defensive handling and dynamic source-name retrieval.
Example 1:
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule language="beanshell" name="AD sAMAccountName Generator" type="AttributeGenerator">
Generates a unique sAMAccountName for Active Directory.
<![CDATA[
import sailpoint.tools.GeneralException;
import org.apache.commons.lang.StringUtils;
int MAX_ITERATION = 1000;
int MAX_LENGTH = 20; // AD sAMAccountName limit
public String generateSam(String firstName, String lastName, int iteration) {
firstName = StringUtils.trimToNull(firstName);
lastName = StringUtils.trimToNull(lastName);
if (firstName == null || lastName == null) return null;
// Strip non-alphanumerics
firstName = firstName.replaceAll(“[^a-zA-Z0-9]”, “”).toLowerCase();
lastName = lastName.replaceAll(“[^a-zA-Z0-9]”, “”).toLowerCase();
String candidate;
if (iteration == 0) {
candidate = firstName.charAt(0) + lastName;
} else {
candidate = firstName.charAt(0) + lastName + iteration;
}
if (candidate.length() > MAX_LENGTH) {
candidate = candidate.substring(0, MAX_LENGTH);
}
if (isUnique(candidate)) {
return candidate;
} else if (iteration < MAX_ITERATION) {
return generateSam(firstName, lastName, iteration + 1);
}
return null;
}
public boolean isUnique(String candidate) throws GeneralException {
// Pull source name dynamically rather than hardcoding -
// protects against sandbox/prod naming differences and renames.
String sourceName = application.getName();
return !idn.accountExistsByDisplayName(sourceName, candidate);
}
return generateSam(identity.getFirstname(), identity.getLastname(), 0);
]]>
</Rule>
Example 2: (Just the BeanShell)
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.text.Normalizer;
import sailpoint.object.Identity;
import sailpoint.object.Application;
import sailpoint.server.IdnRuleUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
public String sanitize(String input) {
if (input == null) return “”;
String normalized = Normalizer.normalize(input, Normalizer.Form.NFD);
normalized = normalized.replaceAll(“\\p{M}”, “”);
return normalized.replaceAll(“[^a-zA-Z0-9]”, “”).toLowerCase();
}
public boolean isUnique(String candidate, Object appObj, Object idnObj) {
if (candidate == null) return false;
sailpoint.server.IdnRuleUtil idnUtil = (sailpoint.server.IdnRuleUtil) idnObj;
Application app = (Application) appObj;
List sourceIds = new ArrayList();
sourceIds.add(app.getId());
int count = idnUtil.attrSearchCountAccounts(
sourceIds,
“sAMAccountName”,
“Equals”,
Collections.singletonList(candidate)
);
return (count == 0);
}
// — Main Logic —
String finalSamAccountName = null;
if (identity != null) {
String first = sanitize(identity.getFirstname());
String last = sanitize(identity.getLastname());
String middle = sanitize((String) identity.getAttribute(“middleName”));
String middleInit = “”;
boolean hasMiddle = StringUtils.isNotBlank(middle);
if (hasMiddle) {
middleInit = middle.substring(0, 1);
}
int maxLen = 20;
String candidate = “”;
// Use Case 1: First.Last
candidate = first + “.” + last;
if (candidate.length() > maxLen) {
candidate = candidate.substring(0, maxLen);
}
if (isUnique(candidate, application, idn)) {
finalSamAccountName = candidate;
}
// Use Case 2: First.MiddleInitial.Last
else if (hasMiddle) {
candidate = first + “.” + middleInit + “.” + last;
if (candidate.length() > maxLen) {
candidate = candidate.substring(0, maxLen);
}
if (isUnique(candidate, application, idn)) {
finalSamAccountName = candidate;
}
}
// Use Case 3: Iterator (Respects Middle Name logic)
if (finalSamAccountName == null) {
// RESTORED LOGIC: Use Middle Name in base if present
String iterBase = hasMiddle
? (first + “.” + middleInit + “.” + last)
: (first + “.” + last);
for (int i = 1; i <= 30; i++) {
String suffix = String.valueOf(i);
int maxBaseLen = 20 - suffix.length();
String trimmedBase = iterBase;
if (trimmedBase.length() > maxBaseLen) {
trimmedBase = trimmedBase.substring(0, maxBaseLen);
}
candidate = trimmedBase + suffix;
if (isUnique(candidate, application, idn)) {
finalSamAccountName = candidate;
break;
}
}
}
}
if (finalSamAccountName != null) {
log.info("Generated Unique sAMAccountName: " + finalSamAccountName);
} else {
log.error("Unable to generate unique sAMAccountName for " + identity.getName());
}
return finalSamAccountName;
Note: that for the second example you would want your account creation profile to have something like the below JSON to reference the right source ID(s). This way when you move from one tenant to another, the cloud rule doesn’t have the hard coded source values. The below screenshot was taken using the SailPoint ISC vscode plugin, for the account creation provisioning policy.
Configuring and Testing the Rule
Step 1 — Develop locally with the Rule Development Kit (RDK)
SailPoint’s Rule Development Kit (RDK) is a Java project scaffold that lets you write the rule body as native Java, run unit tests against mocked IdnRuleUtil inputs, and iterate without round-tripping through your tenant. Develop and unit-test your generator locally before moving on.
Step 2 — Wrap the logic in the AttributeGenerator XML envelope
Once the Java compiles and tests pass, copy the source into the CDATA block of an AttributeGenerator rule XML file (see the example above). Make sure the rule type is exactly AttributeGenerator and that the file is UTF-8 encoded.
Step 3 — Submit the rule for SailPoint review
Cloud-executed rules cannot be uploaded directly through the Admin UI. They must be reviewed and installed by SailPoint. Submit a support ticket through the SailPoint Support portal (or email support@sailpoint.com) and attach: the rule XML, the validator output, your tenant name (e.g., acme-sb.identitynow.com for sandbox or acme.identitynow.com for production), and approval for Expert Services to proceed. Most rules are reviewed within 24 hours.
Step 4 — Wire the rule to a provisioning policy field
After SailPoint installs the rule, log into your tenant and navigate to Admin → Connections → Sources → [Your AD Source] → Accounts → Create Account. Select the attribute (e.g., sAMAccountName), choose the Generator radio option, and pick your rule from the dropdown.
Step 5 — Trigger a test provisioning event
In a sandbox tenant, the simplest test is to assign a role with an AD birthright to a test identity that does not yet have an account on the source. Watch the resulting provisioning task: the generated value will appear in the Account Create payload. Repeat with a second identity whose generated value would collide to confirm the iterator branch fires correctly.
Step 6 — Validate after aggregation
After the test account is created on the target system, run an account aggregation. The new account will appear in the ISC index, and subsequent rule executions will see it. This is the moment when the freshness gap closes — until aggregation runs, the index does not know about the account just created.
Q&A
1. Is uniqueness scoped per AD source, or across a domain/forest?
By default, the query runs against the index of a specific source. In a multi-AD scenario — where you need uniqueness across several connectors representing the same forest or namespace — use idn.attrSearchCountAccounts, which accepts a List of Source IDs and searches across all of them.
2. Which IdnRuleUtil method should I use, and how is it identified — by source name or source ID?
Two distinct methods cover the two distinct cases. Per the IdnRuleUtil descriptors:
• Single-source uniqueness: accountExistsByDisplayName(applicationName, displayName) or accountExistsByNativeIdentity(applicationName, nativeIdentity). Both take an application name (not ID). The SailPoint docs explicitly recommend pulling that name dynamically with application.getName() rather than hardcoding a literal — a hardcoded source name silently returns false (“doesn’t exist”) after a rename or in a sandbox-vs-production split, turning your uniqueness check into a no-op.
• Multi-source uniqueness: attrSearchCountAccounts(sourceIds, attributeName, operation, values). This one takes a List of source IDs, which are stable across renames. It does not, however, work out of the box — see the prerequisites note below.
• LDAP-style live lookup (situational): isUniqueLDAPValue(identityNameOrId, applicationNameOrId, attributeName, attributeValue). Unlike the methods above, this one bypasses the index entirely and asks the connector to run an LDAP-filter-style search against the live target. When it works, it sidesteps the staleness problem this whole post is about. Its constraint: it requires the underlying connector to be reachable from the rule context at execution time, which a cloud-only rule path does not always allow. Treat it as worth knowing about and worth testing in your specific source/connector combination, but not as a default for cloud-executed rules — if the connector lookup fails or times out, the call throws IllegalStateException and your provisioning attempt fails with it.
A common point of confusion: idn.countAccounts(applicationName) is not a uniqueness method. Its descriptor signature takes a single argument and returns the total account count for a source. It cannot filter by attribute or value, so it is not a substitute for the methods above.
Prerequisite for attrSearchCountAccounts: this method only works against attributes you have explicitly registered as searchable via the /beta/accounts/search-attribute-config API. After creating the configuration, you must run a non-optimized (full) aggregation on every source involved so the promoted attribute values are seeded onto existing accounts — a delta aggregation will not backfill them. If you skip this step, the count returns zero regardless of what’s in the index, which is the same silent-failure pattern as the hardcoded source name. Plan the search-attribute setup before relying on this method in production.
3. Does the search include uncorrelated accounts?
Yes. The ISC internal account index for a source contains every account that has been aggregated from that source, regardless of whether the account is currently correlated to an identity. Orphaned and uncorrelated accounts are visible to the uniqueness check, which is usually what you want — you don’t want to hand out a sAMAccountName that already physically exists in AD just because no identity is currently attached to it.
4. What about concurrency? Can two parallel joiners both see count==0?
Yes, this is a genuine race condition that the architect needs to plan for. If two provisioning events fire close together and both rule invocations check the index before either create completes and aggregates, both can return the same “unique” value.
The most common production-grade defense is to base the value on something already-unique-by-construction. Many customers use the employee ID (or a derivative) for sAMAccountName and UPN, and include it as part of the distinguished name. The rule logic still falls back to a counter, but races become near-impossible because the input itself is unique per identity.
5. What aggregation cadence is practical?
It depends on the dynamic nature and scale of the environment — number of identities, churn rate, and how filtered the AD aggregation is. As a general rule, do not run delta aggregations more frequently than every 15 minutes. Going faster tends to back up the queue and degrade overall tenant performance without meaningfully tightening the freshness window.
6. What if the target still returns “already exists”?
Handle the fallback at the workflow / error-handling layer rather than inside the rule. A rule-level retry can mask the underlying race; a workflow path can be logged, assigned to a human owner, and re-run cleanly.
In practice: if the AD aggregation runs before the daily end-of-day Identity Refresh, the account typically gets created on the next pass without intervention. The workflow path is the safety net for the cases where it doesn’t.
Real-World Scenarios and Edge Cases
Here are four scenarios from real implementations where it gets messy, and how to handle each:
Scenario 1 — The bulk joiner Monday
A university onboards 200 new staff on the same Monday morning. All 200 share a common provisioning trigger. Without an employee-ID-based naming convention, the race condition described in Q4 will produce duplicate sAMAccountName candidates. Example: two persons with the same name of “Maria Garcia” would collide.
Mitigation: bake the employee ID into the sAMAccountName seed (e.g., first initial + last name + last 4 of employee ID), so collisions are statistically rare even before the iterator runs.
A common best practice is to change your naming convention to have the sAMAccountName set to an employee number as it is generally unique and you don’t have to consider possible character length issues.
Scenario 2 — The orphaned account collision
A former employee’s AD account was disabled but never deleted. A new hire with a similar name is provisioned, the rule picks the same sAMAccountName, and the index check correctly fails — but only because the orphan is still in the index. If someone had purged the orphan from ISC without deleting from AD, the check would have passed and the target would have rejected the create. Mitigation: keep aggregation and target-side hygiene aligned; do not remove accounts from ISC unless they are also gone from AD.
Scenario 3 — Multi-domain forest with overlapping namespaces
A merger results in two AD sources representing two domains in the same forest, where UPN must be unique across both. A single-source countAccounts check will let through a value already in use in the other domain. Mitigation: use idn.attrSearchCountAccounts with a List of source IDs covering all relevant domains (see Q1).
Scenario 4 — The two Identity classes (namespace conflict)
Inside an ISC cloud rule there are two classes named Identity, and they are not interchangeable. sailpoint.object.Identity is the SailPoint context class — this is what the implicit identity input variable resolves to in an Attribute Generator rule. sailpoint.rule.Identity is a different DTO returned by IdnRuleUtil methods such as idn.getIdentityById() and idn.findIdentitiesBySearchableIdentityAttribute(). A wildcard import like import sailpoint.object.*; plus a method that returns the rule-package version creates an ambiguous reference, which BeanShell often won’t catch until runtime.
Mitigation: when you need to declare an Identity-typed variable, fully qualify it. Use sailpoint.object.Identity for the input identity passed into the rule, and sailpoint.rule.Identity for any object you receive back from an IdnRuleUtil call. The example code in this post sidesteps the issue entirely by calling identity.getFirstname() and identity.getLastname() directly on the input rather than declaring a new typed variable — a pattern worth following whenever you don’t actually need the explicit type.
References — SailPoint Documentation
• Account Profile Attribute Generator — https://developer.sailpoint.com/docs/extensibility/rules/cloud-rules/account-profile-attribute-generator/
• Account Profile Attribute Generator (from Template) — https://developer.sailpoint.com/docs/extensibility/rules/cloud-rules/account-profile-attribute-generator-template/
• Cloud Executed Rules — Overview — https://developer.sailpoint.com/docs/extensibility/rules/cloud-rules/
• Your First Rule (Guide) — https://developer.sailpoint.com/docs/extensibility/rules/guides/your-first-rule/
• Identity Security Cloud Rule Utility (IdnRuleUtil) — https://developer.sailpoint.com/docs/extensibility/rules/rule-utility
• Rule Development Kit (RDK) — https://developer.sailpoint.com/docs/tools/rule-development-kit/
• Identity Attribute Rule (best practice on uniqueness checks) — https://developer.sailpoint.com/docs/extensibility/rules/cloud-rules/identity-attribute-rule/
• Username Generator Transform — https://developer.sailpoint.com/docs/extensibility/transforms/operations/username-generator/
• Rules Java Docs (announced for IdentityNow) — https://developer.sailpoint.com/docs/extensibility/rules/java-docs


