ServiceNow¶
ServiceNow is an IT service management (ITSM) and security operations platform used for ticketing, change/problem management, CMDB asset tracking, and security incident response.
The ServiceNow LimaCharlie Extension is primarily an API bridge: it lets LimaCharlie-side automation (D&R rules, AI agents) drive a ServiceNow instance as it sees fit — create/read/update/delete records on any table, append journal entries, manage attachments, count and query, and resolve CMDB items. On top of that bridge it ships typed incident conveniences and one optional, fully-configurable Case-mirroring recipe (LimaCharlie Cases ⇄ ServiceNow records).
Nothing pins the extension to the stock ITSM incident table. Security teams on Security Incident Response (sn_si_incident), change/problem workflows, or custom tables are all first-class: pass a table (or configure the mirror target) and the same actions apply.
The sync model is stateless on the LimaCharlie side:
- LC → ServiceNow:
mirror_caseidempotently upserts a ServiceNow record from a LimaCharlie Case, anchored on the record's standardcorrelation_id/correlation_displayfields. Repeated calls update the same record. - ServiceNow → LC:
pull_incident_changesreturns records updated since a watermark, normalized for feeding back into Cases. Changes made by the integration's own user are excluded, breaking echo loops.
Setup¶
1. Create a ServiceNow integration user¶
Create a dedicated ServiceNow integration user for the extension and grant it the roles needed for the tables and operations you intend to drive (e.g. itil for incidents, sn_si.analyst for Security Incident Response, plus rest_api_explorer/table ACLs as appropriate). The integration user's ACLs govern everything the extension can read, write, or delete.
Using a dedicated user matters for the Case-mirroring puller: pull_incident_changes filters out the extension's own writes by this user to break echo loops (see Case mirroring).
2. Choose an authentication mode¶
The extension supports three modes (set auth_mode):
| Mode | Requires | Notes |
|---|---|---|
basic |
username, password |
Username/password sent on every request. Simplest for ServiceNow. |
oauth_password |
client_id, client_secret, username, password |
OAuth2 Resource Owner Password Credentials grant, then refresh_token to renew. |
oauth_client_credentials |
client_id, client_secret |
True server-to-server grant (no end-user password). Needs extra instance-side setup — see below. |
For the OAuth modes, register an OAuth application in ServiceNow (System OAuth → Application Registry) and copy its Client ID and Client Secret.
The client credentials grant needs two extra pieces of instance-side setup beyond the client registration:
- Set the system property
glide.oauth.inbound.client.credential.grant_type.enabledtotrue(create it under System Properties if it doesn't exist). Without it the token endpoint returnsaccess_denied/server_error. - On the OAuth application record, set the OAuth Application User (the
userfield) to your integration user. The grant issues tokens as this user, so it must hold the roles the actions need (e.g.itil/sn_incident_writefor incident writes). Without it the token endpoint returnsunauthorized_client("integration user is not configured"). Setintegration_user(below) to this same username so SN → LC polling can de-echo the extension's own writes.
The basic and OAuth password modes don't need this — they authenticate as the username you configure directly.
3. Subscribe to the extension¶
Subscribe to ext-servicenow from the LimaCharlie Marketplace (Extensions → Add-Ons).
4. Store the credentials¶
In Secrets Manager, create secrets for the sensitive values — the password and client_secret fields are resolved as secret references at request time. For example create a servicenow-password secret and reference it as hive://secret/servicenow-password.
5. Configure the extension¶
In Extensions → ext-servicenow → Configuration, fill in:
| Field | Required | Value |
|---|---|---|
instance_url |
yes | ServiceNow instance base URL, e.g. https://acme.service-now.com. |
auth_mode |
no | basic (default), oauth_password, or oauth_client_credentials. |
username |
conditional | Required for basic / oauth_password. |
password |
conditional | Secret reference. Required for basic / oauth_password. |
client_id |
conditional | Required for oauth_password / oauth_client_credentials. |
client_secret |
conditional | Secret reference. Required for oauth_password / oauth_client_credentials. |
integration_user |
no | The ServiceNow user the extension authenticates as. pull_incident_changes excludes changes made by this user. Set it to enable the echo-loop guard. |
correlation_display |
no | Label stamped on mirrored records' correlation_display field (default LimaCharlie). Scopes upserts and SN→LC polling, so multiple integrations can coexist. |
close_code |
no | close_code applied when mirroring a case into Resolved/Closed (default Solution provided). Must be a value in your instance's close_code choice list, which varies by ServiceNow version — the legacy Solved (Permanently) is not present on current releases. An invalid value is silently dropped by ServiceNow, which then trips the mandatory-resolution-code data policy. |
mirror_table |
no | Target table for case mirroring (default incident; set sn_si_incident for Security Incident Response, or any task-derived table). |
mirror_subject_prefix |
no | Prefix for the mirrored record's short_description (default LimaCharlie Case). |
mirror_state_map |
no | Override of case-status→record-state mapping, e.g. {"new":1,"in_progress":2,"resolved":6,"closed":7}. Used in both directions. |
mirror_severity_map |
no | Override of case-severity→{urgency,impact} mapping, e.g. {"critical":{"urgency":1,"impact":1}}. |
Only instance_url is strictly required; the credential fields are validated at request time against the selected auth_mode. The extension is stateless — mirroring state lives in ServiceNow (correlation_id) and the returned watermark, so there is no database to provision.
Actions¶
All actions accept a JSON request body when invoked from a D&R rule via extension request. The typed actions and the mirroring recipe are conveniences — a customer who models ServiceNow differently can ignore them and drive create_record / update_record / query_table directly.
Generic Table API bridge (any table)¶
These actions operate against any table and never assume incident.
create_record¶
Insert a record into any table with an arbitrary field map. The generic write counterpart to query_table — use it for change_request, problem, sc_task, custom tables, etc.
| Field | Type | Notes |
|---|---|---|
table |
string | Required. Table name. |
fields |
object | Required. Field name→value map to set on the new record. |
get_record¶
Fetch a single record from any table by sys_id or by its number field.
| Field | Type | Notes |
|---|---|---|
table |
string | Required. Table name. |
sys_id |
string | Record sys_id. |
number |
string | Record number (alternative to sys_id). |
fields |
string | Comma-separated fields to return (sysparm_fields). |
display_value |
enum | false (raw, default), true (labels), or all. |
update_record¶
Patch a record on any table by sys_id with an arbitrary field map (unspecified fields are left untouched).
| Field | Type | Notes |
|---|---|---|
table |
string | Required. Table name. |
sys_id |
string | Required. Record sys_id to update. |
fields |
object | Required. Field name→value map to change. |
delete_record¶
Delete a record on any table by sys_id. Irreversible — the integration user's ACLs govern what can be deleted.
| Field | Type | Notes |
|---|---|---|
table |
string | Required. Table name. |
sys_id |
string | Required. Record sys_id to delete. |
query_table¶
Read-only Table API query against any table (incident, problem, change_request, cmdb_ci, sys_user, …). An escape hatch for AI agents that need data the typed actions don't cover, and the way to resolve display names to the sys_ids the write actions expect.
| Field | Type | Notes |
|---|---|---|
table |
string | Required. Table name. |
query |
string | ServiceNow encoded query (sysparm_query). |
fields |
string | Comma-separated fields to return. |
limit |
int | Max records (default 50). |
offset |
int | Pagination offset. |
display_value |
enum | false / true / all. |
Returns { "count": N, "records": [...] }.
count_records¶
Return the number of records matching an encoded query, via the Aggregate API (no rows pulled). E.g. count open criticals before deciding to escalate.
| Field | Type | Notes |
|---|---|---|
table |
string | Required. Table name. |
query |
string | Encoded query (optional; empty counts all). |
Typed incident conveniences¶
Table-aware shortcuts that default to incident; set table to e.g. sn_si_incident to operate on Security Incident Response records. Beyond the fields below, each accepts an extra object to merge raw ServiceNow fields the typed schema doesn't model.
create_incident¶
Open a record with typed subject/body, urgency/impact, and assignment. Returns the created record including its sys_id and number.
| Field | Type | Notes |
|---|---|---|
short_description |
string | Required. Incident subject line. |
table |
string | Table to create in (default incident). |
description |
string | Incident body / description. |
state |
int | Incident state (1 New, 2 In Progress, 3 On Hold, 6 Resolved, 7 Closed, 8 Canceled). |
urgency |
int | Urgency (1 High … 3 Low). |
impact |
int | Impact (1 High … 3 Low). |
priority |
int | Usually derived from urgency×impact; set to override. |
category |
string | Category. |
assignment_group |
string | Assignment group sys_id (reference; display names are not auto-resolved). |
assigned_to |
string | Assignee user sys_id. |
caller_id |
string | Caller user sys_id. |
correlation_id |
string | External correlation id (e.g. an LC case id). |
correlation_display |
string | External system label (e.g. LimaCharlie). |
extra |
object | Raw ServiceNow fields to merge. |
update_incident¶
Update a record by sys_id. Set state to drive workflow transitions, reassign, or append a work note/comment.
| Field | Type | Notes |
|---|---|---|
sys_id |
string | Required. Record sys_id to update. |
table |
string | Table to update (default incident). |
work_note |
string | Internal (IT-only) work note to append. |
comment |
string | Customer-visible comment to append. |
short_description, description, state, urgency, impact, priority, category, assignment_group, assigned_to, caller_id, extra |
Same typed incident fields as create_incident. |
get_incident¶
Fetch a single record by sys_id or by human number (e.g. INC0010023, SIR0001001).
| Field | Type | Notes |
|---|---|---|
table |
string | Table to read (default incident). |
sys_id |
string | Record sys_id. |
number |
string | Record number (e.g. INC0010023). |
fields |
string | Comma-separated fields to return. |
display_value |
enum | false / true / all. |
search_incidents¶
Search with a ServiceNow encoded query (sysparm_query), e.g. active=true^state=2^ORDERBYDESCsys_updated_on. Use to dedup before create or to look up existing work.
| Field | Type | Notes |
|---|---|---|
table |
string | Table to search (default incident). |
query |
string | ServiceNow encoded query. |
fields |
string | Comma-separated fields to return. |
limit |
int | Max records (default 50). |
offset |
int | Pagination offset. |
display_value |
enum | false / true / all. |
Returns { "count": N, "incidents": [...] }.
Journal, attachments, CMDB¶
add_note¶
Append an internal work note and/or a customer-visible comment to a record (default table incident). Journal fields append — they never overwrite.
| Field | Type | Notes |
|---|---|---|
sys_id |
string | Required. Record sys_id. |
table |
string | Table name (default incident). |
note |
string | Internal (IT-only) work note. |
comment |
string | Customer-visible additional comment. |
add_attachment¶
Upload a file as an attachment on a record (default table incident). Set content_base64=true to send binary content.
| Field | Type | Notes |
|---|---|---|
sys_id |
string | Required. Record sys_id. |
file_name |
string | Required. Attachment file name. |
table |
string | Table name (default incident). |
content_type |
string | MIME type (default application/octet-stream). |
content |
string | File content (text, or base64 when content_base64=true). |
content_base64 |
bool | true if content is base64-encoded binary. |
list_attachments¶
List a record's attachment metadata (default table incident). Returns each attachment's sys_id, file_name, size and content type; pass an attachment sys_id to get_attachment to download it.
| Field | Type | Notes |
|---|---|---|
sys_id |
string | Required. Record sys_id. |
table |
string | Table name (default incident). |
get_attachment¶
Download an attachment's bytes by its attachment sys_id (from list_attachments). Returns content_base64, content_type and size_bytes.
| Field | Type | Notes |
|---|---|---|
attachment_sys_id |
string | Required. sys_id of the attachment record (sys_attachment), not the parent record. |
lookup_ci¶
Resolve a CMDB configuration item (asset) by name or a custom encoded query — bridges LC sensor hostnames to the ServiceNow CMDB so incidents can reference the right asset.
| Field | Type | Notes |
|---|---|---|
name |
string | CI name to match (LIKE). |
query |
string | Custom encoded query (overrides name). |
class |
string | CMDB table/class (default cmdb_ci). |
limit |
int | Max records (default 50). |
Returns { "count": N, "cis": [...] }.
Case mirroring (optional)¶
A bidirectional, fully-configurable recipe that keeps a LimaCharlie Case and a ServiceNow record in sync. Mirroring is anchored on ServiceNow's purpose-built external-link fields: correlation_id holds the LimaCharlie case id and correlation_display holds the per-integration label (default LimaCharlie).
mirror_case¶
LC → ServiceNow. Idempotently upsert a ServiceNow record from an LC Case. Looks the record up by correlation_id=case_id (scoped to this integration's correlation_display), so repeated calls update the same record rather than creating duplicates. Wire this to a D&R rule on case events.
| Field | Type | Notes |
|---|---|---|
case_id |
string | Required. LimaCharlie case id (stored as correlation_id). |
case_number |
int | LimaCharlie case number (used in the record subject, LimaCharlie Case #N: …). |
status |
enum | new, in_progress, resolved, closed. Maps to state (configurable via mirror_state_map). |
severity |
enum | critical, high, medium, low, info. Maps to urgency/impact (configurable via mirror_severity_map; ServiceNow derives priority). |
classification |
string | Case classification (true_positive, false_positive, pending); appended to the description. |
summary |
string | Case summary (becomes the record subject — first line, truncated to 160 chars — and description). |
conclusion |
string | Case conclusion (appended to description, used as close_notes on terminal states). |
assignees |
list of string | Accepted, but not currently reflected on the record. |
tags |
list of string | Appended to the description. |
table |
string | Override the configured mirror target table for this call. |
correlation_display |
string | Override the correlation_display label for this mirror. |
sync_note |
string | Optional work note to record the sync on the record. |
extra |
object | Raw fields merged into (and overriding) the mapped record fields. |
Default mappings applied (all overridable via config):
- Status →
state:new→ 1,in_progress→ 2,resolved→ 6,closed→ 7. Terminal states (Resolved/Closed) also setclose_code(from config) andclose_notes. - Severity →
urgency/impact:critical→ 1/1,high→ ½,medium→ 2/2,lowandinfo→ 3/3.
Returns { "created": bool, "sys_id": "...", "number": "...", "incident": {...} }.
pull_incident_changes¶
ServiceNow → LC. Return records (on the mirror table, scoped to this integration's correlation_display) updated at/after a watermark, normalized to {case_id, case_status, …} ready to apply back to LC Cases. It excludes changes made by the integration_user to break echo loops, and returns a fresh watermark to drive the next pull. Drive it from a D&R schedule rule (e.g. every 12h per org) and pass the watermark back as rule state.
| Field | Type | Notes |
|---|---|---|
since |
string | ServiceNow datetime watermark (YYYY-MM-DD HH:MM:SS, UTC). Empty bootstraps from the most recent changes (newest first); pass the returned watermark back to move forward. |
limit |
int | Max records (default 100). |
include_own_changes |
bool | Disable the echo-loop guard (include changes by the integration user). |
Returns { "count": N, "changes": [...], "watermark": "YYYY-MM-DD HH:MM:SS" }. Each change carries sys_id, number, case_id (from correlation_id), state, a normalized case_status (new / in_progress / resolved / closed — On Hold maps to in_progress, Canceled to closed), short_description, sys_updated_on, and sys_updated_by.
The watermark boundary is inclusive (≥
since), so de-dupe applied changes bysys_id, and keeplimitabove the largest expected same-second burst of updates.
Wiring up the bidirectional sync¶
Both directions are driven from D&R rules — the extension holds no schedule of its own:
- LC → ServiceNow: a D&R rule on Case events calls
mirror_casewith the case fields, pushing changes as they happen. - ServiceNow → LC: a scheduled D&R rule periodically calls
pull_incident_changes, passing back the watermark from the previous run, and applies the returned changes to Cases.
Because pull_incident_changes excludes the integration user's own writes, the LC → SN → LC round trip does not re-import what the extension itself mirrored.
Detection & Response¶
Example response action that opens a ServiceNow incident for a detection:
- action: extension request
extension action: create_incident
extension name: ext-servicenow
extension request:
short_description: '{{ .cat }} - {{ .routing.hostname }}'
description: '{{ .event }}'
urgency: 2
impact: 2
table: '{{ "incident" }}'
Wrap literal strings in
{{ "..." }}. Values underextension requestare evaluated as templates. A bare string without{{ }}is interpreted as a gjson path against the event and, if it doesn't resolve, the key is silently dropped from the payload.
extension request actions are fire-and-forget — the rule engine does not surface the response back into the rule's evaluation context, so the freshly-created sys_id is not available to a subsequent action in the same rule. Workflows that need to chain (open a record, then attach a file, then add a note) belong in a Playbook or an AI agent, which can hold the sys_id between calls.
To append triage findings on an existing record (for example from a Playbook or AI agent that already knows the sys_id), use add_note:
- action: extension request
extension action: add_note
extension name: ext-servicenow
extension request:
sys_id: '{{ "a1b2c3d4e5f6..." }}'
note: '{{ .routing.hostname }}: suspicious process tree observed. See LC for details.'
Notes¶
- The extension is stateless — mirroring state lives in ServiceNow (
correlation_id) and the returnedwatermark; there is no database. - Reference fields (
assignment_group,assigned_to,caller_id) take sys_ids, not display names — display names are not auto-resolved. Usequery_tableagainstsys_user/sys_user_groupto resolve a name to its sys_id first. - ServiceNow rate limiting (
429) is honored once per request with aRetry-Aftercap of 5 seconds; a persistent429surfaces to the caller. - A business rule or data policy abort surfaces as an error either way: ServiceNow may return a non-2xx status (e.g.
403) or, for some aborts, HTTP200with a{"status": "failure"}envelope. The extension treats both as errors, never as success. (A common cause is resolving a record with aclose_codethat isn't in the instance's choice list — see theclose_codeconfig note above.) correlation_idandcorrelation_displayvalues are sanitized (encoded-query delimiters stripped) on both the write and the lookup path, keeping upserts idempotent even for hostile values.- The OAuth access token is cached and renewed via
refresh_token(inoauth_passwordmode). Rotating the secret in Secrets Manager evicts the cached client on the next surfaced401. pull_incident_changesonly breaks echo loops ifintegration_useris set to the user the extension authenticates as.- Errors are surfaced as
servicenow api <status> on <path>: <message>.