> 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/convert-a-user-type.md).

# Convert a user's type

Convert a user from one user type to another — optionally merging a second record's access in the same operation — and replace the shipped conversion script that does nothing.

## What this is, and when you need it

Conversion changes a user's **user type** — contractor to employee, employee to service account — and, optionally, merges a second user's access into the target in the same operation. That second part is why conversion is a Groovy extension point rather than a field edit: "make this contractor an employee and give them the access from their new-hire record" is a decision only your organisation can express.

The whole conversion runs in **one transaction**: either every part of it commits or none does. There is no partially converted state to clean up.

## At a glance

|                          |                                                                                                                                                      |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | The user carrying the new type and a conversion date, and both users reprovisioned                                                                   |
| **Where**                | **Manage Users** in SelfService — `/selfservice-v2/viewDirectReports`, menu item **View Direct Reports** — then the **Convert User Type** row action |
| **You need first**       | The destination user type on the **Target Type** list, a comment (it is mandatory), and a conversion script if you want any access merged            |
| **Takes**                | Seconds to submit; provisioning follows                                                                                                              |
| **Reversible**           | Partly — the type change reverses, but access your script merged is not revoked by converting back                                                   |

## Convert the user

{% stepper %}
{% step %}

### Open the user's row and choose Convert User Type

Open **Manage Users**, `/selfservice-v2/viewDirectReports`. Find the person, open the **Row actions** menu on their row, and choose **Convert User Type**. The dialog is titled **Convert User Type —&#x20;*****\<name>*** and describes itself as "Change the metadata type for this user."

The screen lists the people who report to you. If the person you need is not there, you are not their supervisor — see [Scope a delegated administrator](/administration-guide/identities/delegated-administration.md).

On the older SelfService screen, `/selfservice/viewDirectReports`, the same operation is the row action whose tooltip reads **Metadata Type Conversion**, and its dialog is headed **Convert User:&#x20;*****\<name>***.
{% endstep %}

{% step %}

### Decide whether the shipped conversion script is enough

{% hint style="warning" %}
**With the shipped script in place, conversion validates nothing and merges nothing.** It changes the type, stamps the conversion date, and reprovisions both users. Its two method bodies ship commented out.

If your operators expect "convert this contractor to an employee" to carry the new-hire record's roles across, they will get a type change, no access, and a success message. Replace the script first — see [Make the conversion merge access](#make-the-conversion-merge-access).
{% endhint %}

A plain type change needs no script work. A merge does.
{% endstep %}

{% step %}

### Fill in Target Type, Comment and, for a merge, Merge into

| Field                     | Required | What to put in it                                                                                                                                             |
| ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Target Type**           | yes      | The destination user type. The list is the user types on your instance, with the person's **current** type left out, so a no-op conversion cannot be selected |
| **Comment**               | yes      | Free text, recorded on the audit event as `Comment: <text>`                                                                                                   |
| **Merge into (optional)** | no       | The person to merge access *from*. Leave it empty for a plain type change                                                                                     |

**There is no shipped `EMPLOYEE` type.** The **Target Type** list is whatever your deployment created as metadata types in the `USER_OBJECT_TYPE` grouping, at **Administration → Metadata Types**.

**Merge into** searches your own direct reports only, and it excludes the person being converted. You cannot merge from somebody who does not report to you — if the source record is elsewhere in the organization, reassign it to yourself first or have that person's supervisor run the conversion.

The **Comment** is the only narrative anyone will have later, so write something meaningful. The **Convert** button stays disabled until both **Target Type** and **Comment** are non-blank.
{% endstep %}

{% step %}

### Confirm

**Convert** moves the dialog to a second step, **Confirm type conversion**, reading "Convert *\<name>* from *\<current type>* to *\<new type>*? This starts a provisioning job that may take a moment to complete." Press **Convert** again.

The screen then shows **Changes may take a moment to reflect** and updates the person's type in the grid straight away. That is the screen anticipating the result, not reporting it — provisioning is still running, so verify from the audit events below rather than from the grid.
{% endstep %}

{% step %}

### Read the audit event, because the screen's notice does not prove the work happened

OpenIAM enriches the audit event before doing any work: it sets the target user, adds `Comment: <your comment>`, and — only once the destination type has resolved — adds `Changing user metadata type to <type name>`.

That last line is your check. It is present on a conversion the product understood and absent on one it did not, with no error either way, so read the event rather than trusting the screen.
{% endstep %}

