> 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/identities/page-templates-and-custom-fields/page-template-reference.md).

# Page template reference

The page-template and custom-field objects, the four template types, the eleven field types, the eight conditional-rule actions, the data-model settings and the three Groovy contracts.

## What this is, and when you need it

Everything a page template and a custom field can be set to, in the order you meet it: the objects and the tables behind them, the two type axes a template carries, the eleven widget types a field can be, the eight actions a conditional rule can take, where a field's values come from, and the three Groovy extension points with a complete working script for each.

Building a template and binding it to a screen is [Build a page template](/administration-guide/identities/page-templates-and-custom-fields.md). Use this page when you are choosing a field type, writing a rule, or working out why a field renders differently from how you configured it.

## The objects and where they live

| Object                    | Table                                                           | Console route                                                                                      |
| ------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Page template             | `METADATA_ELEMENT_PAGE_TEMPLATE`                                | `/webconsole/pageTemplates`, `/webconsole/newPageTemplate`, `/webconsole/editPageTemplate?id=<ID>` |
| Custom field              | `METADATA_ELEMENT`                                              | `/webconsole/customFields`, `/webconsole/newCustomField`, `/webconsole/editCustomField?id=<ID>`    |
| Template ↔ custom field   | `METADATA_ELEMENT_PAGE_TEMPLATE_XREF`                           | On the template's **Custom Fields** section                                                        |
| Template ↔ built-in field | `METADATA_FIELD_TEMPLATE_XREF` (rows from `UI_TEMPLATE_FIELDS`) | On the template's **Template Fields** section                                                      |
| Template ↔ URI pattern    | `METADATA_URI_XREF`                                             | On the template's **URI Patterns** section                                                         |
| Template type             | `UI_TEMPLATE_TYPE`                                              | Read-only; four rows ship                                                                          |

One relationship is easier to read from the custom field than from the template: the field editor's **Linked Page Templates** section lists every template that uses the field. Read it before you delete a field — it is the only place the blast radius is shown in one view.

Saving a page template audits with description "Save page template"; deleting, "Delete page template". Saving a custom field audits with "Save custom field"; deleting, "Remove custom field".

Creating a template also creates a resource: OpenIAM builds a public resource named `<templateName>_<epochMillis>` whose resource type comes from `org.openiam.resource.type.ui.template`, shipped `UI_TEMPLATE`. That is why templates appear in the resource model and can be entitled — search resources for the template's name after you save one and you will find it.

## The four template types

Four rows ship in `UI_TEMPLATE_TYPE`, and the list is read-only:

| `TEMPLATE_TYPE_ID`        | `NAME`                  | `DESCRIPTION`                                | Since   |
| ------------------------- | ----------------------- | -------------------------------------------- | ------- |
| `USER_TEMPLATE`           | User Page Tempate       | Template for SelfService User-centric pages  | 3.0.0.6 |
| `GROUP_TEMPLATE`          | Group Page Template     | Template for SelfService Group-centric pages | 3.4.0.0 |
| `ROLE_TEMPLATE`           | Role Page Template      | Template for edit Role page                  | 4.1.0.0 |
| `MANAGED_SYSTEM_TEMPLATE` | Managed System Template | Template for managed system                  | 4.0.0.0 |

The `USER_TEMPLATE` name contains a typo in the shipped data — "Tempate". Expect to see it on screen.

A **second**, separate axis arrived in 4.2.2.0: three `PAGE_TEMPLATE_TYPE` metadata types, which sit in the metadata grouping that previously held connector types — so the grouping name on the screen may not read the way you expect, and the entry you want is named for the object the template describes:

| `TYPE_ID`                  | `NAME`                   | `CREATEABLE` |
| -------------------------- | ------------------------ | ------------ |
| `USER_PAGE_TEMPLATE_TYPE`  | User page template type  | `Y`          |
| `GROUP_PAGE_TEMPLATE_TYPE` | Group page template type | `Y`          |
| `ROLE_PAGE_TEMPLATE_TYPE`  | Role page template type  | `Y`          |

