sn — command-line interface for ServiceNow. Multi-instance, multi-module, feature parity with servicenow-mcp-server.
- Everyday SN work — ticketing (
incident,change,problem,request,ritm), CMDB (ci), content (kb,catalog), agile (story,epic,task,project), attachments, batch ops. 35+ domains, 146 leaf commands. - Build on SN as a backend —
sn codegen {typescript, python, go}emits type-safe client code from the live dictionary;sn log tail -fandsn watchstream server-side events to stdout;sn openapi importscaffolds a Scripted REST API from an OpenAPI 3.x spec. - LLM integration —
sn mcp serveexposes every leaf as an MCP tool. Point Claude Desktop / Cursor / Claude Code at it and the agent gets the full CLI surface. Read-only by default; writes gated behind--allow-writes. - DevOps —
sn update-set export+sn diff <instance-a> <instance-b> <table>for promoting changes between environments and validating what landed. - OAuth + keyring —
sn auth loginruns OAuth 2.0 Authorization Code + PKCE, stores tokens in the OS keyring (macOSsecurity/ Linuxsecret-tool/ Windowscmdkey) with an AES-256-GCM file fallback. No passwords inconfig.json.
sn incident— list, get, create, update, resolve, close, reopen, comment, work-notesn change— list, get, create, update, submit-approval, approve, reject, add-task, comment, work-notesn problem— list, get, create, update, close, comment, work-notesn request— list, get, submit (catalog order via/api/sn_sc/.../order_now)sn ritm— list, get, update
sn update-set— list, get, create, update, use, current, commit, clone, add, movesn scope— current, setsn script— pull, push, watch (local.sn-sync.jsonmanifest,fs.watchdebounced)sn run-script— execute server-side JS viasys_trigger, optional--wait <seconds>sn business-rule,sn client-script,sn ui-policy,sn ui-action,sn ui-script,sn script-include,sn widget,sn ui-page— list, get, create, update, delete (each)sn rest-api— list, get, create, update +resource {create, update, delete}for operationssn workflow— list, get, create, update, delete, activity-add, transition-add, publish, create-full -f workflow.yamlsn flow— list, get, create, variables, variable-add, stages (logic blocks are UI-only)sn schema— tables, discover, field
sn kb— list, base-create, category-create, article-{list,get,create,update,publish}sn catalog— list + grouped:item {list, get, update, move, validate, recommend, variable {list, create, update}}+category {list, create, update}
sn story,sn epic,sn task,sn project— list, get, create, update, delete (each)
sn ci— list, get, create, relationships, relate (with cmdb_rel_type name resolution)
sn attachment— list, get (with --download), upload (binary-safe multipart)sn batch— create, update, delete (sequential by default;--parallelforPromise.allSettled)sn aggregate <table> <stat>— COUNT/SUM/AVG/MIN/MAX via/api/now/stats/<table>sn import-set— create (staging), run-transformsn instance— list, use, info, add, remove, currentsn user— list, get, create, updatesn group— list, create, update, add-members, remove-members
sn search— natural-language → encoded querysn table— generic Table API (query/get/create/update/delete) for any table
sn codegen {typescript, python, go} <table>— live-schema codegen fromsys_dictionary+sys_choice, walks the super_class chain for inherited fields. TS interfaces + choice unions, Pydantic v2 models, or Go structs with typed const choices.sn log tail [--follow] [--level] [--source] [--message]— streamsyslogliketail -f. Colored by level,-fpolls, JSON/CSV output works.sn watch <table> [--query] [--interval N] [--since]— emits new/updated records as JSONL (one per line),--oncefor a single pass. Pipe tojq/ feed a reactive UI / trigger external automation.
sn auth {login, logout, status}— OAuth 2.0 Authorization Code + PKCE with browser handoff, tokens stored in OS keyring (macOSsecurity/ Linuxsecret-tool/ Windowscmdkey), AES-256-GCM file fallback.sn auth {session-login, session-logout}— form-login escape hatch for features ServiceNow gates on a web session (currently: AMB subscribe). Prompts for username/password, POSTs/login.do, stashes the web-session cookies in the keyring for ~30 min. Credentials themselves are never stored.sn impersonate <user> -- <cmd> [args…]— run a sub-command as another user via SN's session-cookie impersonation. OAuth-only. Ideal for ACL testing.sn webhook create -f spec.yaml— declarative scaffold: REST Message + function + Business Rule trigger from a YAML spec.
sn edit <table> <id> [--field <name>] [--no-confirm]— open a record (or one field) in$EDITOR, show a colored diff, prompt for confirmation, then PATCH only what changed. Dirty-write detection viasys_mod_countre-check. Reference fields get# → Display Nameannotations.sn openapi import <spec.{yaml,json}>— scaffold a Scripted REST API + one operation per path/method from an OpenAPI 3.x spec.--dry-runprints the plan.sn record-producer create -f spec.yaml— scaffold a Record Producer (catalog item that creates records on submit). One YAML → the catalog item + all its variables + choice values + auto-generated submit script that maps form values to the target table.
sn watch <table> --backend amb [--channel PATH]— push-based watch via ServiceNow's AMB (CometD long-poll). Near-zero latency when channels are configured. Default backend stayspoll. Experimental — see Workflows section for caveats.sn amb install-publisher <table>— scaffolds the SN-side Business Rule +sys_amb_processorfor a table. Idempotent.sn amb subscribe <channel> [--once]— raw CometD subscriber for any AMB channel. Good for debugging.sn amb publish <channel> <json>— fire an event from SN viasys_trigger. Pairs withsubscribefor round-trip testing.
sn mcp serve [--allow-writes] [--allow-admin]— expose everysnleaf as an MCP tool over stdio. Drop into Claude Desktop / Cursor / Claude Code config and the agent can run any command you can. Read-only by default.
sn export <table> [id] [--query Q] [--out PATH]— generic XML export via SN's platform/{table}.do?UNLendpoint (same as the UI's "Export to XML" action). Works for every table:sys_update_set,oauth_entity,sp_widget,sys_script_include, etc. Single sys_id → one record; pass--queryto export many.sn update-set export <id-or-name> [--out PATH] [--format xml|json]— packages an update set as the retrieval-shaped XML SN's Import-from-XML accepts:<sys_remote_update_set>wrapper + every<sys_update_xml>child. Also offers a structured--format jsonmode for jq/diffing.sn update-set import <xml>— uploads the XML to the target instance as a Retrieved Update Set (state=loaded). Requiressn auth session-login -i <instance>first (the SN endpoint is web-session gated).sn diff <instance-a> <instance-b> <table> [--query] [--key] [--fields]— field-level record diff across two configured instances. ReportsonlyInA/onlyInB/different/ identical.--key namefor portable records where cross-instance sys_ids differ.
- Output:
-o json | table | csv | yaml(TTY → table, pipe → json) sn completion {bash, zsh, fish}— emits completion scripts (auto-enumerates all commands)- Prebuilt binaries for linux-x64, darwin-arm64, darwin-x64 via
scripts/build-release.sh/ GitHub Releases
npm i -g @seemsindie/servicenow-cli
# or one-shot
npx @seemsindie/servicenow-cli instance listGrab the binary for your platform from the latest release:
# Linux x64
curl -L https://github.com/seemsindie/servicenow-cli/releases/latest/download/sn-linux-x64 -o /usr/local/bin/sn
chmod +x /usr/local/bin/sn
# macOS arm64
curl -L https://github.com/seemsindie/servicenow-cli/releases/latest/download/sn-darwin-arm64 -o /usr/local/bin/sn
chmod +x /usr/local/bin/sngit clone https://github.com/seemsindie/servicenow-cli.git
cd servicenow-cli
bun install
bun run build # → dist/sn (single binary)If no config exists, the CLI launches an interactive wizard:
bun run src/cli.ts instance addOr manually create ~/.config/servicenow-cli/config.json:
{
"instances": [
{
"name": "dev",
"url": "https://dev12345.service-now.com",
"auth": { "type": "basic", "username": "admin", "password": "..." },
"default": true
}
],
"defaultOutput": "table",
"color": "auto",
"scriptSync": { "workDir": "./sn-scripts" }
}Config discovery order:
--config <path>(explicit)./servicenow-cli.config.json(project-local)$XDG_CONFIG_HOME/servicenow-cli/config.json(default~/.config/servicenow-cli/config.json)
# Instances & ticketing
sn instance list
sn incident list --state 2 --priority 1
sn incident create --short-desc "Printer jam" --urgency 2
sn incident resolve INC0012345 --code "Solution provided" --notes "replaced toner"
sn problem close PRB0040001 --close-code "Risk Accepted" --close-notes "done"
# Natural-language search & escape hatch
sn search "high priority incidents assigned to admin"
sn table query sys_user --query "active=true" --sn-fields user_name,email --limit 5If you're building a mobile app, service, or integration that uses SN as a data/workflow backend — not administering it — the CLI has a few superpowers tailored for that workflow.
sn codegen <lang> <table> queries sys_dictionary + sys_choice + the parent-class chain, and emits a type-safe module with:
- A record type (
interface/BaseModel/struct) with correctly-typed fields - Choice fields as literal unions / Enum / typed
constblocks - Reference fields annotated with the target table
- Inherited fields from the super-class chain (e.g.
IncidentincludesTaskfields)
# TypeScript (interface + union types + label maps)
sn codegen typescript incident --out-file src/types/incident.ts
# Python (Pydantic v2 BaseModel + str Enum) — requires pydantic>=2
sn codegen python incident --out-file app/schemas/incident.py
# Go (struct + json tags + typed const blocks)
sn codegen go incident --out-file api/servicenow/incident.go
sn codegen go cmdb_ci_server --package cmdb --out-file cmdb/server.goYour app code is type-checked against the live schema of your instance — no drift between a hand-written definition and a field that got added in an update set last Tuesday.
For production-ish use, prefer OAuth over Basic Auth:
# One-time: register an OAuth app in SN (System OAuth → Application Registry)
# with redirect URL http://127.0.0.1:*/cb
sn instance add # pick "OAuth Authorization Code + PKCE"
sn auth login -i dev # opens browser, captures tokens into OS keyring
sn auth status -i dev # shows current user + expiry
sn incident list -i dev # uses the bearer token from keyring
sn auth logout -i dev # clears the keyring entriesTokens live in the macOS Keychain / GNOME Keyring / Windows Credential Manager — never in config.json. Refresh happens automatically before every call.
sn impersonate abel.tuter -- sn incident list --limit 5
sn impersonate alice.admin -- bash # interactive sub-shell as another userRequires an OAuth profile. Admin's bearer is exchanged for the target user's bearer via /api/now/ui/impersonate/<sys_id>; that token is piped to the sub-command via SN_IMPERSONATION_TOKEN_FILE. Clean temp-file with 0600 perms; auto-cleaned on exit.
# webhook.yaml
name: Notify Slack on P1 incidents
trigger:
table: incident
when: after
condition: priority=1^active=true
endpoint:
url: https://hooks.slack.com/services/...
method: POST
headers:
Content-Type: application/json
body: |
{"text": "P1: ${current.number}"}
retry:
attempts: 3
delay_seconds: 30sn webhook create -f webhook.yamlCreates sys_rest_message + sys_rest_message_fn + a Business Rule that triggers it, all inside the current update-set.
sn log tail streams syslog like tail -f — invaluable when you're iterating on a Scripted REST API or a Business Rule and need to see what gs.info() / gs.error() printed.
# One-shot, recent 50 entries
sn log tail
# Follow-mode with filters
sn log tail --follow --level error
sn log tail --follow --source "Business Rule" --message "my-api"sn watch <table> polls for records with sys_updated_on >= cursor and emits each as a JSON line. Perfect for feeding a reactive dashboard, kicking off external automation, or piping to jq.
# Watch new P1 incidents forever, feed to Slack
sn watch incident --query "priority=1^active=true" --follow |
while read -r rec; do
number=$(echo "$rec" | jq -r .number)
curl -X POST https://slack.../webhook -d "{\"text\":\"P1: $number\"}"
done
# One-shot since a specific time (for cron)
sn watch incident --once --since "2026-04-20 00:00:00" > today.jsonl# Bulk-seed test data
cat seed.json | sn batch create -f -
# Run a server-side fix script with result polling
sn run-script --code "gs.info('hello from ' + gs.getUserName());" --wait 15
# Ad-hoc aggregation for a dashboard
sn aggregate incident count --group-by priority -o jsonAuth works as whoever is in your config. Point your client integration tests at a restricted user to validate ACLs:
sn instance add # add a non-admin test account
sn incident list -i test-user --limit 5 # see what they seesn edit fetches a record, opens it in $EDITOR as YAML (read-only audit fields stripped, reference fields annotated with display names), shows you a colored diff when you save, prompts to confirm, then PATCHes only the fields that changed.
# Full-record edit — opens YAML with reference-field hints
sn edit incident INC0010016 -i devoauth
# Shows a diff + [y/N] prompt; y applies the PATCH, anything else aborts.
# One-field edit — opens just that field with a sensible extension
sn edit sp_widget <sys_id> -i dev --field template # → .html
sn edit sys_script_include <sys_id> -i dev --field script # → .js
# Non-interactive / scripted
sn edit incident INC0010016 -i devoauth --no-confirm --editor /path/to/sed-scriptDirty-write protection is built in: sn edit captures the record's sys_mod_count at fetch time and re-GETs right before the PATCH. If another user modified the record between your fetch and save, the edit aborts with a clear "re-run sn edit to pick up the latest" message.
# Create a set and start working under it
SET=$(sn update-set create --name "CLI smoke" -o json | jq -r .sys_id)
sn update-set use "$SET" # persists to sidecar state
sn business-rule create --name Foo --collection incident --when before --script-file foo.js
sn update-set get "$SET" --full # inspect captured records
sn update-set commit "$SET"Any write command (business-rule create, script-include update, etc.) accepts --update-set <sys_id> to override the current set for that invocation. Pass --no-apply-state to skip entirely.
- Update-set binding on Basic-Auth REST is best-effort. SN's mechanism for the "current update set" depends on a per-user browser session; Basic-Auth REST requests don't always honour the
sys_user_preferenceflip the CLI performs. If records aren't landing where you expect, set the update set interactively in the browser first (with the same user), or attach records manually viasn update-set add. Scope binding (via concoursepicker) is more reliable. - Sidecar state races. The per-instance sidecar at
~/.config/servicenow-cli/state/<instance>.jsonis a single file — parallel invocations to the same instance can clobber each other'supdate-set use/scope setwrites. Prefer--update-set/--scopeflags in automation.
Package an update set as XML, move it to another environment, and verify the landing with a cross-instance diff.
# Export a completed update set (parent + all sys_update_xml children, in
# the retrieval-shape SN's Import-from-XML accepts).
sn update-set export "My Change Set" -i dev --out /tmp/my-change.xml
# Import on the target instance (requires `sn auth session-login -i prod`
# first — the endpoint is web-session gated).
sn auth session-login -i prod
sn update-set import /tmp/my-change.xml -i prod
# → creates a sys_remote_update_set record in state=loaded, returns its sys_id.
# Preview + commit via the SN UI (Retrieved Update Sets → Preview → Commit).
# Or use the generic exporter for any table — same platform endpoint (`.do?UNL`)
sn export sys_update_set <sys_id> -i dev --out /tmp/my-change.xml
sn export oauth_entity <sys_id> -i dev > /tmp/my-oauth-app.xml
sn export sys_script_include --query "nameLIKEMyUtil" -i dev > /tmp/my-utils.xml
# Import in the target (SN UI: Retrieved Update Sets → Import Update Set from XML)
# Then preview + commit via SN's UI, or
sn update-set commit <remote-sys-id> -i test
# Verify what actually landed — field-level diff of the records you care about
sn diff dev test incident --query "priority=1" --fields "number,short_description,state"
# Portable records (script includes, business rules) have different sys_ids across
# instances — key on name instead:
sn diff dev test sys_script_include --key name --limit 50
# JSON output for machine consumption / CI gates
sn diff dev prod sys_script_include --key name -o json | jq '.counts'sn update-set export also has a --format json mode that dumps the parent sys_update_set row plus every sys_update_xml child, useful for structural diffs / version control when you want to track a set's contents as code.
Why no
sn update-set import? SN has no clean REST endpoint for XML-upload — the SN-native pattern is cross-instance retrieval (configure one instance as an "Update Source" of another), which requires SN-side config. Queued for a future release once we confirm an approach that doesn't require per-instance setup.
ServiceNow's Asynchronous Message Bus (CometD long-poll) streams record changes to subscribers the moment they happen — no polling overhead.
# 1. One-time SN-side setup: scaffolds sys_amb_processor (channel
# registration) + a Business Rule that publishes record changes for
# <table> to /sn-cli/record/<table>. Idempotent; pass --force to
# overwrite the BR after upgrading sn-cli.
sn amb install-publisher problem -i dev
# 2. Subscribe to record-change events
sn amb subscribe /sn-cli/record/problem -i dev
# OR via the watch command (same output format as the polling backend):
sn watch problem --backend amb -i dev
# 3. In another shell, make any change to a problem → subscriber prints
# the event within ~1s:
sn problem update <sys_id> --set "short_description=push test" -i dev
# 4. Publish a raw event (useful for debugging subscribers)
sn amb publish /sn-cli/record/problem '{"hello":"amb"}' -i devAuthentication: AMB requires a web session (cookie-based), not the OAuth bearer that powers the rest of the CLI. Basic-auth instances work transparently — sn amb uses basic-auth credentials to obtain a session automatically. OAuth-authcode instances need an explicit session-login first:
sn auth session-login -i dev # prompts for user/pass; ~30 min session
sn amb subscribe /sn-cli/record/problem -i dev
sn auth session-logout -i dev # clear when doneCredentials from session-login are never stored — only the resulting session cookies + CSRF token, in the OS keyring, with a 30-minute TTL.
How it works (for the curious):
install-publishershells through asys_triggerbackground script becausesys_amb_processoris ACL-locked against REST writes. The trigger runs as system user and creates two records: the channel processor (enables subscribe authorization) and a Business Rule (writessys_amb_messagerows on record changes).publishdoes the same — a background script inserts directly intosys_amb_messagebecause SN's scripted publish APIs (sn_ws.AMBClient,GlideChannelAMB, etc.) aren't exposed on all instances. The direct-table insert is the one reliable mechanism.subscribedoes a CometD handshake, captures theBAYEUX_BROWSERcookie for node affinity, and loops/amb/connectwith cookie +X-UserTokenauth.
Known limitations:
- Some locked-down instances block the
sys_amb_processorinsert even from a background script. When that happens the trigger logsprocessor insert returned nulland an admin has to create the processor record manually in the UI (System Definition → AMB → Channel Processors → New,channel_name = /sn-cli/record/<table>,public = true). - Sessions expire after ~30 minutes of inactivity. Re-run
session-loginwhen subscribe starts returning404::message_deletedagain.
Run the CLI as a Model Context Protocol server so Claude Desktop / Cursor / Claude Code agents can call every sn command as a tool — same auth, same multi-instance config, no duplicated implementation.
# Verify it boots and lists tools
sn mcp serve --allow-writes &
npx @modelcontextprotocol/inspector stdio sn mcp serve --allow-writesEdit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%/Claude/claude_desktop_config.json (Windows):
{
"mcpServers": {
"servicenow": {
"command": "sn",
"args": ["mcp", "serve", "--allow-writes"]
}
}
}claude mcp add servicenow -- sn mcp serve --allow-writesEvery leaf becomes one MCP tool with a dot-separated name:
| Command | Tool |
|---|---|
sn incident list |
incident.list |
sn update-set export |
update_set.export |
sn codegen typescript |
codegen.typescript |
- default: read-only.
list,get,query,export,diff,status,info,schema,search,aggregate,watch,tail, allcodegen.*andcompletion.*. --allow-writes: enables creates/updates/adds (incident.create,update_set.create,edit,webhook.create,openapi.import, ...).--allow-admin: enables destructive ops (commit,delete,close,resolve,approve,reject,impersonate,run-script). Implies--allow-writes.
Pick the lowest tier an agent actually needs. Writes are gated on purpose.
# Pull a record's script fields into ./sn-scripts/
sn script pull <sys_id> --table sys_script_include # --table optional; inferred otherwise
# Edit locally in your editor
$EDITOR sn-scripts/myscript.js
# Push back
sn script push sn-scripts/myscript.js
# Or watch + auto-push (debounced fs.watch)
sn script watch sn-scripts/Manifest lives at ./.sn-sync.json in the working directory (one per project / scoped app). Multi-field records (widgets, UI pages) are pulled into a subdirectory with one file per field.
# Fire-and-forget
sn run-script --code "gs.info('hello from cli');"
# From file with auto-wait
sn run-script ./fix.js --wait 15
# From stdin
cat fix.js | sn run-script -The script runs as a one-shot sys_trigger that auto-deletes itself after execution (--no-auto-delete to keep the record). --wait <seconds> polls the trigger's state field and exits when it transitions to executed (1) or error (2).
Available on every command (pass them after the subcommand name):
| Flag | Purpose |
|---|---|
-i, --instance <name> |
Target a specific instance |
-o, --output <json|table|csv|yaml> |
Output format (TTY default: table, pipe default: json) |
--config <path> |
Explicit config path |
--fields <csv> |
Override columns in table output |
-q, --quiet |
Suppress INFO/WARN log lines |
--debug |
Enable DEBUG logging |
--no-color |
Disable ANSI colors |
Write commands (create/update/delete/close/resolve) additionally accept:
| Flag | Purpose |
|---|---|
--update-set <sys_id> |
Override current update-set for this call |
--scope <sys_id> |
Override current scope for this call |
--no-apply-state |
Don't apply any session state |
| Code | Meaning |
|---|---|
| 0 | ok |
| 1 | generic error |
| 2 | config error (missing, invalid) |
| 3 | auth error (HTTP 401) |
| 4 | not found (HTTP 404) |
| 5 | validation error (HTTP 400/409 or Zod) |
| 64 | user cancel |
Both basic and OAuth 2.0 (password grant) are supported. Credentials live in config.json unencrypted — protect the file with filesystem permissions.
bun run dev # run CLI in watch mode
bun run start # run CLI once
bun test # unit tests only
bun run typecheck # tsc --noEmit
bun run build # compile dist/sn single binary
RUN_INTEGRATION=1 bun test # + integration tests against dev PDIUnit tests run offline. Integration tests are env-gated by RUN_INTEGRATION=1 and hit a real ServiceNow developer instance (PDI). Config path defaults to servicenow-mcp-server/config/servicenow-config.json; override with SN_TEST_CONFIG and/or SN_TEST_INSTANCE.
The composite tests/integration/phase2-smoke.test.ts walks the full platform-dev workflow in one shot: update-set create + use → business-rule create → script pull/push → run-script with wait → commit → schema lookup. Use it as a regression check after touching any Phase 2 code.
See LICENSE.