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

# ServiceNow

> The Table API, Attachment API, and Aggregate API.

Stateful emulation of a ServiceNow instance's REST APIs. The Table API serves `incident`, `change_request`, `problem`, `task`, users, groups, roles, locations, companies, and custom `u_` tables from seed config, with encoded queries (`sysparm_query`), dot-walked fields, display values, and `Link` and `X-Total-Count` paging headers. Incidents, changes, and problems follow ServiceNow's choice values, numbering, priority lookup, and state rules. Basic auth with seeded users, roles checked by ACL-like rules, and OAuth at `/oauth_token.do` work as on an instance, and the Attachment and Aggregate APIs cover the basics. ServiceNow has no widely used official Node SDK; the tests drive the emulator with raw HTTP through a thin typed client, as integrations do with `fetch` or axios.

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/servicenow` |
| Port | `4000` when started alone; named services take consecutive ports |
| Config key | `servicenow` |
| HTTP operations | 32, browsable at `http://localhost:3999/docs/servicenow` while it runs ([API reference](/api-reference.md)) |

```bash CLI theme={null}
npx @deploybaseline/cli start --service servicenow
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const servicenow = await createEmulator({ service: "servicenow" });
// servicenow.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "servicenow": {
    "instance": "dev12345",
    "time_zone": "UTC",
    "users": [
      {
        "user_name": "beth.anglin",
        "password": "Password1!",
        "first_name": "Beth",
        "last_name": "Anglin",
        "email": "beth.anglin@example.com",
        "roles": ["itil"],
        "groups": ["Service Desk"]
      },
      {
        "user_name": "abel.tuter",
        "password": "Password1!",
        "first_name": "Abel",
        "last_name": "Tuter",
        "email": "abel.tuter@example.com"
      }
    ],
    "groups": [
      {
        "name": "Service Desk",
        "roles": ["itil"]
      }
    ],
    "oauth": {
      "clients": [
        {
          "client_id": "baseline-client",
          "client_secret": "baseline-secret",
          "name": "My integration"
        }
      ]
    },
    "records": {
      "incident": [
        {
          "short_description": "Email is not syncing on my laptop",
          "caller_id": "abel.tuter",
          "assignment_group": "Service Desk",
          "impact": 2,
          "urgency": 2
        }
      ]
    }
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service servicenow
```

ServiceNow starts only when named in config or with `--service`. When it is the only enabled service, it listens on `http://localhost:4000`.

Without seed config, the instance has the user `admin` with password `admin` and the base roles `admin`, `itil`, `itil_admin`, `user_admin`, `snc_read_only`, and `snc_internal`.

Point your client's instance URL at the emulator:

```ts theme={null}
const instance = process.env.SERVICENOW_EMULATOR_URL ?? "http://localhost:4000";
const auth = `Basic ${Buffer.from("admin:admin").toString("base64")}`;

const created = await fetch(`${instance}/api/now/table/incident`, {
  method: "POST",
  headers: { Authorization: auth, "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({ short_description: "Email is down", caller_id: "abel.tuter", impact: 1, urgency: 2 }),
});
const { result } = await created.json(); // result.number is INC0010001, result.priority is "2"

const list = await fetch(
  `${instance}/api/now/table/incident?sysparm_query=active=true^priority<=2^ORDERBYDESCsys_created_on&sysparm_limit=10`,
  { headers: { Authorization: auth, Accept: "application/json" } },
);
```

## URL mapping

| Real ServiceNow URL | Emulator URL |
| - | - |
| `https://<instance>.service-now.com/api/now/...` | `$SERVICENOW_EMULATOR_URL/api/now/...` |
| `https://<instance>.service-now.com/oauth_token.do` | `$SERVICENOW_EMULATOR_URL/oauth_token.do` |
| `https://<instance>.service-now.com/oauth_auth.do` | `$SERVICENOW_EMULATOR_URL/oauth_auth.do` |

## Auth

* **Basic**: a seeded user's `user_name` and `password`. Every instance has `admin` with password `admin` (sys\_id `6816f79cc0a8016401c5a33be04be441`, role `admin`). User names match without regard to case. Inactive and locked out users cannot sign in.
* **Bearer**: an OAuth access token from `/oauth_token.do`, or a token the CLI accepts (for example `test_token_admin`), which acts as the user whose `user_name` is the token's login (`admin` by default).