The same release repointed the shipped `DEFAULT_GRP_TEMPLATE` at `GROUP_PAGE_TEMPLATE_TYPE` and `DEFAULT_RL_TEMPLATE` at `ROLE_PAGE_TEMPLATE_TYPE`. So a template carries **both** a `TEMPLATE_TYPE_ID` (which field catalogue it draws from) and a `METADATA_TYPE_ID` (which page template type it is). Saving a template without a template type fails with `TEMPLATE_TYPE_REQUIRED`. You can see both axes on any shipped template: open one and read the **Template Type** and the metadata grouping and type name it carries.

A custom field's **metadata type for template** setting draws on a different list again — the `GROUP_TYPE`, `ROLE_TYPE` and `USER_OBJECT_TYPE` groupings, combined.

## Field types

The field type of an element is a metadata type in the `UI_WIDGET` grouping. Eleven ship:

| `TYPE_ID`        | Shipped description / name                                  | Gets a default value | Valid values initialized from user values | Since    |
| ---------------- | ----------------------------------------------------------- | -------------------- | ----------------------------------------- | -------- |
| `TEXT`           | Text field                                                  | yes                  | —                                         | 3.0.0.0  |
| `TEXTAREA`       | Multiline text field                                        | yes                  | —                                         | 3.0.0.0  |
| `PASSWORD`       | Password field                                              | yes                  | —                                         | 3.0.0.0  |
| `DATE`           | Date field                                                  | yes                  | —                                         | 3.0.0.0  |
| `SELECT`         | Combo box                                                   | yes                  | yes                                       | 3.0.0.0  |
| `MULTI_SELECT`   | MultiSelect list                                            | yes                  | yes                                       | 3.0.0.0  |
| `RADIO`          | Radio button                                                | yes                  | —                                         | 3.0.0.0  |
| `CHECKBOX`       | Checkbox                                                    | yes                  | —                                         | 3.0.0.0  |
| `MULTI_CHECKBOX` | MultiCheckbox (English display name "MultiSelect Checkbox") | **no**               | —                                         | 4.1.11.0 |
| `LABEL`          | Label                                                       | no                   | —                                         | 4.0.0.0  |
| `SECTION`        | Section                                                     | no                   | —                                         | 4.2.0.0  |

Two behaviours are worth knowing because they look like data loss.

**User values are filtered against valid values.** For `SELECT`, `MULTI_SELECT`, `RADIO`, `CHECKBOX` and `MULTI_CHECKBOX`, when the field draws its values from anywhere other than the template's own data model and both lists are non-empty, OpenIAM drops any stored user value that is not in the field's valid values before the form renders. So if you narrow a select's valid values, existing users' out-of-range values silently disappear from the form, and the next save persists their absence. Migrate the data before you narrow the list — and prove it on one user first: open that user before and after the change and compare the field.

**`MULTI_CHECKBOX` and `LABEL` do not receive a default value.** A `defaultValue` configured on a multi-checkbox is never applied, which is visible immediately on the rendered form. `SELECT` and `MULTI_SELECT` do the opposite favour: when their valid-value list is empty, OpenIAM back-fills it from the values users already hold, so a select can show valid values you never configured.

## Conditional rules

A field's rules are stored as JSON in its `rules` column: an object with a single `rules` array, each entry carrying an action `type`, an `errorMessage`, and a list of `conditions`. A condition is three strings — `operandA`, the field id of *another* field on the template; `operator`, one of `=`, `!=`, `<` and `>`; and `operandB`, a literal value to compare against.

The default is `{"rules":[]}` — an object with an empty array, **not** `null` and not `[]`. A field whose `rules` column holds a bare array rather than `{"rules":[...]}` will fail to render its rule editor, which is the observable symptom of malformed rule JSON.

### The eight action types