{% step %}

### Open the child event to see whether the work happened

| Action                                | Level       | Where to find it                                                                                    |
| ------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
| `USER_TYPE_CONVERSION`                | The request | A parent event, source `SELFSERVICE` — always `SELFSERVICE`, even when an administrator triggers it |
| `PERFORM_USER_TYPE_CONVERSION_ACTION` | The work    | A **child** of that parent. Set **Show children** on the audit search to see it                     |

The child event is where the failure reason lives when a conversion "succeeded" but nothing changed. Both actions ship with retention `-1` — never pruned. See [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md).
{% endstep %}
{% endstepper %}

## Verification

| What to check                 | Where                                                                                                                                    |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| The conversion was submitted  | The **Changes may take a moment to reflect** notice on the screen. It confirms the submission and nothing more                           |
| The destination type resolved | The `Changing user metadata type to <type name>` line on the `USER_TYPE_CONVERSION` event. Its absence means it did not resolve          |
| The conversion actually ran   | The child `PERFORM_USER_TYPE_CONVERSION_ACTION` event, and its failure reason if it failed                                               |
| The type changed              | The user's type on their record                                                                                                          |
| The conversion is dated       | The conversion date on the user — this is what distinguishes a converted user from one created with that type                            |
| The merge happened            | The target user's roles, groups and organizations. This is your script's work, not the product's                                         |
| The form changed              | Open the person on `/webconsole/user-template`. A different user type should render a different template, if you have templates per type |

## When it goes wrong

| Symptom                                                            | Cause                                                                                                                                                                                                   | Fix                                                                                     |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **Convert** stays disabled                                         | **Target Type** or **Comment** is empty. Both are required                                                                                                                                              | Fill both. The button enables itself                                                    |
| The type you want is not on the **Target Type** list               | It is the person's current type, which the list leaves out; or no metadata type exists for it in the `USER_OBJECT_TYPE` grouping                                                                        | Add the type at **Administration → Metadata Types**                                     |
| Submitted, but the type did not change                             | The child `PERFORM_USER_TYPE_CONVERSION_ACTION` event failed                                                                                                                                            | Read its failure reason. `USER_NOT_FOUND` means the target was not returned by the load |
| `CANNOT_INSTANTIATE_GROOVY_CLASS`                                  | `org.openiam.user.conversion.script` points at a missing or uncompilable script, or the class does not extend `AbstractUserConversion`                                                                  | Check the property value and the class declaration                                      |
| `VALIDATION_ERROR`                                                 | Your `validateUserConversion` threw a `BasicDataServiceException`                                                                                                                                       | Expected. The message key you attached is what the operator sees on the dialog          |
| `USER_CONVERSION_ERROR`                                            | Your `convertUserType` threw a `BasicDataServiceException`                                                                                                                                              | Expected. Read your own token                                                           |
| `INTERNAL_ERROR`                                                   | Your script threw something *other* than `BasicDataServiceException`, which tells the user nothing                                                                                                      | Catch what you can and throw `BasicDataServiceException` with a token instead           |
| Type changed but no access merged                                  | The shipped script's merge method body is empty                                                                                                                                                         | Replace the script                                                                      |
| Type changed, access merged, managed system unchanged              | Provisioning did not reach the managed systems                                                                                                                                                          | Check the provisioning requests raised under the child audit event                      |
| The type ID resolved for you but not for the script                | `getMetadataTypeByNameAndGrouping` matches on the exact **name**, not the display name and not the ID; it also returns `null` and logs "MetadataTypeGrouping cannot be NULL." when the grouping is null | Use the exact name                                                                      |
| The conversion ran but merged nothing, and you did choose somebody | The source record arrives at your script as nothing at all when it cannot be loaded, and only a missing **target** fails the operation, with `USER_NOT_FOUND`                                           | Handle the no-source case in your script — the working example below does               |
| Custom fields did not follow the new type                          | The field's **metadata type for template** setting points at the old type                                                                                                                               | Repoint the field, or add it to the destination type's template                         |
| Converting a user changed the person named in **Merge into** too   | Both people are sent to provisioning whenever **Merge into** is set. That is by design                                                                                                                  | Leave **Merge into** empty for a plain type change                                      |

## Roll it back

**A type change** is reversible: convert back. Two things it does not undo.

The conversion date is **overwritten** by the reverse conversion, so the original conversion timestamp is lost. Read it from the audit event instead.