Without credentials the answer is 401 `{ "error": { "message": "User Not Authenticated", "detail": "Required to provide Auth information" }, "status": "failure" }` with `WWW-Authenticate: Basic realm="Service-now"`. Wrong credentials and unknown, revoked, or expired tokens answer 401 with `User is not authenticated`.

### OAuth

`POST /oauth_token.do` takes a form body (or JSON) with `client_id` and `client_secret` (or HTTP Basic) from `oauth.clients`:

* `grant_type=password` with `username` and `password`
* `grant_type=refresh_token` with `refresh_token`; the answer carries a new access token and the same refresh token
* `grant_type=authorization_code` with `code`, `redirect_uri`, and `code_verifier` when the authorize step sent a PKCE challenge (`S256` or `plain`)
* `grant_type=client_credentials` when `oauth.client_credentials` is true and the client names a `client_credentials_user` (no refresh token)

Answers look like `{ "access_token": "...", "refresh_token": "...", "scope": "useraccount", "token_type": "Bearer", "expires_in": 1799 }`. Access tokens last 1800 seconds and refresh tokens 100 days unless the client sets `access_token_lifespan` and `refresh_token_lifespan`. A bad client, bad credentials, or a used or expired code answer 401 `{ "error_description": "access_denied", "error": "server_error" }`, as an instance does; an unknown grant answers 400 `unsupported_grant_type`.

`GET /oauth_auth.do?response_type=code&client_id=...&redirect_uri=...&state=...` shows a sign-in page listing active users. Choosing one redirects to the redirect URL with `code` and `state`; declining sends `error=access_denied`. `POST /oauth_revoke_token.do` with the client credentials and `token` revokes it (a refresh token takes the user's access tokens with it).

## Table API

Every route is served at `/api/now/table/{table}`, `/api/now/v1/table/{table}`, and `/api/now/v2/table/{table}`.

| Method | Path | Answer |
| - | - | - |
| `GET` | `/api/now/table/{table}` | 200 `{ "result": [...] }` with `X-Total-Count` and `Link` |
| `GET` | `.../{table}/{sys_id}` | 200 `{ "result": {...} }` |
| `POST` | `/api/now/table/{table}` | 201 `{ "result": {...} }` with `Location` |
| `PUT` | `.../{table}/{sys_id}` | 200 `{ "result": {...} }` |
| `PATCH` | `.../{table}/{sys_id}` | 200 `{ "result": {...} }` |
| `DELETE` | `.../{table}/{sys_id}` | 204, no body |

Errors use ServiceNow's body `{ "error": { "message", "detail" }, "status": "failure" }`:

| Case | Status | `message` | `detail` |
| - | - | - | - |
| Unknown table | 400 | `Invalid table <name>` | `null` |
| Unknown sys\_id, or ACL hides the row | 404 | `No Record found` | `Record doesn't exist or ACL restricts the record retrieval` |
| ACL denies a write | 403 | `Operation Failed` | `ACL Exception Insert/Update/Delete Failed due to security constraints` |
| Mandatory field missing | 403 | `Operation Failed` | `Data Policy Exception: \n The following fields are mandatory: Resolution code, Resolution notes` |
| Business rule refusal | 403 | `Operation Failed` | A sentence naming the rule |
| Body is not JSON | 400 | `Exception while reading request` | `The payload is not valid JSON.` |
| Unknown path under `/api/now` | 400 | `Requested URI does not represent any resource` | `null` |

Values are strings. Unknown fields in a body are ignored, read only fields (`sys_*`, `number`, computed fields) are ignored except `sys_id` and `number` on insert, booleans and numbers become strings, and a reference accepts a sys\_id or a display value (user names too). Without `sysparm_input_display_value`, a choice label is stored as given, as GlideRecord does. Saving without a change keeps `sys_mod_count`.

### Query parameters

| Parameter | Effect |
| - | - |
| `sysparm_query` | Encoded query (below) |
| `sysparm_fields` | Comma separated fields, dot-walked ones too (`caller_id.email`); the result keys are the names asked for |
| `sysparm_limit`, `sysparm_offset` | Paging; the default limit is 10000 |
| `sysparm_display_value` | `false` (stored values), `true` (display values), `all` (`{ display_value, value }`) |
| `sysparm_exclude_reference_link` | References as the bare value instead of `{ link, value }` |
| `sysparm_suppress_pagination_header` | No `Link` header |
| `sysparm_no_count` | No `X-Total-Count` header (the `Link` header then has no `last`) |
| `sysparm_input_display_value` | Writes take choice labels, reference display values, and date/times in the user's time zone |

References read as `{ "link": "<url>/api/now/table/sys_user/<sys_id>", "value": "<sys_id>" }`, as `{ "display_value", "link" }` with display values, and as `{ "display_value", "link", "value" }` with `all`. An empty reference is `""`. Date/times are stored in UTC as `yyyy-MM-dd HH:mm:ss` and shown in the user's `time_zone` (or the instance's `time_zone`). Journal fields (`comments`, `work_notes`) read as `""`; writing them adds a `sys_journal_field` entry, and their display value is the history, newest first.