| `type`        | Label key `openiam.ui.page.template.field.actions.*` | English label                                    |
| ------------- | ---------------------------------------------------- | ------------------------------------------------ |
| `validation`  | `.validation`                                        | Show error message                               |
| `hide`        | `.hide`                                              | Hide                                             |
| `show`        | `.show`                                              | Show                                             |
| `disable`     | `.disable`                                           | Deactivate                                       |
| `enable`      | `.enable`                                            | Activate                                         |
| `required`    | `.required`                                          | Set required                                     |
| `notrequired` | `.notrequired`                                       | Set not required                                 |
| `update`      | `.update`                                            | Update — triggered on each user update operation |

Note the labels: `disable` reads "Deactivate" and `enable` reads "Activate", which in an IAM product means something else entirely. They control the *field's* enabled state, not the user's.

`errorMessage` is only collected by the editor for a `validation` rule — the input is not rendered for any other action type — so setting it elsewhere has no effect on screen.

{% hint style="info" %}
The `operandA` drop-down offers the template's other fields, excluding the field being edited, and the rule editor does not render at all when that list is empty. A rule editor that will not appear means the template has only one field. Add the field you want to test against first.
{% endhint %}

### Worked rule: require a cost centre only for contractors

On the **Cost Centre** field of a user template that also has an **Employee Type** field:

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

```json
{
  "rules": [
    {
      "type": "required",
      "errorMessage": "",
      "conditions": [
        { "operandA": "USER_EMPLOYEE_TYPE", "operator": "=", "operandB": "CONTRACTOR" }
      ]
    },
    {
      "type": "hide",
      "errorMessage": "",
      "conditions": [
        { "operandA": "USER_EMPLOYEE_TYPE", "operator": "!=", "operandB": "CONTRACTOR" }
      ]
    }
  ]
}
```

{% endcode %}

`operandA` is the **field id** of the other field, as offered by the drop-down — for built-in user fields that is the `UI_FIELD_ID` from `UI_TEMPLATE_FIELDS`, such as `USER_FIRST_NAME`, `USER_LAST_NAME`, `USER_MIDDLE_INIT`, `USER_MAIDEN_NAME`, `USER_NICKNAME`. Read the drop-down rather than guessing: it is populated from the template's own selected fields.

## Custom fields and their data model

A custom field is created at `/webconsole/newCustomField`. It carries its field type (one of the eleven widget types above), **Required**, **Editable**, **Is Public**, a static default value, display names and default values per language, valid values, a placeholder, its rules, its metadata type for template, and the two settings that decide where its values come from.

The value source is a two-level choice. First **Data Model Source Type**:

| Value          | Label key                                     | English label      |
| -------------- | --------------------------------------------- | ------------------ |
| `staticModel`  | `openiam.ui.custom.fields.model.type.static`  | Static             |
| `dynamicModel` | `openiam.ui.custom.fields.model.type.dynamic` | Dynamic Data Model |

Then, only when `dynamicModel` is chosen, **Data Model Type**:

| Value     | Label key                                      | English label         | Means                                                    |
| --------- | ---------------------------------------------- | --------------------- | -------------------------------------------------------- |
| `inherit` | `openiam.ui.custom.fields.model.type.template` | Inherit from Template | Values come from the template's global data model script |
| `groovy`  | `openiam.ui.custom.fields.model.type.groovy`   | Data Model Script     | Values come from this field's own script                 |

Three behaviours that matter when you change a field's mind:

* The data model type is stored **only** while the source type is `dynamicModel`. Switching a field back to Static clears it.
* The data model script id is cleared whenever the data model type moves away from `groovy`.
* A field loaded with neither a script id nor a data model type opens as Static.

Reopen the field after a switch and confirm both are as you expect — that is the only reliable check, because the two are cleared on different conditions.

Remember the valid-value filtering above: it is skipped when the data model type is `inherit`, because in that case the valid values are not the field's own.