**A merge is not reversible by converting back.** Your script granted the source user's roles, groups and organizations to the target; converting back does not revoke them unless your script explicitly does so. Revoke them as entitlements — see [Grant and revoke access](/administration-guide/identities/grant-and-revoke-access.md).

Provisioning consequences follow the usual rule: accounts created on managed systems by the merged access are not removed by reversing the type.

## Make the conversion merge access

| Property                             | Shipped default                             | Type     | Category |
| ------------------------------------ | ------------------------------------------- | -------- | -------- |
| `org.openiam.user.conversion.script` | `user-conversion/UserTypeConversion.groovy` | `Groovy` | `IDM`    |

Label **User Conversion Script**, shipped in 4.2.2.0 and unchanged since. Read the current value at **Administration → System Configuration**, category `IDM`.

### What runs, and in what order

{% stepper %}
{% step %}

### Both users are loaded with their full access

One load brings back both users with their roles, groups, resources, organizations and logins. A missing target fails the operation with `USER_NOT_FOUND` and the failure reason "Target user not found". A missing *source* does not fail: it arrives at your script as `null`.
{% endstep %}

{% step %}

### Your validation method runs

`validateUserConversion(targetUser, sourceUser, newTypeId)`. A `BasicDataServiceException` thrown here fails the conversion with `VALIDATION_ERROR` and carries your error tokens back to the dialog, so a message key you supply is shown to the operator.
{% endstep %}

{% step %}

### Your conversion method runs

`convertUserType(targetUser, sourceUser, newMdType)`. A `BasicDataServiceException` here fails the operation with `USER_CONVERSION_ERROR`, again carrying your tokens to the dialog.
{% endstep %}

{% step %}

### OpenIAM stamps the type and the date, then provisions both users

OpenIAM sets the new type on the user itself — your script does **not** need to, and should not fight it — and stamps the conversion date with the current time.

Both users, target and source when present, then go to provisioning together under the child audit event, so the connector operations appear as further children of the same tree.
{% endstep %}
{% endstepper %}

### The contract your script must satisfy

Extend `AbstractUserConversion` and override its two methods. The signatures are:

{% code overflow="wrap" %}

```groovy
void validateUserConversion(final User user, final User sourceUser, final String newTypeId)
        throws BasicDataServiceException

void convertUserType(final User targetUser, final User sourceUser, final String newMdType)
        throws BasicDataServiceException
```

{% endcode %}

Keep the parameter **order** — `(target, source, newType)` in both. The `final` modifiers are optional; the shipped file declares them on the first method and not on the second, and both are valid.

`AbstractUserConversion` gives your script eight fields, already wired — you declare nothing and inject nothing:

| Field                         | Type                                                       |
| ----------------------------- | ---------------------------------------------------------- |
| `metadataTypeRabbitMQService` | `MetadataTypeRabbitMQService`                              |
| `userRabbitMQService`         | `UserRabbitMQService`                                      |
| `roleRabbitMQService`         | `RoleRabbitMQService`                                      |
| `groupRabbitMQService`        | `GroupRabbitMQService`                                     |
| `organizationRabbitMQService` | `OrganizationRabbitMQService`                              |
| `resourceRabbitMQService`     | `ResourceRabbitMQService`                                  |
| `context`                     | `ApplicationContext`                                       |
| `requesterId`                 | `String` — the ID of the user who submitted the conversion |

It also provides one helper:

{% code overflow="wrap" %}

```groovy
protected MetadataType getMetadataTypeByNameAndGrouping(final String name, final MetadataTypeGrouping grouping)
```

{% endcode %}

It searches by exact name within the grouping and caches the result per grouping, so calling it in a loop is cheap. It returns `null` — and logs an error — when the grouping is null.

### A working conversion script

Refuses a no-op, refuses a conversion to employee with no source record, and merges the source's roles, groups and organizations into the target. The no-op guard is belt and braces — the **Target Type** list already leaves out the person's current type — but it is worth keeping for conversions raised from the older screen.

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