`Link` holds `first`, `prev` (after the first page), `next` (when more rows follow), and `last`, keeping the other parameters. `X-Total-Count` counts matching rows before row ACLs, so a user who may not read every row can get a short page, as on an instance.

### Encoded queries

| Syntax | Meaning |
| - | - |
| `a^b` | a AND b |
| `a^ORb` | a OR b; binds tighter than `^`, so `a^b^ORc` is a AND (b OR c) |
| `a^NQb` | Two queries OR'd |
| `ORDERBYfield`, `ORDERBYDESCfield` | Sort, in order; references sort by display value |
| `^^` | A literal caret in a value |
| `^EQ` | End of conditions (ignored) |

Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `NOT LIKE` (or `NOTLIKE`), `STARTSWITH`, `ENDSWITH`, `IN`, `NOT IN`, `ISEMPTY`, `ISNOTEMPTY`, `ANYTHING`, `EMPTYSTRING`, `BETWEEN` (`low@high`), `ON` and `NOTON` (`Today@javascript:gs.beginningOfToday()@javascript:gs.endOfToday()`, a label such as `Last week`, or a date), `RELATIVEGT`, `RELATIVEGE`, `RELATIVELT`, `RELATIVELE`, `RELATIVEEE` (`@day@ago@3`), `SAMEAS`, `NSAMEAS`, `DYNAMIC` (the "Me" filter `90d1921e5f510100a9ad2572f2b477fe`), and `INSTANCEOF` on `sys_class_name`.

* Integers and choice fields such as `state` and `priority` compare as numbers, date/times as instants, booleans as `true`/`false`, and strings without regard to case.
* References match sys\_ids with `=` and `IN`, and their display values with `LIKE`, `STARTSWITH`, `ENDSWITH`, and with `=` when the value is not a sys\_id. Dot-walk through them: `caller_id.company.name=ACME`.
* `!=`, `NOT IN`, `NOT LIKE`, and `NOTON` never match empty values (add `^ORfieldISEMPTY`). `field=` matches empty values.
* `javascript:` values recognize the common GlideSystem helpers, worked out in the user's time zone: `gs.getUserID()`, `gs.getUserName()`, `gs.now()`, `gs.nowDateTime()`, `gs.beginningOf...()` and `gs.endOf...()` for Today, Yesterday, Tomorrow, This, Last, and Next Week, Month, Quarter, and Year, and Last 15, 30, or 45 Minutes and Last Hour or 2 Hours, `gs.daysAgo(n)`, `gs.daysAgoStart(n)`, `gs.daysAgoEnd(n)` and the same for minutes, hours, months, quarters, and years, and `gs.dateGenerate('2026-01-01','start')`. Weeks start on Monday. Any other script matches no rows.
* A condition on an unknown field or a term that is not a condition is dropped, as on an instance. With `invalid_query_returns_no_rows: true` (the `glide.invalid_query.returns_no_rows` property) such a query returns no rows.

### Tables

| Table | Number prefix | Notes |
| - | - | - |
| `task` | `TASK0010001` | The parent table: lists incidents, changes, problems, and custom task tables, with task fields |
| `incident` | `INC0010001` | `state` 1 New, 2 In Progress, 3 On Hold, 6 Resolved, 7 Closed, 8 Canceled |
| `change_request` | `CHG0030001` | `state` -5 New, -4 Assess, -3 Authorize, -2 Scheduled, -1 Implement, 0 Review, 3 Closed, 4 Canceled |
| `problem` | `PRB0040001` | `state` 101 New, 102 Assess, 103 Root Cause Analysis, 104 Fix in Progress, 106 Resolved, 107 Closed |
| `sys_user` | | `name` follows `first_name` and `last_name`; `user_name` is unique; `user_password` is write only |
| `sys_user_group` | | |
| `sys_user_role`, `sys_user_has_role`, `sys_group_has_role`, `sys_user_role_contains`, `sys_user_grmember` | | Roles, grants, and memberships |
| `cmn_location` | | `full_name` follows the parent chain |
| `core_company` | | |
| `sys_journal_field` | | Comments and work notes |
| `sys_attachment` | | Attachment metadata |
| `sys_db_object`, `sys_dictionary`, `sys_choice` | | Read only metadata built from the schema, custom tables included |

