> 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/platform/sequence-generators.md).

# Define a sequence generator

Create a named cluster-wide counter and call it from a script, so a synchronization or a policy map can invent an identifier that never repeats.

## What this is, and when you need it

A sequence generator is a named integer that OpenIAM increments atomically across every node in the cluster. You need one when a script has to invent an identifier that must never repeat — an employee number, a badge number, the local part of a generated email address — and the guarantee has to hold while two feeds run at once. Creating it takes a minute on one screen; the work is in the script that calls it.

This is not the mechanism that appends `1`, `2`, `3` to a colliding login name. That one is automatic — see [Login collisions use generators too](#login-collisions-use-generators-too).

## At a glance

|                          |                                                                                                                                   |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | A named generator listed on **Sequence Generators**, and a script that reads a new, never-repeated number from it on every call   |
| **Where**                | **Administration → Sequence generators**, the screen at `/webconsole/counter`                                                     |
| **You need first**       | A key of 32 characters or fewer, the number you want issued first, and edit rights on the script that will call it                |
| **Takes**                | 5 minutes for the generator, longer for the script and one test run                                                               |
| **Reversible**           | Partly — the generator can be deleted, but deletion restarts the series at `1` and every number already issued becomes reissuable |

## Create the generator and call it from a script

{% stepper %}
{% step %}

### Open the Sequence generators screen

Go to **Administration** → **Sequence generators**. The screen is titled **Sequence Generators** and lists every existing generator with its **Name** and current **Value**. If the menu item is not there, it is a grant on the `COUNTE_ADM_M` menu resource rather than a missing feature; the item has shipped since 4.2.1.1.
{% endstep %}

{% step %}

### Click Create Sequence Generator

An editable row appears in the grid with **Name** empty and **Value** pre-filled `0`.
{% endstep %}

{% step %}

### Type the key in Name

Maximum 32 characters, and it is the primary key. Choose what your script will pass — `EMPLOYEE_ID`, `BADGE_NUMBER`. A duplicate or an over-long key is rejected with *"Sequence generator could not be created. A generator with this name may already exist, or the key length exceeds the 32-character limit."*
{% endstep %}

{% step %}

### Set Value to one below the first number you want issued

The first call returns the stored value plus one, so a generator created with `100000` issues `100001` first.

**Do not leave the pre-filled `0`.** Confirming a row whose **Value** is `0` fails validation with *"Key and value are required"*, so the lowest number you can reach from this screen is `2` (create with `1`). If you genuinely need `1` to be the first number issued, do not create the generator here at all — call an unknown key from your script and OpenIAM creates it on first use with the value `1`.
{% endstep %}

{% step %}

### Confirm the row with the add icon in Actions

The screen reports *"Sequence generator created successfully"* and the row becomes read-only. There is no edit action — create and delete only — so the value cannot be corrected from the Webconsole afterwards.
{% endstep %}

{% step %}

### Call the generator from your script

In a synchronization transformation script, two inherited methods are the whole interface:

{% code overflow="wrap" %}

```groovy
Long getNextValueInSeries(String key, long offset)
String getNextFormattedValueInSeries(String format, String key)
```

{% endcode %}

The first adds your offset to the incremented value; the second applies a standard Java format string to it. Every helper a transformation script inherits is in [Synchronization script contracts](/administration-guide/reference/synchronization-script-contracts.md#lookup-helpers-on-abstracttransformscript).

In a policy map Groovy row, reach the same generator through the injected `counterRabbitMQService` and its `incrementAndGet(name)` — see [Policy map reference](/administration-guide/lifecycle/policy-map/policy-map-reference.md#injected-fields).
{% endstep %}

{% step %}

### Run the feed against one test record and read the value back

Return to **Administration → Sequence generators** and confirm **Value** advanced by exactly the number of calls the run made. That is the only place the current value is visible.
{% endstep %}
{% endstepper %}

## A worked example

Adapted from shipped synchronization scripts. The first offsets past a legacy high-water mark instead of seeding the generator; the last suffixes an email local part only from the second holder onwards.

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

```groovy
// inside a class extending AbstractUserTransformScript

// A badge number offset past the range the old system issued.
// The generator itself starts at 1, so the first badge is 98000501.
def badgeNumberLong = getNextValueInSeries("BADGE_NUMBER", 98000500L)
addUserAttribute(pUser, "Legic-Number", String.format("%08d", badgeNumberLong))

// A zero-padded employee number from a generator you created with the value 0... which the
// screen rejects, so create it with 1 and expect EMPLN0002 first, or let this call create it.
def employeeId = getNextFormattedValueInSeries("EMPLN%04d", "EMPLOYEE_ID")

// One generator per email local part: only the second and later holders get a suffix.
def sequenceValue = getNextValueInSeries(localPart, 0)
def emailAddress = localPart + (sequenceValue > 1 ? "." + (sequenceValue - 1) : "") + "@example.com"
```

{% endcode %}

Copy that last pattern deliberately: keying on the *value being contested* rather than one shared name means the first holder gets no suffix and contention is per-name instead of global, at the cost of one row per contested value.

## Verification

| What to check                          | How                                                                                                                                                                        |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The generator was created              | Audit action `CREATE_COUNTER` with the description *"Create sequence generator `<key>`"*                                                                                   |
| The generator was deleted              | Audit action `DELETE_COUNTER` with the description *"Delete sequence generator `<key>`"*                                                                                   |
| It is actually being incremented       | **Value** on **Administration → Sequence generators** advanced by one per call after your test run                                                                         |
| The identifiers are unique in practice | Run two feeds against the same key concurrently, then search the generated attribute for duplicates. Gaps are expected and safe; duplicates are not possible from this API |

## When it goes wrong

| Symptom                                                     | Inspect                                                                                                                                    | Fix                                                                                                                                          |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| *"Key and value are required"* on a row that looks complete | The **Value** column                                                                                                                       | A value of `0` fails validation. Enter `1` or higher                                                                                         |
| *"Sequence generator could not be created…"*                | Whether the key already exists, and its length                                                                                             | The key is the primary key and is capped at 32 characters                                                                                    |
| `COUNTER_TIMEOUT_ERROR`, and user creation failing with it  | The cache tier, and how many callers share one key                                                                                         | Every increment takes a cluster-wide lock named `COUNTER_LOCK_<key>` and waits up to one minute for it. Split the key, or fix the cache tier |
| `COUNTER_ERROR`                                             | The service log of whoever asked for the number — the synchronization or reconciliation service during a feed, the ESB during provisioning | Any failure that is not the lock timeout. The lock is always released, so a failed increment does not wedge the key                          |
| Identifiers start one higher than the design expected       | **Value** immediately after creation                                                                                                       | The first call returns stored value **plus one**. Use the `offset` argument to shift the series instead of recreating the generator          |
| Duplicate identifiers after a generator was recreated       | Whether the generator was ever deleted                                                                                                     | Deletion restarts the series at `1`. See [Retire a generator, do not delete it](#retire-a-generator-do-not-delete-it)                        |
| Numbers are missing from the series                         | Nothing — this is expected                                                                                                                 | A transaction that increments and then rolls back does not give the number back. Gaps are the safe failure mode                              |

The lock is an operational constraint, not a formality: concurrent bulk imports against one key queue behind each other, and the cache tier becomes a hard dependency of user creation for as long as a script calls a generator. The log line is *"Can't get Lock for counter `<key>` due to 1 minute timeout."*

## Retire a generator, do not delete it

{% hint style="danger" %}
**Deleting a generator reissues its numbers.** Deletion removes the row, and the next request for that key starts again at `1`. If any identifier derived from it still exists — in OpenIAM or in any managed system — deletion will eventually produce a collision that the series itself cannot detect. The confirmation dialog says so: *"Are you sure you want to delete this sequence generator? This may result in duplicate identities or primary keys. Please review the potential consequences before proceeding."*

Retire a generator by removing the call from the script and leaving the row in place. Moving a value forward past an already-issued number is a break-glass write to `COUNTER.VALUE`, because the Webconsole has no edit path. It bypasses nothing but the audit record, and the next call returns your new value plus one.
{% endhint %}

A generator does not check the managed system. It guarantees the *number* is unique in its series and says nothing about whether `EMPLN0042` already exists as a `sAMAccountName`. Where that matters, do the target lookup too.

## Login collisions use generators too

The uniqueness suffix on a generated login is the same mechanism, applied automatically. A policy map `PRINCIPAL` row generating `<firstName>.<lastName>` increments a generator **keyed on the candidate login itself**, so `John.Smith` is followed by `John.Smith1`, then `John.Smith2`. Expect one `COUNTER` row per contested login rather than one per rule — the table grows with collisions, not with configuration — and note that a login over 32 characters cannot be a generator key. The generation itself is in [Policy map reference](/administration-guide/lifecycle/policy-map/policy-map-reference.md#injected-fields).

## Related

* [Synchronization script contracts](/administration-guide/reference/synchronization-script-contracts.md) — every helper a transformation script inherits, including these two
* [Policy map reference](/administration-guide/lifecycle/policy-map/policy-map-reference.md) — generating a `PRINCIPAL` value, and the injected counter service
* [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md) — editing and versioning the script that calls the generator

*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/platform/sequence-generators.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.