```groovy
import org.apache.commons.collections4.CollectionUtils
import org.openiam.base.ws.ResponseCode
import org.openiam.dto.MetadataType
import org.openiam.dto.User
import org.openiam.esb.core.user.converter.AbstractUserConversion
import org.openiam.exception.BasicDataServiceException
import org.openiam.exception.EsbErrorToken
import org.openiam.idm.srvc.meta.domain.MetadataTypeGrouping

class UserTypeConversionPerformer extends AbstractUserConversion {

    @Override
    void validateUserConversion(final User user, final User sourceUser, final String newTypeId)
            throws BasicDataServiceException {

        // Refuse a no-op.
        if (user != null && newTypeId != null && newTypeId.equalsIgnoreCase(user.getMdTypeId())) {
            BasicDataServiceException be = new BasicDataServiceException(ResponseCode.VALIDATION_ERROR)
            be.addErrorToken(new EsbErrorToken("openiam.ui.common.field.validation.error"))
            throw be
        }

        // Converting to an employee requires a source record to merge access from.
        MetadataType employee = getMetadataTypeByNameAndGrouping("Employee",
                MetadataTypeGrouping.USER_OBJECT_TYPE)
        if (employee != null && employee.getId().equalsIgnoreCase(newTypeId) && sourceUser == null) {
            BasicDataServiceException be = new BasicDataServiceException(ResponseCode.VALIDATION_ERROR)
            be.addErrorToken(new EsbErrorToken("openiam.ui.selfservice.direct.reports.covert.user.error"))
            throw be
        }
    }

    @Override
    void convertUserType(User targetUser, User sourceUser, String newMdType)
            throws BasicDataServiceException {

        if (sourceUser == null) {
            // Nothing to merge; the new type and the conversion date are still applied.
            return
        }

        if (CollectionUtils.isNotEmpty(sourceUser.getRoles())) {
            sourceUser.getRoles().each { xref -> targetUser.addRole(xref.getEntityId()) }
        }
        if (CollectionUtils.isNotEmpty(sourceUser.getGroups())) {
            sourceUser.getGroups().each { xref -> targetUser.addGroup(xref.getEntityId()) }
        }
        // Organization memberships are held on User as `affiliations`, not `organizations`.
        if (CollectionUtils.isNotEmpty(sourceUser.getAffiliations())) {
            sourceUser.getAffiliations().each { xref -> targetUser.addOrganization(xref.getEntityId()) }
        }
        // Do not set the type here — OpenIAM applies newMdType after this method returns.
    }
}
```

{% endcode %}

The `addRole(xref.getEntityId())` pattern over `sourceUser.getRoles()` is taken verbatim from the shipped script's own commented example, as is the `openiam.ui.selfservice.direct.reports.covert.user.error` message key — the misspelling of "convert" is in the shipped file, so use the key exactly as it is or it will not resolve.

Store the script through the Groovy manager, `/webconsole/groovyManager`, and set `org.openiam.user.conversion.script` to its ID. See [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md).

## Where user types come from

A user type is a metadata type in the `USER_OBJECT_TYPE` grouping, stored on the user record. Manage them at **Administration → Metadata Types** (`/webconsole/metaDataTypeSearch`, `/webconsole/metaDataTypeEdit`); the grouping is editable from those screens.

Three things consume user types:

| Consumer                    | How                                                                                                                                |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| The classic user form       | The **Type** drop-down is the list of types in the grouping                                                                        |
| The template creation route | `/webconsole/new-user-template` shows a type picker when no type is supplied, and carries your choice in the address bar           |
| The custom field editor     | A field's **metadata type for template** setting is chosen from three groupings — `GROUP_TYPE`, `ROLE_TYPE` and `USER_OBJECT_TYPE` |

Because the third exists, a custom field can be scoped to a user type: it appears on templates for that type and not on others. That, plus the type picker, is the whole of "different forms for different kinds of people".

{% hint style="info" %}
Two groupings are easy to confuse. `USER_OBJECT_TYPE` holds **user types** — what kind of person this is. `USER` holds **secondary status** values such as `DECEASED`, `LEAVE_WITH_PAY`, `LEAVE_OF_ABSENCE`, `PENDING_DEACTIVATION` and `PENDING_DELETE`. A third, `USER_SUB_TYPES`, exists for sub-typing. Conversion operates on `USER_OBJECT_TYPE` only. See [Metadata type groupings](/administration-guide/reference/enums.md#metadata-type-groupings).
{% endhint %}

## Related

* [Build a page template](/administration-guide/identities/page-templates-and-custom-fields.md) — how a user type selects a form
* [Create a user](/administration-guide/identities/create-a-user.md) — the type picker on the template creation route
* [Grant and revoke access](/administration-guide/identities/grant-and-revoke-access.md) — revoking access a merge granted

*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/convert-a-user-type.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.