Every row has `sys_id` (32 hex characters), `sys_created_on`, `sys_created_by`, `sys_updated_on`, `sys_updated_by`, `sys_mod_count`, and `sys_tags`. Impact and urgency are 1 High, 2 Medium, 3 Low; priority is 1 Critical to 5 Planning.

### Business rules

* **Incident**: `priority` follows impact and urgency through the default priority lookup and cannot be set directly. Resolving (6) or closing (7) needs `close_code` and `close_notes` (`Resolution code` and `Resolution notes`), then stamps `resolved_at` and `resolved_by`; closing also stamps `closed_at` and `closed_by`. `active` is false when Closed or Canceled. Leaving Resolved counts `reopen_count` and clears the resolution stamps. Leaving On Hold clears `hold_reason`. Changing the assignment group counts `reassignment_count`. `incident_state` follows `state`.
* **Change request**: types `normal` (default), `standard`, and `emergency` follow ServiceNow's state models: normal New, Assess, Authorize, Scheduled, Implement, Review, Closed; emergency skips Assess; standard goes New, Scheduled, Implement, Review, Closed. Assess, Authorize, and Scheduled may go back to New, and any open state may be Canceled. Other moves answer 403 `Invalid state transition ...`. Scheduling a normal or emergency change needs `approval` `approved`. Closing needs `close_code` (`successful`, `successful_issues`, `unsuccessful`) and `close_notes`. A planned end before the planned start is refused.
* **Problem**: `priority` follows impact and urgency. Resolving (106) or closing (107) needs `resolution_code` (`fix_applied`, `risk_accepted`, `canceled`, `duplicate`). Closed problems are inactive.

### Access

* `admin` passes every check. `snc_read_only` denies every write, even for admin.
* Task tables: `itil` reads and writes; users without it read incidents they are the caller of or opened, and create incidents. Closed and canceled incidents, closed problems, and closed or canceled changes are read only except for admin. Only admin deletes task rows.
* Users, groups, roles, and memberships: everyone reads; `user_admin` writes. Locations and companies: everyone reads; admin writes.
* Journal entries and attachments are readable with the record they belong to.
* Custom tables: `read_roles` (default: any signed in user) and `write_roles` (default `itil`).
* Roles come from `sys_user_has_role`, from groups through `sys_group_has_role`, and from roles they contain (`itil_admin` contains `itil`).

A hidden row answers 404 on read; a denied write answers 403 `ACL Exception ... Failed due to security constraints`.

## Attachment API

* `POST /api/now/attachment/file?table_name=incident&table_sys_id=<sys_id>&file_name=log.txt` with the raw bytes and their `Content-Type`
* `POST /api/now/attachment/upload` with multipart form fields `table_name`, `table_sys_id`, and a file part
* `GET /api/now/attachment` with `sysparm_query`, `sysparm_limit`, and `sysparm_offset`
* `GET /api/now/attachment/{sys_id}` for metadata (`file_name`, `content_type`, `size_bytes`, `hash`, `table_name`, `table_sys_id`, `download_link`, ...)
* `GET /api/now/attachment/{sys_id}/file` for the bytes, with `X-Attachment-Metadata`
* `DELETE /api/now/attachment/{sys_id}`

Attachments go with their record when it is deleted.

## Aggregate API

`GET /api/now/stats/{table}` (also `/api/now/v1/stats/{table}`) takes `sysparm_query`, `sysparm_count=true`, `sysparm_avg_fields`, `sysparm_sum_fields`, `sysparm_min_fields`, `sysparm_max_fields`, `sysparm_group_by` (or `GROUPBYfield` in the query), `sysparm_order_by` (`COUNT` or `COUNT^DESC`), and `sysparm_display_value`. Without grouping it answers `{ "result": { "stats": { "count": "12", "avg": { ... } } } }`; with grouping, `{ "result": [{ "stats": { "count": "3" }, "groupby_fields": [{ "field": "priority", "value": "1", "display_value": "1 - Critical" }] }] }`.