{% hint style="info" %}
**A custom field can also be filled by the transformation script that already runs.** If the value should come from a managed system rather than from a person typing it, edit the Groovy transformation script the policy map already uses and name your custom attribute in its `addAttribute` call. The field is then populated by the same synchronization or provisioning path as the built-in attributes, with no data model script and no change to the page template. Put the field on a template only if somebody needs to see or edit it.
{% endhint %}

### Why a custom field is missing from the field drop-down

You create a custom field, go back to the template, and it is not offered under **Select Field Type**. Three causes, in the order worth checking.

**The template has no template type yet.** The field list is fetched only once the template type is known, so on a template saved without a type the drop-down is empty of everything, not only of your field.

**The field is already on this template.** The options are filtered against the fields the template already holds, so a field you added earlier disappears from the drop-down rather than appearing twice. Read the **Template Fields** grid to the bottom before concluding the field is missing.

**The field's field type has not reached the Webconsole's metadata cache.** The drop-down offers the built-in fields for the template type plus every custom field whose field type is one of the eleven widget types. That list is read from a cached copy of the metadata, refreshed on `org.openiam.metadata.cache.sweeptime`, shipped `120000` — two minutes. Wait one sweep interval and reload the screen; if the field appears then, the cache was the cause.

{% hint style="info" %}
Neither **Is Public** nor **Is Editable** decides whether a field is *offered* here — the search behind the drop-down does not filter on either. What they decide is whether the field *renders* for a given requester: OpenIAM keeps an element on the rendered form only when the field is public, or the request is an administrative one, or the requester is entitled to the field's own resource. A field that is neither public nor entitled is therefore added to the template successfully and then silently absent from the screen — which looks identical to the person filling in the form. Field guidance that circulates in support tickets — "tick Is Public and set Is Editable" — treats both symptoms at once and is worth trying, but the two causes are separate and only the render one is about those flags.
{% endhint %}

### Dependent drop-downs

A recurring request: when the operator picks an organization, the department drop-down should offer only that organization's departments. There is no product setting for a dependent drop-down. What the product gives you is two hooks on the template, and between them they get you a workable — if statically defined — cascade.

The first hook is the template's global data model. Whatever your global data model script returns is serialised onto the page by the legacy client as a JavaScript variable named `TemplateDataModel`, and your own script runs after the fields are drawn.

The second hook is the template's **Custom JS** field — the `CUSTOM_JS` column on the page template, present since 3.4.0.0, edited on the template screen and returned with the template.

To address a field from that script, use the wrapper's `uifieldname` attribute: the legacy widget renderer sets it to the element's ID, so the value you need is the custom field's own ID, readable from `/webconsole/editCustomField?id=<ID>` in the address bar. You do not have to dig it out of the browser inspector.

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

```javascript
// Template Custom JS. Reads its option lists from the template's global data model,
// so the mapping lives in a Groovy script rather than in this page.
(function () {
    var orgSelect  = document.querySelector("[uifieldname='<ORGANIZATION_FIELD_ID>'] select");
    var deptSelect = document.querySelector("[uifieldname='<DEPARTMENT_FIELD_ID>'] select");
    if (!orgSelect || !deptSelect || typeof TemplateDataModel === "undefined") {
        return;
    }
    function refreshDepartments() {
        var allowed = TemplateDataModel[orgSelect.value] || [];
        deptSelect.innerHTML = "";
        allowed.forEach(function (d) {
            var option = document.createElement("option");
            option.value = d.value;
            option.text = d.displayName;
            deptSelect.appendChild(option);
        });
    }
    orgSelect.addEventListener("change", refreshDepartments);
    refreshDepartments();
})();
```

{% endcode %}

Three limits to state plainly before anybody builds on this.

