> ## Documentation Index
> Fetch the complete documentation index at: https://docs.serval.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ServiceNow

> Connect ServiceNow to Serval with OAuth 2.0 client credentials or Basic authentication for bidirectional ticket sync, catalog import, knowledge sync, access profiles, and optional live agent handoff.

## About ServiceNow

ServiceNow is an IT service management (ITSM) platform that manages tickets, incidents, change requests, problems, and the service catalog. Connecting ServiceNow to Serval enables native bidirectional ticket sync, service-catalog import and ordering, knowledge-base sync (with optional mirroring of articles into Serval's native knowledge base), and workflow automation across incidents, problems, changes, and HR cases. Serval connects to a single ServiceNow instance using either OAuth 2.0 (client credentials) - the default and recommended option - or HTTP Basic authentication, and acts as a dedicated integration service account.

**Authentication:** OAuth 2.0 (client credentials), or Basic auth

**Data sync:** Bidirectional. Serval polls ServiceNow for ticket and comment updates (with an optional Business Rule webhook for near-real-time comment sync), keeps groups, roles, departments, companies, locations, and cost centers current with scheduled background syncs, can mirror synced knowledge articles into a read-only native Serval knowledge base, and writes back via the Table, Service Catalog, and Change Management REST APIs on demand when workflows run. Sync frequency is admin-configurable per sync.

## What the ServiceNow integration enables

| Capability                        | Description                                                                                                                                                                                                                         |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ticket sync                       | Native bidirectional sync between Serval tickets and ServiceNow incidents, catalog requested items, and HR cases - including comments and work notes. Serval formatting is converted to clean plain text on the way in.             |
| Service catalog import            | Import ServiceNow catalog items so employees can order them through Serval - as a confirmed form, a pure conversation, or a link back to ServiceNow - with reference data, access rules, and source-system popularity carried over. |
| Improve with Catalyst             | Hand any imported catalog item to Catalyst, which redesigns the ITSM form as a conversational Serval experience and submits the request back to ServiceNow.                                                                         |
| Knowledge base                    | Sync ServiceNow knowledge articles into Serval so the AI agent can answer from your documented guidance - and optionally mirror them into a read-only native Serval knowledge base.                                                 |
| Workflow automation               | Build workflows that create and update incidents, problems, and change requests, including approvals and HR cases.                                                                                                                  |
| Access profiles and resource sync | Sync ServiceNow groups, roles, departments, companies, locations, and cost centers, and provision or deprovision group and role membership for access control.                                                                      |
| User criteria import              | Import ServiceNow user criteria as Serval access profiles so the same people who can request an item in ServiceNow can request it in Serval.                                                                                        |
| Configurable sync settings        | Set per-sync frequency (hourly through weekly, or automatic) and an optional ServiceNow encoded-query filter that scopes which catalog items are imported.                                                                          |
| Real-time comment sync (optional) | A single ServiceNow Business Rule pushes new comments to Serval for near-instant delivery instead of waiting for the next poll.                                                                                                     |
| Live agent handoff (optional)     | Hand a conversation off to a human ServiceNow agent through Virtual Agent Bot-to-Bot and bridge the agent's replies back into the Serval ticket.                                                                                    |

Anything exposed by ServiceNow's REST APIs can be accessed through Serval (see the [ServiceNow product documentation](https://www.servicenow.com/docs/)).

### Catalog import

Each catalog item is imported in one of three modes, chosen with the **Collect inputs as** control (and the split Import button) on the catalog import page:

| Mode     | What the employee experiences                                               |
| -------- | --------------------------------------------------------------------------- |
| **Form** | "The agent shows a form to confirm the fields before submitting."           |
| **Chat** | "The agent gathers what's needed conversationally, then submits - no form." |
| **Link** | "The agent sends a link to complete the request in the external system."    |

Items Serval cannot faithfully render fall back to link-only automatically, with the reason shown - client scripts, scripted UI policies, UI scripts, unsupported required field types, or dynamic JavaScript reference qualifiers. Per-item controls let you toggle form confirmation (Form vs. Chat), set **Force link-only mode** (always redirect employees to the ServiceNow form), unimport, or disable an item.

Forms with reference fields - lookups into ServiceNow tables - prompt a **Set up reference data** step. Confirming **Set up reference data** creates entity ingestion configs for those tables so pickers resolve real options; use **Re-sync** to refresh them later. Until reference data is set up, the item shows a **Reference data not set up** state.

While browsing, catalog items appear as structured cards with field counts and a **Ready** status, a category filter, and a **Most requested** sort backed by source-system popularity ("Ordered N times in the source system"). Each item's **Understanding** tab shows everything Serval captured from ServiceNow - variables, client scripts, UI policies (rendered as When/Show/Hide/Require/Lock rules, with scripted ones flagged), script includes, and the user criteria that gate access - along with an AI second opinion on whether the item can be filled conversationally.

If a sync gets stuck, it can be cancelled or restarted with **Sync now**; stale syncs are detected automatically.

### Improve with Catalyst

Every imported catalog item has an **Improve with Catalyst** action. It launches a Catalyst session pre-loaded with the item's full captured understanding - variables, client scripts, UI policies, script includes, user criteria, and the imported workflow as a starting point - plus a built-in skill that reimagines the ITSM form as a conversational Serval experience: one orchestrating skill that asks only what the employee must actually decide, small collection workflows per branch, and a submit workflow that posts the request back to ServiceNow and links the resulting requested item to the ticket.

There are two entry points: for natively importable items ("Turn this form into a conversation"), and for items too complex to import, where Catalyst redesigns around the client scripts and UI policies that block native import. Resources Catalyst builds stay attached to the catalog item under a **Built with Catalyst** list (with Skill/Workflow badges and an Unlink action), even after the session ends. This is the recommended path for items that were flagged link-only.

### Importing user criteria as access profiles

The **User Criteria** tab in the catalog import area imports ServiceNow user criteria as Serval access profiles. Pick a ServiceNow connection, click **Sync user criteria** to pull criteria from ServiceNow (progress is live-polled, and failures show error details with copyable run identifiers), then review each criterion and choose **Import as access profile**.

The detail panel for each criterion shows:

* **Who it matches** - groups, roles, user attribute conditions (attribute criteria such as department or location map to Serval access-profile attribute conditions), and individual users.
* **What it makes requestable** - the catalog items the criterion gates, including whether each link is an allow ("Available") or deny ("Not available") relationship.
* **Proposed access profile** - a preview of the Serval equivalent before you import.

Criteria backed by ServiceNow match scripts show a **Cannot be fully mirrored** warning, and a criterion with no captured conditions warns that importing it would grant access to everyone on the team. A bulk **Set up all access profiles** action imports everything outstanding, with progress shown. Each catalog item's Access tab cross-links to the user criteria that gate it and back; once imported, the access profiles gate the corresponding catalog items in Serval the same way ServiceNow does.

### Mirroring knowledge articles into Serval

Knowledge sync can do more than index articles for search: once syncing is enabled for a knowledge source, the source's settings panel shows an **Import to Serval Knowledge Base** toggle (off by default). Its help text reads: "When enabled, synced articles from ServiceNow are also written into a Serval native knowledge base. Disable to stop syncing without deleting already-imported pages."

When enabled:

* Mirrored knowledge bases appear in the Knowledge Bases sidebar under an **External Connections** section.
* Mirrored documents are sync-locked and read-only, with a banner - "This document is synced from ServiceNow and is read-only." - and a **View in ServiceNow** link.
* Article HTML is converted to native editor blocks (tables, lists, and headings are supported), and the folder hierarchy from ServiceNow is preserved.
* Re-syncs update content in place and record version history.
* Clicking a knowledge item reference offers a destination choice: **Open in Serval** or **Open in ServiceNow**.

Disabling the toggle stops future writes but keeps already-imported pages - they freeze rather than delete. Sources or folders with nothing syncable show a **No documents** sync status.

### Sync settings: frequency and catalog filter

A **Sync settings** dialog - reachable from the app instance's Workflow Sync settings and from the catalog import page - exposes two controls for catalog sync:

* **Catalog filter** - an optional ServiceNow encoded query (for example `category=hardware^active=true`). Only matching catalog items are imported; leaving it blank imports everything. The filter is combined (AND) with the standard active-items filter. This field appears only for integrations that support it - currently ServiceNow.
* **Sync frequency** - Automatic (recommended, the default), Every hour, Every 6 hours, Every 12 hours, Once a day, or Once a week.

Database/entity ingestion syncs are configurable too: each entity ingestion config's side panel has the same **Sync frequency** selector.

Two things to know about cadence:

* Unset means the platform default: catalog sync fan-out runs every 30 minutes for ServiceNow, and entity ingestion ticks every 4 hours.
* A configured interval is a **floor, not an exact cadence** - scheduled ticks are skipped until the interval has elapsed since the last completed sync. For entity syncs, values at or below about 4 hours behave like Automatic; only the 6-hour, 12-hour, daily, and weekly settings genuinely throttle.

### How formatting translates to ServiceNow

ServiceNow journal fields (comments and work notes) and description/close-notes fields render plain text only, so everything Serval writes into them is automatically converted from Serval's rich formatting to clean plain text. This applies to the built-in two-way ticket sync for all three ServiceNow ticket types - incidents, catalog requested items, and HR cases - covering ticket descriptions on create and update, and message bodies posted as comments or work notes.

| Serval formatting                    | What ServiceNow receives                           |
| ------------------------------------ | -------------------------------------------------- |
| Links and images                     | "text (url)" - ServiceNow auto-linkifies bare URLs |
| Bullet and numbered lists            | Normalized list lines beginning with "- "          |
| Bold, italic, headings, code markers | Stripped, text preserved                           |
| @-mentions                           | Resolved to the user's name                        |

Custom workflows that write their own content into ServiceNow text fields should run it through the markdown-to-ServiceNow-text helper the ServiceNow SDK provides, so raw formatting markers do not leak into the record (see the gotcha below).

***

## How the connection works

Serval connects to **one ServiceNow instance** and acts as a single dedicated integration user. You pick one of two authentication methods when you connect:

* **OAuth 2.0 (Client Credentials)** - the default and recommended option. On each request, Serval exchanges your **Client ID** and **Client Secret** for a short-lived bearer token from your instance's token endpoint (`https://<instance>.service-now.com/oauth_token.do`) and sends that token on every API call. ServiceNow runs those calls as the **OAuth Application User** you designate (the same dedicated integration user described below), so that user's roles and table ACLs determine exactly what Serval can do. Serval does **not** request any OAuth scopes - the token is bounded entirely by the integration user's permissions, not by a scope list.
* **Basic authentication** - Serval stores the integration user's **username and password** and sends them as standard HTTP Basic auth on every call. Use this when inbound OAuth client credentials aren't available on your instance, or for a quick test setup.

<Note>
  Whichever method you choose, you create the **same** dedicated integration user and assign it the **same** roles and ACLs. The only difference is how Serval authenticates as that user.
</Note>

Serval only ever calls hosts ending in `service-now.com` - requests to any other host are refused before they leave Serval. Data flows both ways: Serval polls your instance for ticket and comment changes, keeps groups, roles, departments, companies, locations, and cost centers current with scheduled background syncs, and writes back (creating and updating incidents, problems, change requests, HR cases, and catalog requests) on demand when a workflow runs.

<Info>
  Live agent handoff (Virtual Agent Bot-to-Bot) uses one extra shared token that is **not** part of the connect form. You generate it later from the ServiceNow app's Ticket Sync settings - see [Live agent handoff](#live-agent-handoff-via-va-bot-to-bot-optional) at the end of this page.
</Info>

## Get your credentials

You configure everything on the **ServiceNow side first**, then paste two values into Serval. The authoritative provider reference is the [ServiceNow product documentation](https://www.servicenow.com/docs/); the exact clickpath for inbound OAuth is in ServiceNow's official guide, [Enable OAuth with inbound REST](https://www.servicenow.com/docs/bundle/yokohama-api-reference/page/integrate/inbound-rest/task/t_EnableOAuthWithREST.html).

<Warning>
  Use a **dedicated service account**, never a personal or full-admin account. Serval runs every call as this user, so least-privilege here is what bounds what Serval can do - and keeps your ServiceNow audit trail clean (`sys_created_by` / `sys_updated_by` will show this account).
</Warning>

<Steps>
  <Step title="Create a dedicated integration user">
    In ServiceNow, go to **User Administration → Users** and create a new user. Recommended values:

    | Field                         | Value                                             |
    | ----------------------------- | ------------------------------------------------- |
    | **User ID**                   | `serval.integration`                              |
    | **First name**                | `Serval`                                          |
    | **Last name**                 | `Integration`                                     |
    | **Active**                    | Checked                                           |
    | **Web service access only**   | Checked (blocks interactive login)                |
    | **Internal Integration User** | Checked (if available in your ServiceNow version) |

    <Warning>
      Give the user **both a first and last name**. ServiceNow's inbound-OAuth user picker omits users that are missing either name - a known quirk of the picker (see this [ServiceNow Community thread](https://www.servicenow.com/community/developer-forum/zurich-integration-user-doesn-t-show-up-in-oauth-application/td-p/3502337)) - so you won't be able to select the account as the OAuth Application User later.
    </Warning>

    If you plan to use **Basic authentication**, also click **Set Password → Generate Password** and copy the password now - you will not be able to view it again. You'll paste it into Serval. If you use OAuth 2.0, you typically do not need a password for Serval: API access is authorized via Client ID and Client Secret, and the integration user is designated on the OAuth application below.
  </Step>

  <Step title="Create a custom role for Serval">
    <Info>
      Creating a custom role with granular permissions is the **recommended approach** for production environments. It follows the principle of least privilege and provides better security than out-of-the-box roles like `itil` or `admin`. (The simpler OOB-role alternative is covered in the next step.)
    </Info>

    Go to **User Administration → Roles** and create:

    | Field           | Value                                                                |
    | --------------- | -------------------------------------------------------------------- |
    | **Name**        | `x_serval_integration`                                               |
    | **Description** | `Custom role for Serval integration with least-privilege API access` |

    Then open the role and, in the **Contains Roles** related list, add these base roles:

    | Role                     | Purpose                                              |
    | ------------------------ | ---------------------------------------------------- |
    | `rest_api_explorer`      | Enables REST API access                              |
    | `personalize_choices`    | Allows reading sys\_choice values                    |
    | `personalize_dictionary` | Allows reading sys\_dictionary for field definitions |

    These base roles provide the foundational REST API capabilities; granular table access comes from the ACLs in the next step.
  </Step>

  <Step title="Grant table access via ACLs">
    Serval requires specific read/write access to ServiceNow tables. Create Access Control List (ACL) rules that grant only the necessary permissions.

    <Info>
      Serval uses ServiceNow's **Table API**, **Service Catalog API**, **Attachment API** (all active by default), and the **Change Management REST API**, which requires the Change Management - REST API plugin (`com.snc.change_management.rest_api`) - active by default on most instances.
    </Info>

    <Tabs>
      <Tab title="Required Table Access">
        #### Incident Management Tables

        | Table               | Read | Create | Write | Purpose                                |
        | ------------------- | ---- | ------ | ----- | -------------------------------------- |
        | `incident`          | Yes  | Yes    | Yes   | Incident records (all standard fields) |
        | `sys_journal_field` | Yes  | -      | -     | Comments and work notes                |

        #### HR Service Delivery Tables

        Required only if you sync or create HR cases.

        | Table             | Read | Create | Write | Purpose                                            |
        | ----------------- | ---- | ------ | ----- | -------------------------------------------------- |
        | `sn_hr_core_case` | Yes  | Yes    | Yes   | HR case records, including comments and work notes |

        #### Service Catalog Tables

        | Table                      | Read | Create | Write | Purpose                                                                                                                      |
        | -------------------------- | ---- | ------ | ----- | ---------------------------------------------------------------------------------------------------------------------------- |
        | `sc_cat_item`              | Yes  | -      | -     | Catalog item details                                                                                                         |
        | `sc_category`              | Yes  | -      | -     | Catalog categories                                                                                                           |
        | `sc_request`               | Yes  | Yes    | -     | Service requests                                                                                                             |
        | `sc_req_item`              | Yes  | Yes    | Yes   | Requested items                                                                                                              |
        | `item_option_new`          | Yes  | -      | -     | Catalog item variable definitions                                                                                            |
        | `io_set_item`              | Yes  | -      | -     | Variable set to catalog item mapping                                                                                         |
        | `question_choice`          | Yes  | -      | -     | Choices for select-type catalog variables                                                                                    |
        | `catalog_script_client`    | Yes  | -      | -     | Catalog client scripts (fillability analysis, Understanding tab)                                                             |
        | `catalog_ui_policy`        | Yes  | -      | -     | Catalog UI policies (When/Show/Hide/Require rules)                                                                           |
        | `catalog_ui_policy_action` | Yes  | -      | -     | UI policy actions                                                                                                            |
        | `sys_script_include`       | Yes  | -      | -     | Script includes referenced by catalog client scripts                                                                         |
        | `sys_attachment`           | -    | Yes    | -     | File attachment uploads for requested items (written through the Attachment API, but the user still needs create permission) |

        #### Configuration Tables (Read-Only)

        | Table             | Read | Create | Write | Purpose                    |
        | ----------------- | ---- | ------ | ----- | -------------------------- |
        | `sys_user`        | Yes  | -      | -     | User lookup for assignment |
        | `sys_user_group`  | Yes  | -      | -     | Assignment group lookup    |
        | `sys_choice`      | Yes  | -      | -     | Field dropdown values      |
        | `sys_dictionary`  | Yes  | -      | -     | Field definitions          |
        | `cmdb_ci_service` | Yes  | -      | -     | Business services          |

        #### Knowledge Base Tables (Read-Only)

        | Table                            | Read | Create | Write | Purpose                                                                                   |
        | -------------------------------- | ---- | ------ | ----- | ----------------------------------------------------------------------------------------- |
        | `kb_knowledge_base`              | Yes  | -      | -     | Knowledge base metadata                                                                   |
        | `kb_category`                    | Yes  | -      | -     | Knowledge categories                                                                      |
        | `kb_knowledge`                   | Yes  | -      | -     | Knowledge articles                                                                        |
        | `kb_article_template`            | Yes  | -      | -     | Article template metadata (templated articles)                                            |
        | `kb_article_template_definition` | Yes  | -      | -     | Article template definitions; templated articles also read their extended template tables |

        #### User Criteria Tables (Read-Only)

        These tables determine which users have access to knowledge articles and catalog items, and back the user criteria import.

        | Table                               | Read | Create | Write | Purpose                                                    |
        | ----------------------------------- | ---- | ------ | ----- | ---------------------------------------------------------- |
        | `user_criteria`                     | Yes  | -      | -     | User criteria definitions (users, groups, companies, etc.) |
        | `kb_uc_can_read_mtom`               | Yes  | -      | -     | Knowledge article access - users who CAN read              |
        | `kb_uc_cannot_read_mtom`            | Yes  | -      | -     | Knowledge article access - users who CANNOT read           |
        | `sc_cat_item_user_criteria_mtom`    | Yes  | -      | -     | Catalog item access - users who CAN access                 |
        | `sc_cat_item_user_criteria_no_mtom` | Yes  | -      | -     | Catalog item access - users who CANNOT access              |

        #### Change Management Tables

        | Table                  | Read | Create | Write | Purpose                                                               |
        | ---------------------- | ---- | ------ | ----- | --------------------------------------------------------------------- |
        | `change_request`       | Yes  | Yes    | Yes   | Change request records                                                |
        | `sysapproval_approver` | Yes  | -      | Yes   | Change request approval records (write is needed to act on approvals) |

        <Note>
          Creating change requests uses the Change Management REST API, which requires the **Change Management - REST API** plugin (`com.snc.change_management.rest_api`) to be active on your ServiceNow instance. This plugin is active by default on most instances.
        </Note>

        #### Problem Management Tables

        | Table     | Read | Create | Write | Purpose         |
        | --------- | ---- | ------ | ----- | --------------- |
        | `problem` | Yes  | Yes    | Yes   | Problem records |

        #### Access Management & Resource Sync Tables

        These tables are required if you use Serval's access profile capabilities for syncing organizational resources and provisioning/deprovisioning users.

        <Info>
          If you are not using access profiles, you can skip these ACLs. The `sys_user` and `sys_user_group` tables listed above under Configuration Tables still require **read** access for basic functionality regardless.
        </Info>

        | Table               | Read | Create | Write | Delete | Purpose                                                                   |
        | ------------------- | ---- | ------ | ----- | ------ | ------------------------------------------------------------------------- |
        | `sys_user`          | -    | -      | Yes   | -      | Write access for department assignment (read access already listed above) |
        | `sys_user_grmember` | Yes  | Yes    | -     | Yes    | Group membership provisioning and deprovisioning                          |
        | `sys_user_role`     | Yes  | -      | -     | -      | Role definitions for resource sync                                        |
        | `sys_user_has_role` | Yes  | Yes    | -     | Yes    | Role assignment provisioning and deprovisioning                           |
        | `core_company`      | Yes  | -      | -     | -      | Company records for resource sync                                         |
        | `cmn_department`    | Yes  | -      | -     | -      | Department records for resource sync                                      |
        | `cmn_location`      | Yes  | -      | -     | -      | Location records for resource sync                                        |
        | `cmn_cost_center`   | Yes  | -      | -     | -      | Cost center records for resource sync                                     |
      </Tab>

      <Tab title="Create ACLs">
        For each table that requires access, create appropriate ACLs:

        <Steps>
          <Step title="Navigate to ACLs">
            1. In the navigator, search for **"Access Control (ACL)"**
            2. Select **System Security → Access Control (ACL)**
          </Step>

          <Step title="Create Read ACLs">
            For each table that requires **read** access, create a new ACL:

            1. Click **New**
            2. Set **Type** to `record`
            3. Set **Operation** to `read`
            4. Set **Name** to the table name (e.g., `incident`)
            5. In the **Requires role** related list, add your custom role `x_serval_integration`
            6. Click **Submit**

            Repeat for all tables listed in the Required Table Access tab.
          </Step>

          <Step title="Create Write ACLs">
            For tables that require **create**, **write**, or **delete** access:

            1. Click **New**
            2. Set **Type** to `record`
            3. Set **Operation** to `create`, `write`, or `delete` as needed
            4. Set **Name** to the table name
            5. In the **Requires role** related list, add your custom role `x_serval_integration`
            6. Click **Submit**

            Create ACLs for the following tables:

            **Incident, Problem & HR Case Management:**

            * `incident` (create, write)
            * `problem` (create, write)
            * `sn_hr_core_case` (create, write - only if you use HR cases)

            **Change Management:**

            * `change_request` (create, write)
            * `sysapproval_approver` (write)

            **Service Catalog:**

            * `sc_request` (create)
            * `sc_req_item` (create, write)
            * `sys_attachment` (create)

            **Access Management (if using access profiles):**

            * `sys_user` (write)
            * `sys_user_grmember` (create, delete)
            * `sys_user_has_role` (create, delete)
          </Step>
        </Steps>
      </Tab>

      <Tab title="Alternative: Use OOB Roles">
        <Warning>
          Using out-of-the-box roles is simpler but grants **more permissions than necessary**. Only use this approach for development/testing or if your organization's security policy permits.
        </Warning>

        Instead of creating custom ACLs, you can assign these out-of-the-box roles to the integration user:

        | Role         | Purpose                                           | Notes                                                                  |
        | ------------ | ------------------------------------------------- | ---------------------------------------------------------------------- |
        | `itil`       | ITSM functionality (incidents, problems, changes) | Grants broad access to incident, problem, and change management        |
        | `catalog`    | Service catalog access                            | For catalog item browsing, ordering, and request management            |
        | `knowledge`  | Knowledge base access                             | For knowledge article sync                                             |
        | `user_admin` | User and group administration                     | Required only if using access profiles for provisioning/deprovisioning |

        <Note>
          The `itil` role includes access to the Change Management REST API. If you use the custom role approach instead, ensure the **Change Management - REST API** plugin (`com.snc.change_management.rest_api`) is active on your instance.
        </Note>

        To assign roles:

        1. Open the integration user record
        2. Scroll to the **Roles** related list
        3. Click **Edit**
        4. Add the roles from the list above
        5. Click **Save**
      </Tab>
    </Tabs>
  </Step>

  <Step title="Assign the custom role to the integration user">
    1. Navigate to **User Administration → Users** and open the `serval.integration` user
    2. Scroll down to the **Roles** related list and click **Edit**
    3. Add the `x_serval_integration` role (or the OOB roles if you chose that approach)
    4. Click **Save**
  </Step>

  <Step title="(OAuth only) Enable the inbound client-credentials grant">
    Inbound OAuth client credentials are **disabled by default** on most ServiceNow instances. In **System Properties** (`sys_properties.list`), find or create:

    | Field           | Value                                                      |
    | --------------- | ---------------------------------------------------------- |
    | **Name**        | `glide.oauth.inbound.client.credential.grant_type.enabled` |
    | **Type**        | true \| false                                              |
    | **Value**       | `true`                                                     |
    | **Application** | `Global`                                                   |

    <Warning>
      If this property is missing or `false`, **every** token request to `oauth_token.do` fails with `access_denied`, no matter how the OAuth app is configured. This is the single most common cause of a failed OAuth connection. ServiceNow may also display a banner on the Application Registry form warning that the property is disabled. ServiceNow documents this requirement in its official guide, [Up your OAuth 2.0 game: Inbound Client Credentials](https://www.servicenow.com/community/developer-blog/up-your-oauth2-0-game-inbound-client-credentials-with-washington/ba-p/2816891).
    </Warning>
  </Step>

  <Step title="(OAuth only) Create the OAuth Application Registry entry">
    Go to **System OAuth → Application Registry → New**. ServiceNow shows a list of OAuth application types - pick the one for **inbound** access (external systems calling **into** your instance). Do **not** choose **Connect to a third party OAuth Provider** or other outbound options; those are for ServiceNow calling external OAuth servers, not for Serval.

    * **Recommended:** **New Inbound Integration Experience** - the current UI for inbound OAuth, including client credentials. When the wizard asks **Select your application connection type**, choose **OAuth - Client credentials grant** (machine-to-machine). Do not choose Authorization code, JWT bearer, Resource owner password, or third-party ID token flows for the Serval integration.
    * **Alternative:** **\[Deprecated UI] Create an OAuth API endpoint for external clients** - the older wizard for the **same** kind of inbound app. "Deprecated UI" refers to ServiceNow replacing the screen flow, not to the credentials; it still produces a working Client ID / Client Secret. Some teams see odd default forms here (for example, the OAuth Application User field hidden until you change the form layout); using the New Inbound Integration Experience often avoids that.

    <Frame caption="ServiceNow New Inbound Integration Experience: choose OAuth - Client credentials grant. The next steps in the wizard (or on the Application Registry record) are where you tie the app to your integration user for roles and auditing.">
      <img src="https://mintcdn.com/serval/kEJkqsoMd8RKyAo5/images/integrations/servicenow/inbound-oauth-connection-type-modal.png?fit=max&auto=format&n=kEJkqsoMd8RKyAo5&q=85&s=796a2d9d7ac80cc9909b42b5cc036c52" alt="ServiceNow modal Select your application connection type showing five options; OAuth Client credentials grant is highlighted as the choice for Serval" width="1024" height="937" data-path="images/integrations/servicenow/inbound-oauth-connection-type-modal.png" />
    </Frame>

    Give the application a clear name (for example, `Serval integration`) and save; ServiceNow auto-generates the Client Secret. Then:

    1. Set **Client type** to run API calls as your integration account - for example **Integration as a Service** (wording varies by release). That tells ServiceNow which `sys_user` supplies roles, ACLs, and audit fields for requests made with a client-credentials token.
    2. Allow the **Client credentials** grant. Often this is not on the main form: open the **OAuth Entity** or **OAuth Entity Profile** related list on the same Application Registry record, open the related row, and confirm **Client credentials** is enabled. Save that record if you changed it.
    3. Set the **OAuth Application User** to your `serval.integration` user, so issued tokens act as that account.

    <Tip>
      If you don't see an **OAuth Application User** field on the form, ServiceNow hides it on the default layout. Open the form's context menu → **Configure → Form Layout**, move **OAuth Application User** from *Available* to *Selected*, save, and reload. Some instances instead expose the value as a column in the Application Registry **list view**, or on the related **OAuth Entity Profile** row. On the newer Inbound Integration UI, only the first 1,000 users (alphabetical) appear in the picker - a [documented ServiceNow known error](https://support.servicenow.com/kb?id=kb_article_view\&sysparm_article=KB2624687) - search precisely for your account.
    </Tip>

    The integration user must already have the roles and ACLs from the earlier steps so token-backed API calls are authorized correctly.
  </Step>

  <Step title="(OAuth only) Copy the Client ID and Client Secret">
    From the OAuth application record, copy the **Client ID** and **Client Secret** (click the lock icon to reveal the secret). Store them securely - anyone holding both can mint tokens that act as your integration user.
  </Step>

  <Step title="Identify your ServiceNow Instance Name">
    Your **Instance Name** is the subdomain of your ServiceNow URL: if your instance lives at `https://mycompany.service-now.com`, the instance name is `mycompany`. You'll enter it in Serval next.
  </Step>
</Steps>

## Connect in Serval

In Serval, open **Apps → ServiceNow → Connect** and complete the form. The fields that appear depend on the **Authentication method** you pick.

<Steps>
  <Step title="Enter your Instance Name">
    **Instance Name** - the subdomain of your ServiceNow instance. For `https://mycompany.service-now.com`, enter `mycompany`.

    <Tip>
      You can paste the full URL (e.g. `https://mycompany.service-now.com/now/nav`) - Serval strips the scheme, path, port, and the `.service-now.com` suffix and lowercases what's left. If the result isn't a single valid subdomain, you'll see *"Instance Name must be the subdomain of your ServiceNow instance (e.g. 'mycompany' for mycompany.service-now\.com)"*.
    </Tip>
  </Step>

  <Step title="Choose the authentication method and fill in credentials">
    **Authentication method** defaults to **OAuth 2.0 (Client Credentials)**. The remaining fields switch based on your choice:

    <Tabs>
      <Tab title="OAuth 2.0 (recommended)">
        | Field             | What to paste                                         |
        | ----------------- | ----------------------------------------------------- |
        | **Client ID**     | The Client ID from your ServiceNow OAuth application. |
        | **Client Secret** | The Client Secret from the same OAuth application.    |
      </Tab>

      <Tab title="Basic authentication">
        | Field        | What to paste                                                     |
        | ------------ | ----------------------------------------------------------------- |
        | **Username** | The User ID of your integration user (e.g. `serval.integration`). |
        | **Password** | The password you set for that user.                               |
      </Tab>
    </Tabs>
  </Step>

  <Step title="Save the connection">
    Save. Serval immediately tries to authenticate against your instance. If anything is off (wrong instance, bad credentials, or inbound OAuth not enabled), ServiceNow's error surfaces back to you.
  </Step>
</Steps>

<Note>
  When you later **edit** the connection, secret fields show as masked placeholders. Leave them untouched to keep the stored values; only retype a field to change it. Switching auth methods (OAuth ↔ Basic) does require you to enter that method's credentials fresh.
</Note>

## Verifying the connection

Open the ServiceNow app's **API Integration** tab and run the health checks. They confirm Serval can authenticate as your integration user and exercise the real surfaces it uses:

* **Test ServiceNow Connection** - authenticates and queries your user table (a single row). Success reads *"Successfully authenticated with ServiceNow"*. Failure reads *"Unable to connect to ServiceNow. Please verify your credentials are valid and have the necessary permissions."*
* **List ServiceNow Users** - samples up to ten rows from `sys_user`. Success reports the sample size; failure reads *"Unable to list users from ServiceNow. Please verify the credentials have permission to read the sys\_user table."*
* **List ServiceNow Incidents**, **List ServiceNow Knowledge Articles**, **List ServiceNow Catalog Items**, and **List ServiceNow User Criteria** - the same ten-row sample pattern against `incident`, `kb_knowledge`, `sc_cat_item`, and `user_criteria`. Each failure message names the table the credentials couldn't read.

<Info>
  A deeper lifecycle health check **creates real records on your instance**: it looks up the `serval.integration` user, creates a temporary incident whose short description begins with "Health Check Test Incident", drives it through state transitions (New → In Progress → On Hold with a hold reason → In Progress), resolves it, creates additional short-lived incidents to exercise impact/urgency permutations (all resolved afterwards), and places a test catalog request against your first active catalog item, then closes the requested item. Test incidents are **resolved, not deleted** - seeing "Health Check Test" records appear and resolve is expected. The resolution-codes check fails if your instance has no resolution codes defined. The user lookup is hard-coded to the `serval.integration` user name - if you chose a different User ID in [Get your credentials](#get-your-credentials), this check fails at the user-lookup step even though the connection itself is healthy.
</Info>

<Tip>
  A green Test Connection but a failing list check almost always means the integration user is missing a table ACL (for example, read on `kb_knowledge` or `user_criteria`). The failing check names the table it couldn't reach.
</Tip>

## Gotchas and troubleshooting

<AccordionGroup>
  <Accordion title="OAuth fails with access_denied / server_error">
    This means ServiceNow **rejected the token request** at `oauth_token.do` - not a Serval storage problem. Work through these on the ServiceNow side, most common first:

    1. **Inbound client credentials enabled** - `glide.oauth.inbound.client.credential.grant_type.enabled` must be `true` in **Global** scope. This is the number-one cause.
    2. **OAuth Application User set** - the app must be tied to your integration user, and that user must be **Active** with the right roles/ACLs.
    3. **Client credentials grant allowed** - confirm it's enabled on the app (or its OAuth Entity / Entity Profile record).
    4. **Client ID / Secret correct** - re-copy from ServiceNow with no leading or trailing spaces. If you rotated the secret in ServiceNow, update the Serval connection too.

    If all four checks above pass and the token request still fails, the cause is on the ServiceNow side rather than in how Serval formats the request - contact **[support@serval.com](mailto:support@serval.com)**.
  </Accordion>

  <Accordion title="The integration user doesn't appear in the OAuth Application User picker">
    ServiceNow's inbound-OAuth user picker omits accounts that are **missing a first or last name** - a known quirk of the picker (see this [ServiceNow Community thread](https://www.servicenow.com/community/developer-forum/zurich-integration-user-doesn-t-show-up-in-oauth-application/td-p/3502337)). Populate both names on the user, confirm it's **Active**, and reload. On the newer Inbound Integration experience, only the first 1,000 users alphabetically are listed - a [documented ServiceNow known error](https://support.servicenow.com/kb?id=kb_article_view\&sysparm_article=KB2624687) - so search precisely. If the field itself is absent, add it via **Configure → Form Layout** as described in [Get your credentials](#get-your-credentials).
  </Accordion>

  <Accordion title="Instance Name was rejected">
    Serval normalizes whatever you paste down to a single subdomain. Blank input returns *"Instance Name is required"*. Anything that doesn't reduce to a valid DNS label returns *"Instance Name must be the subdomain of your ServiceNow instance (e.g. 'mycompany' for mycompany.service-now\.com)"*. Enter just `mycompany` (or paste the full URL and let Serval trim it) - don't enter an IP, a non-`service-now.com` host, or extra path segments that aren't part of the subdomain.
  </Accordion>

  <Accordion title="'Required' validation errors when saving credentials">
    Serval enforces complete credentials per method:

    * OAuth: *"Client ID and Client Secret are required for OAuth 2.0"* (or *"Client ID and Client Secret are required when switching to OAuth 2.0"* if you changed methods).
    * Basic: *"Username and Password are required for Basic Auth"* (or *"Username and Password are required when switching to Basic Auth"*).

    When you're only editing an existing OAuth or Basic connection, you can leave the masked fields as-is; Serval keeps the stored values. But **switching** auth methods requires entering that method's credentials in full.
  </Accordion>

  <Accordion title="A write 'succeeds' (200) but the field didn't change">
    ServiceNow choice fields (like `close_code`, `category`, `priority`) silently ignore invalid values - the API returns 200 but the field stays empty, and a downstream Data Policy then blocks the state change. Valid choice values are **instance-specific**, so Serval's helpers and workflows query them rather than hard-coding. If a workflow write isn't sticking, the usual cause is an instance Business Rule, write ACL, or Data Policy on the target table - those are configured in ServiceNow and can't be worked around from Serval.
  </Accordion>

  <Accordion title="Change requests won't create or transition state">
    Change-request creation needs the **Change Management - REST API** plugin (`com.snc.change_management.rest_api`) active. Most state transitions also require an **assignment group** to be set - set it at creation time, since adding it later is often blocked. Change approvals live in a **separate table** (`sysapproval_approver`), so the integration user needs write access there for approval workflows to work. A `403` from the Change Management API usually means a state-model condition isn't met; a `400` naming a rule means a Business Rule is blocking.
  </Accordion>

  <Accordion title="Knowledge, catalog, or access-profile syncs come back empty">
    Each surface needs its own read ACLs on the integration user's role. Empty knowledge sync → check read on the `kb_*` tables; empty catalog → check the catalog definition tables; empty access profiles → check the `user_criteria` and organizational tables. The matching health check (knowledge articles, catalog items, user criteria) will name the table it couldn't read.
  </Accordion>

  <Accordion title="Catalog sync fails or imports nothing after setting a catalog filter">
    The **Catalog filter** in Sync settings is a ServiceNow encoded query that is combined (AND) with the standard active-items filter - a filter that matches nothing imports nothing. The sync also **fails loudly if the integration user lacks the ACLs needed to run the query** against the catalog tables, so if a previously working sync breaks right after adding a filter, check the filter syntax first and the integration user's catalog-table read access second.
  </Accordion>

  <Accordion title="A configured sync frequency doesn't seem to take effect">
    Configured intervals are a **minimum interval, not an exact schedule**: the platform's scheduled ticks simply skip a sync until the configured interval has elapsed since the last completed run. Entity ingestion ticks every 4 hours, so entity-sync values at or below 4 hours behave like Automatic - only Every 6 hours, Every 12 hours, Once a day, and Once a week genuinely slow things down. Catalog sync fan-out runs every 30 minutes by default.
  </Accordion>

  <Accordion title="Mirrored knowledge documents can't be edited or moved in Serval">
    That's by design. While **Import to Serval Knowledge Base** is enabled, mirrored documents are sync-locked - ServiceNow remains the source of truth and re-syncs update the Serval copy in place. Edit the article in ServiceNow (the document banner has a **View in ServiceNow** link). Disabling the toggle stops future writes but keeps already-imported pages rather than deleting them.
  </Accordion>

  <Accordion title="A catalog item only imports as a link">
    Serval falls back to link-only automatically when it can't faithfully render the form: client scripts, scripted UI policies, UI scripts, unsupported required field types, or dynamic JavaScript reference qualifiers. The item's Understanding tab shows exactly which of these applies. You can keep it as a link, or use **Improve with Catalyst** to redesign the form as a conversational experience that works around those constraints.
  </Accordion>

  <Accordion title="A user criterion imports with warnings, or grants more access than expected">
    Criteria backed by ServiceNow **match scripts** show a **Cannot be fully mirrored** warning - Serval imports the declarative parts (groups, roles, attributes, users) but cannot execute the script. And a criterion with **no captured conditions** imports as an access profile that matches everyone on the team - Serval warns before you import it. Review the **Proposed access profile** preview before confirming.
  </Accordion>

  <Accordion title="Custom workflow writes show literal formatting markers in ServiceNow">
    ServiceNow's journal and description fields render plain text only. Serval's built-in ticket sync converts formatting automatically (see [How formatting translates to ServiceNow](#how-formatting-translates-to-servicenow)), but a custom workflow that writes Serval-authored content directly into description, comments, work notes, or close notes should first run it through the markdown-to-ServiceNow-text helper in the ServiceNow SDK - otherwise bold markers, bullets, and link syntax appear verbatim in the record.
  </Accordion>

  <Accordion title="Basic auth works but you want to move to OAuth (or vice versa)">
    You can switch methods on an existing connection without recreating it - just change **Authentication method** and enter the new method's credentials. Serval clears the old method's stored secret when you switch, so there's no stale password or client secret left behind. The Instance Name and the integration user stay the same.
  </Accordion>
</AccordionGroup>

***

## Real-time comment sync via webhook (optional)

By default, Serval polls your ServiceNow instance to sync comments. For faster, near-instant comment delivery, you can configure a Business Rule that pushes new comments to Serval via webhook. This applies to comments on Incidents, HR cases, and Catalog Requested Items that were created by the Serval integration.

<Info>
  This requires a single Business Rule on the `sys_journal_field` table. No ServiceNow plugins, store apps, or additional licensing are required.
</Info>

### How it works

When a comment is added to a Serval-managed record, an **async insert** Business Rule fires on the `sys_journal_field` table. The rule verifies the parent record was opened by the Serval service account (the dedicated integration user you created in [Get your credentials](#get-your-credentials)), constructs a JSON payload, and sends it asynchronously to the Serval webhook. Serval processes the event and delivers the comment to the relevant conversation in real time - entries on the `comments` field surface as public replies, entries on `work_notes` surface as internal notes.

<Note>
  The script filters on the parent record's **opened\_by** field, not on table name - it fires for journal entries on any record opened by the Serval service account. Serval's webhook handler processes events for `incident`, `sn_hr_core_case`, and `sc_req_item` records.
</Note>

### Get your webhook token

The webhook carries a per-environment token that Serval provides during onboarding, sent in the `X-Serval-Webhook-Token` header (server-side verification of this token is on the roadmap; the token is forwarded with each event today). Contact **[support@serval.com](mailto:support@serval.com)** to obtain your token before creating the Business Rule. For production, store it in a System Property (for example `x_serval.webhook_token`) rather than hard-coding it in the script.

### Webhook endpoint

| Property           | Value                                                     |
| :----------------- | :-------------------------------------------------------- |
| **URL**            | `https://svwebhook.api.serval.com/servicenow/new-comment` |
| **Method**         | `POST`                                                    |
| **Content-Type**   | `application/json`                                        |
| **Authentication** | Token via `X-Serval-Webhook-Token` header                 |

<Accordion title="Payload reference">
  The Business Rule sends a JSON body with these fields. `record_sys_id`, `comment.sys_id`, and a parseable `instance_url` are required; Serval routes the event to your integration install by the instance domain.

  | Field                    | Type   | Description                                                                                                  |
  | :----------------------- | :----- | :----------------------------------------------------------------------------------------------------------- |
  | `instance_url`           | string | Your ServiceNow instance URL                                                                                 |
  | `table`                  | string | Source table: `incident`, `sn_hr_core_case`, or `sc_req_item`                                                |
  | `record_sys_id`          | string | `sys_id` of the parent record                                                                                |
  | `number`                 | string | Record number, e.g. `INC0012345`, `HRC0001234`, `RITM0056789`                                                |
  | `comment.sys_id`         | string | `sys_id` of the journal entry                                                                                |
  | `comment.element`        | string | `comments` (public reply) or `work_notes` (internal note)                                                    |
  | `comment.value`          | string | The comment body text                                                                                        |
  | `comment.sys_created_by` | string | `user_name` of the commenter (must be a ServiceNow user\_name - Serval resolves it to attribute the comment) |
  | `comment.sys_created_on` | string | ServiceNow timestamp                                                                                         |
</Accordion>

### Create the Business Rule

<Steps>
  <Step title="Navigate to Business Rules">
    In ServiceNow, go to **System Definition → Business Rules** and click **New**.
  </Step>

  <Step title="Configure the rule settings">
    Set the following fields:

    | Field        | Value                              |
    | :----------- | :--------------------------------- |
    | **Name**     | `Serval - Sync Comment to Webhook` |
    | **Table**    | `sys_journal_field`                |
    | **Advanced** | Checked                            |
    | **When**     | `async`                            |
    | **Insert**   | Checked                            |
    | **Update**   | Unchecked                          |
    | **Delete**   | Unchecked                          |
    | **Query**    | Unchecked                          |

    <Frame caption="Business Rule settings on the 'When to run' tab">
      <img src="https://mintcdn.com/serval/kEJkqsoMd8RKyAo5/images/integrations/servicenow/business-rule-settings.png?fit=max&auto=format&n=kEJkqsoMd8RKyAo5&q=85&s=1dba2f4b32dc09ebf34a759f794ede67" alt="ServiceNow Business Rule configuration showing the When to run tab with async timing, Insert checked, and the sys_journal_field table selected" width="1024" height="694" data-path="images/integrations/servicenow/business-rule-settings.png" />
    </Frame>
  </Step>

  <Step title="Add the script">
    Switch to the **Advanced** tab and paste the following script:

    <Frame caption="The Advanced tab with the webhook script">
      <img src="https://mintcdn.com/serval/kEJkqsoMd8RKyAo5/images/integrations/servicenow/business-rule-script.png?fit=max&auto=format&n=kEJkqsoMd8RKyAo5&q=85&s=968a11f2a4a286294c7497021ca2d760" alt="ServiceNow Business Rule Advanced tab showing the webhook script in the script editor" width="1024" height="528" data-path="images/integrations/servicenow/business-rule-script.png" />
    </Frame>

    ```javascript theme={null}
    (function executeRule(current, previous /*null when async*/) {

        var tableName   = current.getValue('name');
        var elementType = current.getValue('element');

        // Validate: only Serval-managed records.
        // Replace 'serval.integration' with the user_name of your
        // Serval service account if it differs.
        var SERVAL_USER = 'serval.integration';

        var recordSysId = current.getValue('element_id');
        var parentRecord = new GlideRecord(tableName);
        if (!parentRecord.get(recordSysId)) {
            gs.warn('Serval webhook: parent record not found: '
                    + tableName + '/' + recordSysId);
            return;
        }

        var openedBy = parentRecord.opened_by.user_name.toString();
        if (openedBy !== SERVAL_USER) {
            return;
        }

        // Build payload
        var instanceName = gs.getProperty('instance_name');
        var instanceUrl  = 'https://' + instanceName + '.service-now.com';

        var payload = {
            instance_url:  instanceUrl,
            table:         tableName,
            record_sys_id: recordSysId,
            number:        parentRecord.getValue('number'),
            comment: {
                sys_id:         current.getUniqueValue(),
                element:        elementType,
                value:          current.getValue('value'),
                sys_created_by: current.getValue('sys_created_by'),
                sys_created_on: current.getValue('sys_created_on')
            }
        };

        // Configuration
        // Replace the token value with the token provided by Serval.
        // For production, store this in a System Property
        // (e.g., x_serval.webhook_token) and retrieve it via
        // gs.getProperty('x_serval.webhook_token').
        var SERVAL_WEBHOOK_URL   = 'https://svwebhook.api.serval.com/servicenow/new-comment';
        var SERVAL_WEBHOOK_TOKEN = '<TOKEN_PROVIDED_BY_SERVAL>';

        try {
            var request = new sn_ws.RESTMessageV2();
            request.setEndpoint(SERVAL_WEBHOOK_URL);
            request.setHttpMethod('POST');
            request.setRequestHeader('Content-Type', 'application/json');
            request.setRequestHeader('X-Serval-Webhook-Token', SERVAL_WEBHOOK_TOKEN);
            request.setRequestBody(JSON.stringify(payload));

            // Run asynchronously so we don't block the user's transaction
            request.setEccParameter('skip_sensor', 'true');

            var response = request.executeAsync();

            gs.info('Serval webhook: sent ' + elementType + ' event for '
                + tableName + '/' + parentRecord.getValue('number')
                + ' (async)');
        } catch (e) {
            gs.error('Serval webhook: failed to send event: ' + e.message);
        }

    })(current, previous);
    ```

    <Warning>
      The script references `serval.integration` as the Serval service account `user_name`. This must match the `user_name` of the integration user you created in [Get your credentials](#get-your-credentials). If you chose a different User ID, update the `SERVAL_USER` variable accordingly.
    </Warning>
  </Step>

  <Step title="Save the Business Rule">
    Click **Submit** to save the rule. Comment sync will begin working immediately for Serval-managed records.
  </Step>
</Steps>

***

## Live agent handoff via VA Bot-to-Bot (optional)

Serval can hand a conversation off to a human ServiceNow agent mid-flow, then bridge the agent's replies back into the Serval ticket so the user keeps chatting in their original surface (Slack, Teams, web, etc.). This rides on ServiceNow's native **Virtual Agent Bot-to-Bot** APIs - there is no Serval-shipped update set and no Scripted REST API to install on your instance. Setup on the ServiceNow side is roughly: install the Virtual Agent API plugin (if not already installed), grant the Serval service account a few additional permissions, create a Token Verification + Message Auth + Provider Application + Outbound REST Message pointing at a Serval-hosted webhook URL, and (optionally) tune the AWA routing for the chat channel you want to use for the handoff.

<Info>
  Live agent sessions appear on Serval tickets as an additional external ticket with `subtype = "interaction"` on the existing ServiceNow integration - they do **not** create a separate integration or channel type. The auto-created `interaction` record on the ServiceNow side is the same record type ServiceNow uses for all VA-mediated chats.
</Info>

### How the bridge works

The bridge has three legs:

1. **Inbound to ServiceNow** - Serval opens, sends messages on, and ends a Bot-to-Bot conversation by calling ServiceNow's Virtual Agent Bot Integration endpoint (installed by the Virtual Agent API plugin). The action discriminator travels in the request body: `START_CONVERSATION`, `AGENT_CHAT`, or `END_CONVERSATION`.
2. **Outbound from ServiceNow** - when the live agent types a reply, ServiceNow's Provider Application looks up its associated Outbound REST Message and POSTs the response to the URL you configured there. That URL is the Serval-hosted webhook.
3. **Agent presence pre-flight (optional)** - Serval's availability check reads the `awa_agent_presence_capacity` table to know whether any agents are currently online before starting a handoff.

Routing (which queue / which agent the conversation lands in) is handled entirely by AWA on the ServiceNow side. The two knobs you have from Serval workflow code are:

* **`channelId`** on the start action - maps to a ServiceNow `sys_cs_channel` record. Defaults to `"chat"`. The channel record's "Bot to Bot Synchronous" flag and its AWA queue assignment together determine where the conversation routes.
* **`contextVariables`** on the start action - a free-form key/value bag passed to ServiceNow on every turn. Customers configure their AWA routing rule against the variable name(s) of their choice (e.g. `u_liveagent_optional_skills`). See [Customize routing with context variables](#step-5-customize-routing-with-context-variables) below.

### Prerequisites

* **Virtual Agent** + **Virtual Agent API** plugins installed on your ServiceNow instance (Quebec or later).
* The Serval ServiceNow integration must already be connected (everything above on this page).
* **Now Assist Pro** licensing is not required - Bot-to-Bot is part of the base Virtual Agent API plugin.

### Limitations (read before you commit)

* **Closing a Serval ticket does not automatically end the ServiceNow conversation.** Your workflow should call the end-session action when the ticket resolves if you want symmetric cleanup. The conversation eventually times out on the ServiceNow side either way.
* **Agent attribution is automatic; assignment details are not.** ServiceNow's Bot-to-Bot payloads carry per-message agent information, and Serval resolves it to the ServiceNow user - so agent replies are attributed correctly on the Serval ticket with no extra work. What the asynchronous START acknowledgment does *not* tell you is which queue the conversation landed in or when an agent picked up; poll the auto-created `interaction` record's `assigned_to` field from a workflow if you need that - see the example workflow below.
* **Inbound auth verification is currently advisory.** The Basic-auth password on inbound webhooks is forwarded onto the event for downstream verification, but the constant-time check against the rotated token is not yet enforced server-side. The per-install webhook URL contains an unguessable UUID, which is the practical access control today. Treat the rotated token as a not-yet-load-bearing secret in the meantime - full server-side verification is on the roadmap.
* **Attachments require trusted-media-domain configuration.** Files do cross the bridge in both directions, but ServiceNow refuses to download attachments from URLs whose host isn't listed under the active Provider Application record's *Trusted media domains* (the `sys_cs_provider_application` record - labeled **Provider Channel Identity** on some releases). You must add Serval's S3 attachment host (the host of the presigned URL Serval sends for attachments - visible on a test egress) to that list, otherwise the live agent will see *"We couldn't download your file using the specified URL"* instead of the image. See the ServiceNow docs: [Set up trusted media domains for secure file upload](https://www.servicenow.com/docs/r/yokohama/conversational-interfaces/virtual-agent/ccif-secure-file-upload.html) for the field and ACL setup.

### What agent replies look like on the Serval ticket

Three kinds of inbound items surface on the bridged ticket; everything else (topic pickers and other Virtual Agent scaffolding) is accepted and dropped:

* **Agent text** surfaces as a comment, attributed to the ServiceNow agent.
* **Agent images and files** surface as attachments. Serval downloads them from ServiceNow's conversational media API using your stored integration credentials, capped at 50 MB per file. If a download fails, Serval posts the placeholder comment `[attachment <name> could not be downloaded - please refresh ServiceNow Agent Workspace]` instead of failing the whole message.
* **System prompts** (such as AWA idle-timeout warnings) surface as comments so the user can reply and keep the chat alive.

When the conversation ends - whether the agent closes it or it times out - Virtual Agent's closing lifecycle text (such as "no agents available" or "the conversation has ended" boilerplate) is suppressed so it doesn't clutter the ticket. A real agent's final message **is** preserved and lands before the channel disconnects; afterwards Serval posts a single "Live agent chat session has ended." note. Duplicate webhook deliveries are deduplicated automatically.

### Step 1: Grant additional permissions to the Serval service account

The standard Serval ACLs cover the Table API surface used by the rest of the integration. Live Agent needs two additional ServiceNow roles plus reads on the AWA tables and the `interaction` table. Add these to your Serval service-account user:

**Roles:**

| Role                  | Why                                                                                                                                                                                                                                     |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `virtual_agent_admin` | Required for ServiceNow's Bot Integration endpoint to accept Serval's Bot-to-Bot calls.                                                                                                                                                 |
| `interaction_agent`   | Lets Serval read the auto-created `interaction` record after `START_CONVERSATION` (for the optional assignment poll) and lets the inbound attachment-download path resolve the agent-supplied conversational-media URLs on image items. |

**Table reads (one ACL per table, `Type: record`, `Operation: read`, `Requires role: x_serval_integration` or whichever role you assigned to the Serval service account):**

| Table                         | Purpose                                                                                                              |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `awa_agent_presence_capacity` | Used by the availability check. Without this, the check will report no agents available for everyone.                |
| `awa_agent_presence`          | Used by AWA routing - `presence_capacity` is a view that joins this in.                                              |
| `awa_agent_capacity`          | Same; joined by the `presence_capacity` view.                                                                        |
| `interaction`                 | Read access for the optional `assigned_to` poll. Already covered if you're using the `interaction_agent` role above. |

### Step 2: Generate the shared bot token in Serval

In your Serval workspace, navigate to **App Instances → ServiceNow → Ticket Sync settings**. Scroll to the **Live Agent (VA Bot-to-Bot)** section and click **Generate token**.

You'll see three values you'll need in the next step:

| Value                       | What it is                                                                                                                                                                                   |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Inbound Webhook URL**     | Serval-hosted URL the ServiceNow Outbound REST Message will POST agent replies to. Per-install - contains an unguessable UUID - so do not share between environments.                        |
| **Authentication Username** | `serval` by convention. Any username is accepted - the password (the token below) is the authenticator, and it must be non-empty.                                                            |
| **Authentication Token**    | One-time-displayed opaque secret - **copy it now**, you cannot view it again. Click **Rotate token** later if you ever need a new one (rotation invalidates the previous value immediately). |

<Info>
  **One token, two destinations on the ServiceNow side.** This token value lives in two places on ServiceNow:

  * The **Token Verification** record. Serval injects this value into the body of every Bot-to-Bot call; ServiceNow's Message Auth middleware verifies it before processing.
  * The **Outbound REST Message** Basic-auth password. ServiceNow presents this value when posting agent replies to the Inbound Webhook URL above.

  You'll wire both up in the next step.
</Info>

### Step 3: Configure the four ServiceNow records that wire up Bot-to-Bot

ServiceNow's Bot-to-Bot setup links four records together. The record names below are the OOB tables - the navigation labels may vary slightly by release.

<Steps>
  <Step title="Enable inbound auth on the VA Bot Integration Scripted REST API">
    Navigate to **System Web Services → Scripted Web Services → Scripted REST APIs** and open the **VA Bot Integration** record. Open the **Bot Integration** resource. On the **Security** tab, set **Requires authentication = true** and **Requires ACL authorization = false**. Save.

    This makes the bot integration endpoint reject anonymous calls - the Token Verification record you'll create next is what gates it.
  </Step>

  <Step title="Create the Token Verification record">
    In the filter navigator, type `token_verification.list` and press Enter. Click **New**. Fill in:

    | Field     | Value                                              |
    | --------- | -------------------------------------------------- |
    | **Name**  | `Serval` (or any human-readable name)              |
    | **Token** | The Authentication Token from Serval (from Step 2) |

    Save. This is the value ServiceNow will validate on every inbound call from Serval.
  </Step>

  <Step title="Create the Message Auth record">
    In the filter navigator, type `message_auth.list` and press Enter. Click **New**. Fill in:

    | Field                            | Value                                                                |
    | -------------------------------- | -------------------------------------------------------------------- |
    | **Name**                         | `Serval Bot Auth`                                                    |
    | **Provider**                     | `Serval` (free-text; only used for organization)                     |
    | **Inbound Message Verification** | Pick the Token Verification record from the previous step (`Serval`) |
    | **Outbound Message Creation**    | Pick the same Token Verification record                              |

    Save.
  </Step>

  <Step title="Create the Provider Application">
    In the filter navigator, type `sys_cs_provider_application.list` and press Enter. Click **New**. Fill in:

    | Field            | Value                                                                                                                                         |
    | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
    | **Name**         | `Serval Bot`                                                                                                                                  |
    | **Inbound ID**   | `serval` (or any unique slug - this is the `appInboundId` value, only needed in multi-bot instances; single-bot instances can pick any value) |
    | **Message Auth** | Pick the Message Auth record from the previous step (`Serval Bot Auth`)                                                                       |
    | **Provider**     | `VA Bot to Bot Provider`                                                                                                                      |

    Save. The name of this record matters for the next step. This is also the record that carries the **Trusted media domains** list used for attachment egress (see Limitations above).
  </Step>

  <Step title="Create the Outbound REST Message">
    In the filter navigator, type `REST Message` and open the table. Click **New**. Fill in:

    | Field                         | Value                                                                                                                                                                                      |
    | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | **Name**                      | **Must exactly match the Provider Application name from the previous step** (`Serval Bot`). ServiceNow finds the outbound endpoint by name lookup against the active Provider Application. |
    | **Endpoint**                  | The Inbound Webhook URL from Serval (Step 2).                                                                                                                                              |
    | **Authentication type**       | `Basic`                                                                                                                                                                                    |
    | **Use mutual authentication** | unchecked                                                                                                                                                                                  |
    | **Basic auth profile**        | Create a new Basic Auth profile (or pick an existing one) with `Username: serval` and the Authentication Token from Step 2 as the password.                                                |

    Save. ServiceNow will now use this REST Message - looked up by name - whenever the Provider Application emits an agent reply.

    By convention, also create a single HTTP Method record on the REST Message named `post` with:

    | Field            | Value                            |
    | ---------------- | -------------------------------- |
    | **HTTP method**  | `POST`                           |
    | **Endpoint**     | Same as the parent REST Message  |
    | **HTTP Headers** | `Content-Type: application/json` |

    Some ServiceNow releases auto-create this from the parent record; if your release does, you can skip the explicit creation.
  </Step>

  <Step title="Verify the chat channel is Bot-to-Bot enabled">
    In the filter navigator, type `sys_cs_channel.list` and press Enter. Find the channel you intend to route through (the OOB one is named `chat` - its sys\_id is what you'll pass as `channelId` from your workflows; the literal string `"chat"` also works on most releases).

    On the channel record, confirm **Bot to Bot Synchronous = true**. If it's not, set it and save. Without this flag, the bot integration endpoint will reject `START_CONVERSATION` calls with a routing error.
  </Step>
</Steps>

<Note>
  If you ever click **Rotate token** in Serval, you must update **both** ServiceNow records:

  1. The Token Verification record's `Token` field.
  2. The Basic Auth profile used by the Outbound REST Message (or, if you embedded the password directly on the REST Message, update it there).

  Serval-to-ServiceNow calls (starting sessions, sending messages, ending conversations) fail until the Token Verification record matches the new token - ServiceNow checks it on every call. Inbound agent messages keep flowing in the meantime because Serval's inbound verification is currently advisory (see [Limitations](#limitations-read-before-you-commit) above), but update the Basic Auth profile anyway so the inbound direction does not break once server-side verification is enforced.
</Note>

### Step 4: SDK building blocks

The ServiceNow SDK exposes five thin building-block actions for live agent. They are intentionally **not orchestration helpers** - live-agent flows vary enough between customer ServiceNow configurations (different chat experiences, custom AWA routing rules, renamed topics, varying trigger phrases for NLU) that workflow authors compose these directly rather than configure a single high-level wrapper:

* **`checkLiveAgentAvailability`** - pre-flight: are any human agents online right now? Reads the agent-presence capacity table and returns whether agents are available plus a queue-depth figure (the sum of agents' remaining capacity). Skill-level filtering is intentionally not exposed - routing belongs to AWA.
* **`startLiveAgentSession`** - opens a conversation. The caller supplies the Serval `ticketId`, the user (a ServiceNow `sys_user` sys\_id is preferred, plus email), the verbatim `message` text (including any NLU trigger phrase), `channelId` (defaults to `"chat"`), `contextVariables` for routing (re-sent on every turn), and optionally `appInboundId` for multi-bot instances. `connectTicket` defaults to true, which wires the ServiceNow conversation onto the Serval ticket as an `interaction`-subtype external ticket so user replies auto-egress as `AGENT_CHAT` turns. It returns the session identifiers - including `clientSessionId` and a `liveAgentData` blob that the attach action below consumes.
* **`sendLiveAgentMessage`** - manually sends an `AGENT_CHAT` turn. Usually **not** needed: once the ticket is connected, user replies auto-egress through the standard pipeline. Use it for programmatic messages (auto-replies, scripted status updates, testing).
* **`endLiveAgentSession`** - force-ends a session (e.g. on ticket resolve, user abandon, or SLA timeout).
* **`attachInteractionSysIdToLiveAgentSession`** - stashes a polled `interaction` sys\_id onto the Serval external ticket so the ticket-channels surface in the Serval UI renders a deep link to the chat in ServiceNow Agent Workspace.

### Step 5: Customize routing with context variables

`contextVariables` is the principal knob for AWA routing. The variable names you pass are **customer-specific** - they must match the conditions on whichever AWA routing rule you've configured on your ServiceNow instance.

For example, an "IT Live Agent Chat" queue whose routing rule has the condition `context.u_liveagent_optional_skills = IT` is targeted by passing a `u_liveagent_optional_skills` context variable with the value `IT`. Other instances might use `u_skill_required`, `routing_skill`, or any other custom variable name. To find the right variable name on your instance, check your AWA routing rule's condition script, or ask your ServiceNow admin.

Custom interaction-record fields (e.g. CMDB tag, custom assignment-group overrides, region routing) can also be projected onto the auto-created `interaction` record via a ServiceNow-side Business Rule that reads the `contextVariables` payload. This is a pure ServiceNow-side customization - no Serval changes required.

### Step 6: Example workflow - START\_CONVERSATION with routing and assignment poll

This mirrors the canonical pattern verified end-to-end against a real ServiceNow instance. You can drop this into a Serval workflow as a starting point and adapt the routing skills, trigger phrase, and message composition to your environment.

```ts theme={null}
import { workflow, sleep } from "serval/core";
import * as servicenow from "serval/integrations/servicenow";

export const main = workflow({
  fn: async function (
    args: {
      email: string;       // The end-user's email
      userMessage: string; // The user's natural-language message
      ticketId: string;    // The Serval ticket to bridge onto
    },
    ctx: servicenow.context.ServiceNowIntegration,
  ) {
    // 1) Resolve the ServiceNow sys_user.sys_id for this user. The
    //    Bot-to-Bot endpoint accepts email/username but account-linking
    //    works best with a sys_id, and AWA assignment is more reliable.
    const userSysId = await servicenow.lookupUserSysIdByEmail(
      { email: args.email },
      ctx,
    );

    // 2) Compose the message text. The trailing trigger phrase is what
    //    ServiceNow's NLU classifies as the "Live Agent Support" topic -
    //    the OOB topic that drives the AWA handoff without going through
    //    the Greetings picker. Customers who have renamed that topic
    //    or have a custom topic should adapt the phrase to match.
    const ROUTING_TRIGGER_PHRASE =
      "I need a live agent. Please connect me with Live Agent Support - I want to talk to an agent.";
    const message =
      args.userMessage && args.userMessage.length > 0
        ? args.userMessage + "\n\n" + ROUTING_TRIGGER_PHRASE
        : ROUTING_TRIGGER_PHRASE;

    // 3) Open the conversation. `connectTicket: true` (default) wires
    //    the ServiceNow clientSessionId onto the Serval ticket as an
    //    interaction-subtype external ticket, so subsequent user
    //    replies on the Serval ticket auto-egress as AGENT_CHAT turns.
    const { clientSessionId, liveAgentData } =
      await servicenow.startLiveAgentSession(
        {
          ticketId: args.ticketId,
          user: { userId: userSysId, email: args.email },
          message,
          channelId: "chat",
          contextVariables: { u_liveagent_optional_skills: "IT" },
        },
        ctx,
      );

    // 4) Optional: poll the `interaction` table to learn which agent
    //    actually picked up the conversation. START returns only an
    //    async ack - the real assignment happens a moment later when
    //    AWA matches an available agent. Stop early once `assigned_to`
    //    is populated.
    let routingInteraction: Record<string, unknown> | null = null;
    for (let attempt = 0; attempt < 4; attempt++) {
      await sleep({ durationMs: 3000 });
      const { result } = await servicenow.tableApiRequest(
        {
          method: "GET",
          path: "/api/now/table/{tableName}",
          pathParams: { tableName: "interaction" },
          query: {
            sysparm_query:
              "opened_for=" + userSysId + "^type=chat^ORDERBYDESCsys_created_on",
            sysparm_fields:
              "sys_id,number,state,assigned_to,assignment_group,queue,opened_at",
            sysparm_display_value: "true",
            sysparm_exclude_reference_link: "true",
            sysparm_limit: "1",
          },
        },
        ctx,
      ) as { result: Array<Record<string, unknown>> };

      if (result.length > 0) {
        routingInteraction = result[0];
        if (routingInteraction["assigned_to"]) {
          break; // Agent picked up - done polling.
        }
      }
    }

    // 5) Optional: stash the polled `interaction.sys_id` onto the
    //    Serval external ticket so the ticket-channels surface in the
    //    Serval UI renders a canonical Agent Workspace deep link to
    //    the chat. Skip this if you don't run the post-START poll -
    //    the channel still surfaces, just without a clickable link.
    if (routingInteraction && typeof routingInteraction["sys_id"] === "string") {
      await servicenow.attachInteractionSysIdToLiveAgentSession(
        {
          ticketId: args.ticketId,
          clientSessionId,
          liveAgentData,
          interactionSysId: routingInteraction["sys_id"],
        },
        ctx,
      );
    }

    return {
      clientSessionId,
      assignedAgent: routingInteraction?.["assigned_to"] ?? null,
      assignmentGroup: routingInteraction?.["assignment_group"] ?? null,
    };
  },
});
```

While the session is active:

* **User → agent**: any new message the user sends in their original surface fans out to the ServiceNow agent automatically as an `AGENT_CHAT` turn - no extra workflow code required; Serval's ticket-channel egress handles it.
* **Agent → user**: ServiceNow's Outbound REST Message posts the agent's typed reply to the Inbound Webhook URL configured in Step 3. Serval ingresses it as a comment on the bridged ticket (attributed to the agent), which surfaces back in the user's original surface.

### Step 7: Optionally end the session symmetrically

When the Serval ticket auto-resolves or the user abandons, call `endLiveAgentSession` with the `clientSessionId` saved from the START workflow output, the same user, a short `reason` such as `ticket_resolved`, and the same `channelId` (and `contextVariables`, if your routing rule expects them on every turn).

This is best-effort - the ServiceNow side eventually times the conversation out either way, but an explicit `END_CONVERSATION` cleans up the `interaction` record faster and gives the agent a clean "user ended chat" signal.

### Step 8: Real-time translation (optional)

Live agents and end users frequently speak different languages - a Spanish-speaking employee opens a chat in Slack, the routed IT queue is staffed by English-speaking agents, and you want both sides to read their own language without the agent having to copy/paste into a translation tool.

To enable **bidirectional real-time translation** for the lifetime of a conversation, pass two extra fields on `startLiveAgentSession`: `userLanguage` (a BCP-47 code for the requester, e.g. `es`) and `agentLanguage` (the code your live agents read and write, e.g. `en`).

What changes after that:

* **User → agent.** Every message the user types on the upstream surface is translated from `userLanguage` into `agentLanguage` before it lands as an `AGENT_CHAT` turn on ServiceNow.
* **Agent → user.** Every reply the live agent posts is translated from `agentLanguage` into `userLanguage` before it's persisted on the Serval ticket and fanned back out to the user's surface.

Behavior worth knowing:

* Both fields must be set. If either is omitted, or both share a primary subtag (e.g. `en` vs `en-US`), the conversation runs through verbatim - no translation is attempted.
* URLs, code blocks, ticket numbers, and @mentions are preserved verbatim across the translation.
* Translation failures fall back to the original text and log a warning. A passthrough turn is strictly better than a dropped turn on a live chat - the agent or user can ask the other side to rephrase.
* Translation is handled by Serval automatically - there is nothing to configure on the ServiceNow side.

Detecting the requester's language automatically (e.g. from the user's Serval profile locale, or from the first inbound message) is up to the workflow author - pass whatever you've resolved into `userLanguage`. If `userLanguage` is unknown, no translation is attempted in either direction.

***

Need help? Contact **[support@serval.com](mailto:support@serval.com)** for assistance with your ServiceNow integration - including custom routing-rule design, multi-bot Provider Application setups, interaction-field Business Rules, and real-time translation rollouts.
