> 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/requests/approval-flows/raise-a-servicenow-incident.md).

# Raise a ServiceNow incident from an approval

Attach a customer-owned Groovy script to a request's target resource so an approved request raises exactly one ServiceNow incident, and make its failures visible.

{% hint style="warning" %}
From 2026.10.2 OpenIAM raises, updates and closes ServiceNow and Jira tickets itself, through the Ticket Connector and ticket rules: see [Ticket integration with Jira and ServiceNow](/administration-guide/ticketing.md), and [ServiceNow ticketing system](https://github.com/openiam-platform/gitbook-docs/tree/main/connectorconfig/servicenow-ticketing.md) for the connector itself. The script hook on this page still runs but is deprecated; each run is audited with the info line `Deprecated: use the Ticket Connector`. Use this page only to maintain an existing script, and plan the move described in [Operate ticket integration](/administration-guide/ticketing/operate-ticketing.md#operate-migrate-from-the-groovy-hook).
{% endhint %}

## What this is, and when you need it

This is one of the things an approval completing can trigger, and it is configured as part of the approval flow rather than on its own. Where a service desk owns the work instead of a connector, an approved OpenIAM request can create a ServiceNow incident: you attach a Groovy script to the request's target resource, and OpenIAM invokes it when the request completes.

Two example scripts ship and neither is ready to use — one has no idempotency check at all, and both swallow their own errors, so a ServiceNow outage looks like success. This page builds a customer version and proves the failure path before you rely on it. Set the approval up first ([Approval flows](/administration-guide/requests/approval-flows.md)); the extension point this hangs on, and the audit contract it must satisfy, are on [Integration patterns for external systems](/administration-guide/operations/enterprise-integrations.md#the-ticket-system-extension-point).

## At a glance

|                          |                                                                                                                                                                                                                                                                                    |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | One customer-owned script on one request's target resource, raising exactly one incident per approved request, with a per-request marker on the user and a `TICKET_SYSTEM_INTEGRATION` audit event that carries a failure reason when ServiceNow does not answer                   |
| **Where**                | **Access Control → Resource** (`/webconsole/resources`), the **Groovy Script** field at `/webconsole/editResource`; the credential on **Provisioning → Managed System** (`/webconsole/mngsystem`); the script in **Administration → Groovy Manager** (`/webconsole/groovyManager`) |
| **You need first**       | A ServiceNow account permitted on the `incident` table, the resource id of the request type you are integrating, and one disposable request you can approve end to end                                                                                                             |
| **Takes**                | Two to four hours for a first version, most of it in the failure testing                                                                                                                                                                                                           |
| **Reversible**           | Partly — clearing **Groovy Script** on the resource stops further incidents at the next request, but incidents already created in ServiceNow are not recalled and must be closed there                                                                                             |

## Build and attach the incident script

{% stepper %}
{% step %}

### Create the ServiceNow managed system that holds the credential

**Provisioning → Managed System → New**, at `/webconsole/mngsystem`. Give it the id `SERVICE_NOW_MANAGED_SYS` — both shipped scripts hard-code that id, so keeping it lets you run them unmodified. Set **Host URL** to the instance base URL, `https://<TENANT_ID>.service-now.com`, **Login Id** to the integration account and **Password** to its password. The script reads all three from this record and decrypts the password through `getDecryptedMngSysPassword`, so nothing about the credential lives in the script.

Permit that account on the `incident` table and on nothing else. The script makes a single JSON call to ServiceNow's `<Host URL>/api/now/table/incident`, authenticating as that account with the credentials it just read off this record — so a `401` is nearly always one of these three fields rather than the script.
{% endstep %}

{% step %}

### Branch the shipped script instead of editing it

In Groovy Manager, open `workflow/resource/ServiceNowTicketCreation_1.0.groovy` and use **Branch off of this version** to make a customer-owned copy. Editing the shipped script in place means the next upgrade either overwrites your work or conflicts with it. See [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md#version-with-branch) for the branch mechanics and [Promote configuration between environments](/administration-guide/platform/import-export-and-promotion.md#what-does-export-groovy-scripts) for getting the result to production.

Keep the class declaration and the `perform` signature exactly as they are. Keep the two constants:

{% code overflow="wrap" %}

```groovy
private static String serviceNowMngSysId = "SERVICE_NOW_MANAGED_SYS";
private static String createIncidentEndpoint = "/api/now/table/incident";
```

{% endcode %}
{% endstep %}

{% step %}

### Replace the placeholder incident with your own fields

The shipped create example sends `short_description = "test Short Description"`, `comments = "Test comments"` and `urgency`, `priority` and `impact` all `2`. Set them from the request context instead, and keep `priority`, `impact` and `urgency` inside **1 to 3** — the serialisation helper rejects anything else, even though ServiceNow itself accepts 1 to 5. Every property you can set and the ServiceNow column it becomes is in [The incident field mapping](#the-incident-field-mapping) below.

Never log the password, the `Authorization` header, the whole user record or a questionnaire answer.
{% endstep %}

{% step %}

### Choose the idempotency key and check it before creating

The shipped create example has no check at all, so a replayed or retried workflow creates a second incident. Key a user attribute on the request or the role — `SN_INCIDENT_<request id>` — read it before you call ServiceNow, and write it immediately after. `RequestId` is in the `taskVariables` map you were handed:

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

```groovy
final User user = (User) object
final String requestId = (String) taskVariables.get("RequestId")
final String marker = "SN_INCIDENT_" + requestId

if (user?.userAttributes?.any { it.key == marker }) {
    auditLogBuilder.addInfo("Incident already raised for request " + requestId)
    return
}
```

{% endcode %}
{% endstep %}

{% step %}

### Decide the failure contract, knowing that neither option is clean

OpenIAM marks the `TICKET_SYSTEM_INTEGRATION` event successful immediately after `perform` returns, and that call overwrites any result your script set. So there are two choices and no third:

* **Record the reason and return.** Call `auditLogBuilder.setFailureReason(...)` and `auditLogBuilder.addWarning(...)`, then return. Both attributes are written and survive, but the event's **Result** still reads `SUCCESS`. Alert on the *presence of a failure reason* on a `TICKET_SYSTEM_INTEGRATION` event, never on its result.
* **Let the exception escape `perform`.** The request-completion event is then marked failed carrying your exception, the workflow step aborts, and the request does not complete. The child event is written with no result at all. Choose this only where a missing incident must block the request.

What you must not do is what the shipped create example does: catch the error, log it, and return silently.
{% endstep %}

{% step %}

### Attach it to one test resource, and only one

Open the target resource of one disposable request type at `/webconsole/editResource` and set its **Groovy Script** field to your script version. That field is the only selector there is, so the blast radius of this integration is exactly the set of resources you set it on. For a new hire, a self-registration or a group change, set it on the fixed request resource instead — the five ids are listed under [what invokes it](/administration-guide/operations/enterprise-integrations.md#what-invokes-it).
{% endstep %}

{% step %}

### Submit one approved request, then force every failure

Approve one request for `<TEST_USER>`. Confirm exactly one incident in ServiceNow, the exact payload you intended, the marker attribute on the user, and the `TICKET_SYSTEM_INTEGRATION` child event under the request's own audit event. Confirm no secret appears in any log.

Then break it on purpose: a timeout, a non-2xx status, an invalid credential, a malformed body. Each must leave a failure reason on the event, or fail the request, according to the contract you chose in step 5. Finally replay the same workflow and confirm no second incident.
{% endstep %}
{% endstepper %}

## Verification

| Evidence                                                                                                                      | Meaning                                                                                                |
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| A `TICKET_SYSTEM_INTEGRATION` child event under the request's own event, info *"Calling perform operation from `<scriptId>`"* | Your script was found on the resource, instantiated, and `perform` was entered                         |
| That event **failed** with *"Error while instantiating groovy file `<scriptId>`"*                                             | The class could not be loaded, or does not extend `ResourceGroovyScriptHelper`                         |
| That event **succeeded** and carries no failure reason                                                                        | Only that `perform` returned without throwing. On its own it is not evidence a ticket exists           |
| A `FAILURE_REASON` attribute on a **succeeded** `TICKET_SYSTEM_INTEGRATION` event                                             | Your script detected a failure. This is the only place it can appear; the result column cannot show it |
| The request-completion event **failed** carrying your exception, and the child event has no result                            | Your script threw. The request did not complete                                                        |
| A user attribute `SN_INCIDENT_<request id>`                                                                                   | The marker was written, so a replay will not duplicate                                                 |
| The incident in ServiceNow, matched by `result.number`                                                                        | The only proof a ticket exists                                                                         |

## When it goes wrong

| Symptom                                                                                                                    | Inspect                                                                                                                                | Fix                                                                                                                                                                                                 |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No incident, and the audit event says the integration succeeded                                                            | Your script's error handling, then the event's attributes for a failure reason                                                         | OpenIAM marks the event successful once the script returns, whatever the script did. A script that swallows its own exception reports success. Record the reason on the audit log builder, or throw |
| The script never runs at all                                                                                               | The **Groovy Script** field on the request's **target** resource                                                                       | That field is the only selector. A script attached to any other resource is never invoked                                                                                                           |
| A group change, new hire or self-registration ignores the script                                                           | The fixed resources `NEW_GROUP_AR`, `EDIT_GROUP_AR`, `NEW_HIRE_WITH_APPROVAL_AR`, `NEW_HIRE_NO_APPROVAL_AR` and `SELF_REGISTRATION_AR` | Those five paths use a fixed resource id, not the request's target resource. Attach the script there instead                                                                                        |
| `401` from ServiceNow                                                                                                      | **Host URL**, **Login Id** and **Password** on `SERVICE_NOW_MANAGED_SYS`                                                               | The script builds Basic auth from those three fields. The password reads back as a mask, which proves it is masked, not that it is right                                                            |
| `IllegalArgumentException: … must be between 1 and 3 in service now request`, wrapped as *"Error creating JSON object: …"* | `priority`, `impact`, `urgency`                                                                                                        | The serialisation helper allows only 1 to 3. Build the JSON yourself if you need 4 or 5. The message appears in the workflow service log, on no screen                                              |
| ServiceNow answers 201 and the script reports nothing                                                                      | The response body                                                                                                                      | Only 200 and 201 are parsed, and only the `result` object is read. A body without `result` yields nothing and no error                                                                              |
| Duplicate incidents after a workflow replay                                                                                | Whether a marker is read before creating                                                                                               | The shipped create example has none                                                                                                                                                                 |
| You cannot tell which `Incident Number` attribute belongs to which request                                                 | The user's attributes                                                                                                                  | The shipped create example writes one generic attribute name, which collides by design. Key it per request or per role                                                                              |
| The revoke script creates no incidents                                                                                     | The **Assignment Group** attribute on the revoked roles                                                                                | A role without a non-blank value there is treated as non-privileged and skipped                                                                                                                     |
| The request itself fails after you added error handling                                                                    | Whether your script throws                                                                                                             | A throw aborts the workflow step. If the incident must not block the request, record the reason and return instead                                                                                  |

## Roll it back

Roll back the *attachment* first, then the implementation. Clear **Groovy Script** on the resource and confirm that the request path still completes with no `TICKET_SYSTEM_INTEGRATION` event. Only then restore the previous script version, and only then the previous credential.

Do not delete the evidence. Disabling the integration does not remove incidents already created during the change window — reconcile those in ServiceNow — and revoke any temporary integration credential once the rollback is accepted rather than leaving it live.

## The incident field mapping

Your script populates a `ServiceNowIncidentRequest` and serialises it with `ServiceNowUtils.toJsonObject(request, objectMapper)`, the helper the shipped script uses. These are the eighteen properties it has, and the `incident` table column each becomes:

| Property you set   | ServiceNow field    | Type                |
| ------------------ | ------------------- | ------------------- |
| `callerId`         | `caller_id`         | String              |
| `category`         | `category`          | String              |
| `contactType`      | `contact_type`      | String              |
| `assignmentGroup`  | `assignment_group`  | String              |
| `shortDescription` | `short_description` | String              |
| `description`      | `description`       | String              |
| `priority`         | `priority`          | Integer, **1 to 3** |
| `impact`           | `impact`            | Integer, **1 to 3** |
| `urgency`          | `urgency`           | Integer, **1 to 3** |
| `comments`         | `comments`          | String              |
| `cmdbCi`           | `cmdb_ci`           | String              |
| `location`         | `location`          | String              |
| `additionalField`  | `additional_field`  | String              |
| `assignedTo`       | `assigned_to`       | String              |
| `state`            | `state`             | String              |
| `openedBy`         | `opened_by`         | String              |
| `workNotes`        | `work_notes`        | String              |
| `requestedFor`     | `requested_for`     | String              |

The helper validates before it serialises: any of `priority`, `impact` or `urgency` outside 1 to 3 throws `IllegalArgumentException` with *"`<Priority|Impact|Urgency>` property must be between 1 and 3 in service now request"*, wrapped as *"Error creating JSON object: …"*. ServiceNow accepts 1 to 5 for those fields and this helper does not, so build the JSON in your own script if you need the wider range.

The response is parsed only on 200 or 201, only the `result` object is read from the body, and the incident number comes from `result.number`.

## The two shipped examples, and which to copy

Both live under `workflow/resource/` and both extend `ResourceGroovyScriptHelper`: `ServiceNowTicketCreation_1.0.groovy`, one incident when a request completes, and `ServiceNowTicketCreationRevoke_1.0.groovy`, one incident per revoked privileged role.

**Copy the revoke one.** The create example re-reads the user and adds an attribute literally named `Incident Number`.

{% hint style="warning" %}
**A single generic `Incident Number` attribute is not a correlation design.** A user who raises several requests collects several attributes with the same name and no way to tell which incident belongs to which request, and because there is no check before creation, a retried or replayed workflow raises a second incident.
{% endhint %}

The revoke example is the shape to reuse:

* It selects the revoked memberships from the user's roles where the operation is a delete or an end date is set, and skips any role whose **Assignment Group** role attribute is missing or blank. That attribute name is the switch deciding which roles raise incidents at all.
* **It checks for a marker before creating**, looking for a user attribute keyed `SN_INCIDENT_<role id>`. Per-role keys mean no collisions and a real idempotency check.
* It sets `short_description` to `Revoke Privileged Access - <role name>`, takes `assignment_group` from the role attribute, and builds `comments` from the original questionnaire, recovered from the workflow history through the `Request` task variable.
* It catches failures per role, so one bad role does not stop the rest.

Copy that keying and the per-item error handling; copy none of the placeholder values. The revoke example still swallows its errors instead of recording them on the audit log builder, which is what step 5 above is for.

### Testing the revoke path

Exercise, at minimum: a direct role removal, an inherited one, a termination, an entitlement expiry, a role with no **Assignment Group** attribute, several privileged roles revoked at once, a replay where an `SN_INCIDENT_<role id>` marker already exists, and a partial failure after one of several incidents has been created.

One case will surprise you: a role that *has* an **Assignment Group** attribute whose value list is empty is not skipped cleanly. It raises an error inside the per-role handler, which logs *"Error creating ticket for role"* and moves on, so a privileged role silently raises no incident. Give every role that should raise one a non-blank value.

## Related

* [Integration patterns for external systems](/administration-guide/operations/enterprise-integrations.md) — the extension point, its four invocation points and the audit contract
* [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md) — branching, versioning, compiling and pinning a script version
* [Manual fulfilment](/administration-guide/requests/fulfilment.md) — the alternative when a person, not a ticket, is the fulfilment
* [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md) — the two ServiceNow examples among the 745

*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/requests/approval-flows/raise-a-servicenow-incident.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.