* **It is presentation only.** Nothing on the server knows the cascade exists, so a request that names a department outside the selected organization is not rejected. If the pairing has to hold, enforce it in a custom validator as well — see [The Groovy contracts](#the-groovy-contracts).
* **`uifieldname` is emitted by the legacy renderer only.** The rewritten React screens render each field with an `id` instead and no `uifieldname` attribute, so a selector written against `uifieldname` finds nothing there. Inspect the rendered markup of the screen you are targeting before writing the selector — that check takes a minute and saves an afternoon.
* **The option lists belong in the data model, not in the script.** Keeping them in the global data model script keeps them in one Groovy script that can query the organization tree, instead of in a JavaScript literal that has to be edited in the Webconsole every time a department is added.

### Multi-value fields, managed systems and user search

Two things about custom fields that only show up after the field is in use.

**A multi-value field usually reaches a managed system as a single value.** The shipped policy-map Groovy idiom for a custom attribute reads exactly one value and adds exactly one:

{% code overflow="wrap" %}

```groovy
String customAttr = diffObject.getAttributeDiffs().find({
    it.getValue().getName().equalsIgnoreCase(ATTR_NAME)
})?.getValue()?.getFirstValue()
if (customAttr) {
    attribute.addValue(new StringOperationalConnectorValue(customAttr, diffObject.getStatus()));
}
```

{% endcode %}

That is the shipped `customAttr1_1.0.groovy`, and `getFirstValue()` returns the first value and nothing else. So changing a field from **Combo box** (`SELECT`) to **MultiSelect list** (`MULTI_SELECT`) does not change what the managed system receives — the first selected value still arrives, the rest are dropped, and nothing anywhere reports an error. To send all of them, edit the policy map's transformation script to loop the values and add each one. The field type is not the thing to change. Confirm either way on the account itself in the managed system, not on the OpenIAM form.

**A custom field is searchable as an extended attribute as soon as it exists.** The **Select Extended Attributes** list on user search is built from the same "every custom field with a widget field type" search, keyed by the field's **Attribute Name**. The field does **not** have to be on a page template to be searchable, and the value the search matches is the Attribute Name, not the display name.

## The Groovy contracts

Three extension points, each with a shipped example script you can copy.

### Field data model: `fieldDataModel_1.0.groovy`

Your script extends `AbstractFieldDataModel` and implements two methods, and it can reach the Spring context through the inherited `context` field:

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

```groovy
import org.openiam.esb.core.template.model.AbstractFieldDataModel
import org.openiam.idm.srvc.meta.dto.PageElementValidValue;

class CustomFieldDataModel extends AbstractFieldDataModel{

    @Override
    public String getDefaultValue(String requesterId, Map<String, Object> bindingMap) {
        return null;
    }

    @Override
    public List<PageElementValidValue> getDataModel(String requesterId, Map<String, Object> bindingMap) {
        return null;
    }
}
```

{% endcode %}

That is the shipped file verbatim. Both methods returning `null` is the shipped behaviour: attach this script unmodified and the field gets no default and no valid values, which is exactly what you will see on the rendered form.

### Template global data model: `templateDataModel_1.0.groovy`

Your script extends `AbstractTemplateGlobalDataModel` and implements one method, whose return type is a raw `Map`:

{% code overflow="wrap" %}

```groovy
import org.openiam.esb.core.template.model.AbstractTemplateGlobalDataModel

class CustomTemplateDataModel extends AbstractTemplateGlobalDataModel{

    @Override
    Map getDataModel(String requesterId) {
        return null;
    }
}
```

{% endcode %}

Attach this on the template as its data model script, and every field whose data model type is `inherit` reads from the map it returns.

### Custom validator

Set your script on the template as its **custom validation script**. It implements `IPageTemplateCustomValidator` for the object being saved, with one method — `void isValid(<object>) throws BasicDataServiceException`.

The contract the product requires of you: throw a `BasicDataServiceException` carrying a response code *and* a value that is one of the UI's own error message keys. Anything else and the operator sees a rejection with no message to read. With no script attached nothing is validated beyond the field-level rules — the shipped default validator does nothing.

**The failure response code is `CANNOT_INSTANTIATE_GROOVY_CLASS`.** You will meet it in two different situations:

* On **save of the template**, OpenIAM instantiates the script there and then, and wraps any failure as `CANNOT_INSTANTIATE_GROOVY_CLASS`. A script that does not compile therefore blocks the template from saving — which is useful, because the save itself is your compile check.
* On **validation**, an exception your validator raises deliberately is passed through unchanged, while any *other* exception is reported as `CANNOT_INSTANTIATE_GROOVY_CLASS` as well. So a null-pointer bug in your validator surfaces to the operator as an instantiation failure, not as a validation failure. Guard your nulls, or you will debug the wrong thing.

#### A complete working validator

Rejects a user whose employee ID is missing when the user type is an internal employee. The error token uses a real UI message key, `openiam.ui.common.field.validation.error`.

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

```groovy
import org.apache.commons.lang3.StringUtils
import org.openiam.base.ws.ResponseCode
import org.openiam.dto.User
import org.openiam.esb.core.meta.service.IPageTemplateCustomValidator
import org.openiam.exception.BasicDataServiceException
import org.openiam.exception.EsbErrorToken

class InternalEmployeeIdValidator implements IPageTemplateCustomValidator<User> {

    @Override
    void isValid(User user) throws BasicDataServiceException {
        // Guard every dereference: any other kind of exception from here is
        // reported to the operator as CANNOT_INSTANTIATE_GROOVY_CLASS, not as a
        // validation failure.
        if (user == null) {
            return
        }
        if ("INTERNAL_USER".equalsIgnoreCase(user.getMdTypeId())
                && StringUtils.isBlank(user.getEmployeeId())) {
            BasicDataServiceException be =
                    new BasicDataServiceException(ResponseCode.VALIDATION_ERROR)
            be.addErrorToken(new EsbErrorToken("openiam.ui.common.field.validation.error"))
            throw be
        }
    }
}
```

{% endcode %}

Store it through Groovy Manager, then set the template's **custom validation script** to its ID. The error-token message key pattern is the one the shipped `UserTypeConversion_1.0.groovy` uses in its own commented example, so it is the product's own idiom rather than an invention.

{% hint style="warning" %}
The template save instantiates your script with an empty binding map and then requires the result to be a page template validator. A script whose class does not implement the interface compiles fine and then fails that check, producing `CANNOT_INSTANTIATE_GROOVY_CLASS` at save time with no further detail. If the template refuses to save after you attach a validator, check the class declaration first.
{% endhint %}

#### Where the validator runs, and where it does not

Template validation does nothing at all unless the save request carries a page template. The required checks, the valid-value checks and your custom validator all sit behind that one test, so a save that sends no page template is not validated by the template and there is no error to read.

The saves that do carry one: create **and** edit a user, from the Webconsole and from SelfService; a person editing their own SelfService profile; self-registration; and create or edit of a group or a role. Create and edit of a user are one path, and validation happens above every branch that distinguishes them — there is no create-only route around it.

So when a validator fires on create and not on an edit, the thing to establish is whether that screen's save actually carried the page template, not whether the product validates edits. Capture the request body of the failing save in the browser's network tab and look for a `pageTemplate` object in it.

{% hint style="danger" %}
**Do not move field validation into `CustomDefaultEditUserApproverAssociationIdentifier_1.0.groovy`.** It is a real shipped script — named by the property `org.openiam.activiti.edit.user.approver.association.groovy.script` and seeded in 4.2.2.0 — but its one method decides *who approves* an edit-user request. It is not a validation extension point, it does not run on a direct save, and validation placed there is skipped entirely for any edit that does not go through the approval workflow. Validation belongs in the custom validator.
{% endhint %}

#### Enforcing required on a built-in template field

**The server-side required check applies to custom fields only.** OpenIAM walks the page elements the save request sent and acts only on those that are the template's **custom** fields — the `METADATA_ELEMENT_PAGE_TEMPLATE_XREF` rows. Built-in template fields come from a different table (`METADATA_FIELD_TEMPLATE_XREF`, rows of `UI_TEMPLATE_FIELDS`) and are never in that set, so a `REQUIRED` rejection is never raised for one.

The practical consequence: ticking **Required** on a built-in field is a hint to the form, not a constraint on the save. The composite organizational-structure field is the one that catches people — `USER_SHOW_ORGANIZATIONS`, display name *User can define his organizational structure*. Mark it required and a user can still be created and saved with no department chosen, no popup and no error. Business rules keyed on the department then do not fire, and the account is created on the managed system incomplete. Prove it on your own build in one minute: mark it required, save a user with the department empty, and read the user back.

Enforce it in the custom validator instead. Which check you write depends on how the department is captured:

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

```groovy
import org.apache.commons.collections4.CollectionUtils
import org.apache.commons.lang3.StringUtils
import org.openiam.base.ws.ResponseCode
import org.openiam.dto.User
import org.openiam.dto.UserToOrganizationMembershipXref
import org.openiam.esb.core.meta.service.IPageTemplateCustomValidator
import org.openiam.exception.BasicDataServiceException
import org.openiam.exception.EsbErrorToken

class DepartmentRequiredValidator implements IPageTemplateCustomValidator<User> {

    // The organization type that represents a department in this deployment.
    private static final String DEPARTMENT_ORG_TYPE = "<DEPARTMENT_ORG_TYPE_ID>"

    @Override
    void isValid(User user) throws BasicDataServiceException {
        if (user == null) {
            return
        }

        // Case 1 - the department is an organization affiliation, captured by the
        // built-in USER_SHOW_ORGANIZATIONS field.
        boolean hasDepartment = CollectionUtils.isNotEmpty(user.getAffiliations()) &&
                user.getAffiliations().any { UserToOrganizationMembershipXref xref ->
                    DEPARTMENT_ORG_TYPE.equals(xref.getOrganizationTypeId())
                }

        // Case 2 - the department is a custom field, so it is a user attribute.
        // Use the field's Attribute Name, not its display name.
        if (!hasDepartment) {
            hasDepartment = StringUtils.isNotBlank(user.getAttribute("DEPARTMENT")?.getFirstValue())
        }

        if (!hasDepartment) {
            BasicDataServiceException be =
                    new BasicDataServiceException(ResponseCode.VALIDATION_ERROR)
            be.addErrorToken(new EsbErrorToken("openiam.ui.common.field.validation.error"))
            throw be
        }
    }
}
```

{% endcode %}

A user's affiliations are organization memberships, and each one carries the organization's type id and name, which is what the first check reads; `getAttribute(name)` reads the user's attribute map and `getFirstValue()` its first value, which is what the second check reads. Read `DEPARTMENT_ORG_TYPE_ID` off your own organization types at **Access Control → Organization**; do not guess it.

{% hint style="warning" %}
Attach the validator to **every** template bound to a screen that creates or edits this population, including self-registration if that screen captures the field. A validator on the administrator's template does not constrain a save made from a SelfService template — the validator is a property of the template, and each screen resolves its own.
{% endhint %}

## Related

* [Build a page template](/administration-guide/identities/page-templates-and-custom-fields.md) — the procedure, the faculty worked example and the binding trap
* [Add a custom user page to SelfService](/administration-guide/identities/add-a-selfservice-user-page.md) — a purpose-built page on its own URI pattern
* [Configure self-registration](/administration-guide/identities/self-registration.md) — templates on public patterns, and the shipped double binding
* [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md) — storing the data model and validator scripts
* [Content providers and URI patterns](/administration-guide/access/content-providers-and-uri-patterns.md) — where the patterns a template binds to come from
* [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md) — the `ui-template/` family

*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/identities/page-templates-and-custom-fields/page-template-reference.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.