## Seed config

```json theme={null}
{
  "servicenow": {
    "instance": "dev12345",
    "time_zone": "America/Los_Angeles",
    "invalid_query_returns_no_rows": false,
    "roles": [
      {
        "name": "x_custom_role",
        "description": "Custom role",
        "contains": ["itil"]
      }
    ],
    "companies": [
      {
        "name": "ACME North America",
        "city": "San Diego"
      }
    ],
    "locations": [
      {
        "name": "San Diego",
        "city": "San Diego",
        "company": "ACME North America"
      }
    ],
    "users": [
      {
        "user_name": "beth.anglin",
        "password": "Password1!",
        "first_name": "Beth",
        "last_name": "Anglin",
        "email": "beth.anglin@example.com",
        "roles": ["itil"],
        "groups": ["Service Desk"],
        "company": "ACME North America"
      },
      {
        "user_name": "abel.tuter",
        "password": "Password1!",
        "first_name": "Abel",
        "last_name": "Tuter",
        "manager": "beth.anglin"
      }
    ],
    "groups": [
      {
        "name": "Service Desk",
        "roles": ["itil"],
        "manager": "beth.anglin"
      }
    ],
    "oauth": {
      "client_credentials": true,
      "clients": [
        {
          "client_id": "baseline-client",
          "client_secret": "baseline-secret",
          "name": "My integration",
          "redirect_uris": ["http://localhost:3000/servicenow/callback"],
          "client_credentials_user": "beth.anglin"
        }
      ]
    },
    "tables": [
      {
        "name": "u_asset_request",
        "label": "Asset Request",
        "number_prefix": "AREQ",
        "display": "u_item",
        "fields": [
          {
            "name": "u_item",
            "label": "Item",
            "mandatory": true
          },
          {
            "name": "u_quantity",
            "type": "integer",
            "default": "1"
          },
          {
            "name": "u_requested_for",
            "type": "reference",
            "reference": "sys_user"
          },
          {
            "name": "u_status",
            "choices": ["draft", "approved"],
            "default": "draft"
          }
        ],
        "write_roles": ["itil"]
      }
    ],
    "fields": {
      "incident": [
        {
          "name": "u_vendor_ticket",
          "label": "Vendor ticket"
        }
      ]
    },
    "records": {
      "incident": [
        {
          "short_description": "Email is down",
          "caller_id": "abel.tuter",
          "assignment_group": "Service Desk",
          "impact": 1,
          "urgency": 2
        }
      ],
      "u_asset_request": [
        {
          "u_item": "Laptop",
          "u_requested_for": "abel.tuter"
        }
      ]
    }
  }
}
```

* `time_zone`: users without their own time\_zone see date/times here

* Custom tables and fields must start with `u_`. Tables may `extend` another table (`task` puts their rows in the task table and gives them task fields and rules). Field `type` is one of `string` (default), `integer`, `decimal`, `boolean`, `glide_date_time`, `glide_date`, `reference` (with `reference`), `email`, `url`, `phone_number`, `glide_list`, or `journal_input`.

* `records` values are read as display values: choice labels, user names, and names of referenced rows work, and date/times are in the instance's time zone. Business rules run, so a resolved incident needs its resolution fields.

* Seeded rows get stable sys\_ids, so seeding again on saved state adds nothing.

* Without config the instance has `admin` / `admin` and the base roles `admin`, `itil`, `itil_admin`, `user_admin`, `snc_read_only`, and `snc_internal`.

## Inspector

`GET /_inspector` shows incidents, changes, problems, users with their effective roles, groups, tables with row counts, attachments, and OAuth clients.

## Limits

Outbound webhooks and business rule scripts (ServiceNow has no standard signed webhook), the Import Set, CMDB, Service Catalog, and Scripted REST APIs, approvals (`sysapproval_approver`), SLAs, workflows and flows, UI policies, domain separation, XML responses, and `sysparm_view`. ACLs are the model above rather than configurable rules.

## Conformance

| | |
| - | - |
| Routes | 37 supported or partial, 0 planned |
| Route gate | fail |
| Spec gate | no public spec |
| SDKs tested | HTTP only |
| Dual mode | not yet |

See [CONFORMANCE.md](https://github.com/deploybaseline/cli/blob/main/CONFORMANCE.md) for every provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.