- What SDK documentation already exists in this project (and what was missing)
- What the SDK is, and when to use it instead of Studio
- Install and authenticate — every
authflag - Complete command reference — all 12 commands
explain— the built-in 256-topic documentation system- Project anatomy —
now.config.json,keys.ts, build outputs - Full tutorial — build the HR Leave app from zero in Fluent
- Cursor integration — rules, skills, agent mode, the explain-first loop
- Bringing existing applications into the SDK
- CI/CD —
--frozenKeysand OAuth client credentials - Corrections: three files in this repo call a command that does not exist
- Troubleshooting
- Appendix — all 256
explaintopics - Quick reference card
0What already exists in this project — and what was missing
Read this first so you know which file to trust
The short answer to "where is the full SDK documentation": it did not exist until this page. What the repository had was five fragments totalling about six kilobytes, plus three real working SDK projects with no prose explaining them. Worse, the file whose name promises SDK documentation does not contain any.
| File | Size | What it actually contains | Verdict |
|---|---|---|---|
html/11_SDK_CURSOR_CLAUDE.html | 9.4 KB | "Studio Practice Companion" — a Studio-only workflow guide. The SDK content was replaced by tools/convert_docs_01_16_studio_only.py. Zero mentions of the SDK, npm, or Cursor. |
MISLEADING NAME |
11_SDK_CURSOR_CLAUDE.md | 2 KB | The original markdown: a status table of what is installed on dev398980, four test steps, and two daily commands. Useful as a project status note. | Status note, not documentation |
sdk/examples/README.md | 0.6 KB | Seven-line quick start: npm init, install, auth --add, init, build, deploy. |
Correct but minimal |
servicenow-ai-workspace/README.md | 0.9 KB | The three-app table and a deploy-all snippet. | Orientation only |
.cursor/rules/servicenow-sdk-master.mdc | 1.4 KB | Cursor agent rules: conventions, scopes, deploy commands, model selection. | Good, but tells the agent to run a command that does not exist — see §10 |
html/AI_SERVICENOW_FULL_DEMO_MASTER.html Part 10 | — | A 14-row command table written from general knowledge, not from your installed CLI. | HAS ERRORS — corrected in §10 |
What does exist, and is genuinely good: three working SDK projects
| Project | Scope | State |
|---|---|---|
servicenow-ai-workspace/hr-leave-sdk/ | x_786686_hr_leave | Fully built. 12 Fluent source files, generated keys.ts with ~1,340 lines of pinned sys_ids, and a successful build artefact at target/hr_leave_sdk_1_0_0.zip (114 KB). |
servicenow-ai-workspace/crm-suite-sdk/ | x_snc_crm_suite | Scaffolded with a few Fluent files, not built. |
servicenow-ai-workspace/portal-website-sdk/ | x_snc_sp_website | Scaffolded with Fluent + a src/portal/ folder, not built. |
Every code example in §6 of this page is taken from hr-leave-sdk/src/fluent/ — real source that has actually compiled and installed, not invented samples.
1What the SDK is, and when to use it instead of Studio
Developer
The ServiceNow SDK is a Node command-line tool that lets you define platform artefacts as TypeScript source on your own machine, compile them into an installable application package, and install that package onto an instance. The TypeScript dialect you write is called Fluent: a set of typed factory functions — Table(), BusinessRule(), Acl(), Role(), Flow() — exported from @servicenow/sdk/core.
The thing most people miss on first contact: this is not a wrapper that drives the Studio UI, and it is not an update-set generator. It produces the application package itself. Your source files are the definition of the app; the instance is a deployment target.
1.1 The honest comparison
| Concern | Studio | SDK + Fluent |
|---|---|---|
| Where truth lives | The instance | Your git repository |
| Code review | Compare update set XML, painfully | A normal pull request diff |
| Rollback | Back out an update set and hope | git revert, rebuild, reinstall |
| Two developers on one app | Update-set collisions | Git branches and merges |
| Record identity across instances | sys_id travels in the update set | Pinned in committed keys.ts — see §5.3 |
| Working offline | Impossible | Everything but install and query |
| AI coding assistance | Weak — the model cannot see a browser form | Strong — it is just TypeScript in your editor |
| Learning curve | Low | Real. Expect a slow first week. |
| Coverage | Everything on the platform | Wide but not total — check now-sdk explain --list before promising a feature |
| Who is comfortable | Admins, configurators | Developers who already use git daily |
Do not mix uncontrolled. If a table is defined in Fluent and someone also edits it in the instance UI, the next install can overwrite their change — and install --reinstall will definitely destroy any on-instance metadata that is not in your source. Decide per application: either it is SDK-managed, or it is Studio-managed. "Both" is how teams lose work.
1.2 When each one is the right answer
Use the SDK when…
More than one developer touches the app; you want pull-request review; you need repeatable deploys to several instances; you want an AI assistant to help write platform code.
Stay in Studio when…
You are configuring rather than building, the change is a one-off, the team has no git practice, or the artefact type has no Fluent API yet.
Hybrid that works
Core logic (tables, script includes, business rules, ACLs) in the SDK. Instance-specific configuration (properties values, scheduled job times, data) set on the instance.
Hybrid that fails
Editing Fluent-managed business rules in Studio "just this once". The next build silently reverts it and nobody can explain why.
2Install and authenticate
Developer · 10 minutes
2.1 Prerequisites
| Node.js | 20.18 or newer. Verified working here on v20.20.2. Check with node -v. |
| npm | Ships with Node. The SDK is installed per project, not globally. |
| Instance | A PDI or sub-production instance where your user has admin and app_creator. |
| OS | Windows, macOS or Linux. This project is on Windows, so every example below is PowerShell-safe. |
2.2 Install into a project
mkdir my-app
cd my-app
npm init -y
npm install -D @servicenow/sdk@latest
# confirm what you got
npx @servicenow/sdk --version
npx @servicenow/sdk --help
Three binaries are installed into node_modules/.bin/: now-sdk, now-sdk-debug, and @servicenow/sdk. They are the same program. Inside an npm script you can write now-sdk build directly; from your shell use npx @servicenow/sdk build or npx now-sdk build.
This project pins the version rather than using latest, which is the right call for a team:
{
"name": "hr-leave-sdk",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "now-sdk build",
"clean": "now-sdk clean",
"install:dev": "now-sdk install --auth DEV",
"deploy:dev": "now-sdk install --auth DEV"
},
"devDependencies": {
"@servicenow/sdk": "4.12.2"
}
}
2.3 Authentication — the complete auth surface
Credentials are stored in your OS keychain under an alias, never in the project. One alias per instance.
--add <instance> | Instance name or URL to store credentials for. Prompts interactively unless you pipe a password. |
--type <basic|oauth> | Authentication type for the new credential. Only these two values. |
--alias <name> | The alias to store it under. Pick something short: dev, test. |
--delete <alias> | Delete a stored credential. |
--list | List all stored credentials. |
--use <alias> | Set an alias as the default for subsequent commands. |
--print [alias] | Print a live auth token for manual API calls. Omit the alias to use the default. |
--format <headers|bearer|env> | Output shape for --print. Default headers. |
--password-stdin | Read the password from stdin — the only safe way to script --add. |
# interactive, the normal case
npx @servicenow/sdk auth --add https://dev398980.service-now.com --type basic --alias DEV
# list what you have
npx @servicenow/sdk auth --list
# make one the default so you can drop --auth from later commands
npx @servicenow/sdk auth --use DEV
# fully non-interactive (CI, containers) — password never appears in the command line
echo "$env:SN_PASSWORD" | npx @servicenow/sdk auth --add https://dev398980.service-now.com `
--type basic --alias DEV --username admin --password-stdin
# remove it
npx @servicenow/sdk auth --delete DEV
2.4 auth --print — the flag worth knowing about
This is the most useful command almost nobody discovers. It mints a live token from a stored credential so you can hit the REST API by hand without ever typing a password into a terminal:
# ready-to-paste curl headers
npx @servicenow/sdk auth --print DEV --format headers
# just the bearer token, for scripting
npx @servicenow/sdk auth --print DEV --format bearer
# shell-exportable form
npx @servicenow/sdk auth --print DEV --format env
Combine it with now-sdk query (§3.11) and you have a complete read-only investigation toolkit for an instance without leaving your terminal.
Validation for §2: auth --list shows your alias, and auth --print <alias> --format bearer returns a token string. If the second one fails, the stored credential is wrong — delete and re-add it.
3Complete command reference — all 12 commands
Captured from now-sdk <command> --help on SDK 4.12.2
Three global options apply to every command: -d, --debug (print debug output, default false), -h, --help, and -v, --version. Where a command takes a source or --directory argument, the default is the current working directory — so run commands from the project root and you can omit it.
3.1 The twelve commands at a glance
| Command | Alias | One line | Needs instance? |
|---|---|---|---|
auth | — | Configure authentication to an instance | Yes, to validate |
init | create | Create a project, apply a template, or convert a legacy app | Only with --from <sys_id> |
download <directory> | — | Download application metadata from an instance | Yes |
build [source] | — | Compile Fluent sources into app files and an installable package | No |
install | deploy | Install or update the application on an instance | Yes |
dependencies [sysIds..] | — | Download declared dependencies and TypeScript type definitions | Yes |
transform | — | Convert instance XML records into Fluent source code | Yes, unless --from a local path |
clean [source] | — | Clean the output directory | No |
pack [source] | — | Zip the built app into an installable artefact | No |
explain [topic] | — | Display built-in documentation for a Fluent topic | No |
query <table> | — | Query records from a table on the instance | Yes |
cicd <command> | — | Run sn_cicd operations: app repo install/publish/rollback, test suites | Yes |
There is no validate command and no upgrade command. Running now-sdk validate exits with Unknown command: validate. Three files in this repository call it — see §10. Validation happens inside build; to upgrade the SDK you change the version in package.json and run npm install.
3.2 init — create or convert a project
Initialises a new ServiceNow custom application, applies a template to an existing application, or converts a legacy scoped application — from an instance or from a local directory — into a Fluent app.
--from <sys_id|path> | sys_id of a legacy application on the instance, or a filesystem path to a directory containing a legacy scoped app, to convert into Fluent. |
--appName <name> | Display name of the application. |
--packageName <name> | npm package name for the project. Must follow npm naming rules. |
--scopeName <scope> | Scope name. Must start with your vendor prefix if you have one, and cannot exceed 18 characters. |
-a, --auth <alias> | Credential alias to authenticate with. |
--template <id> | Project template. Choices: partial.javascript.react, partial.javascript.basic, partial.typescript.react, partial.typescript.vue, partial.typescript.basic, partial.typescript.react-devserver. |
# brand new app, fully specified so it never prompts
npx @servicenow/sdk init --appName "HR Leave" --packageName hr-leave-sdk `
--scopeName x_786686_hr_leave --auth DEV
# with a UI template
npx @servicenow/sdk init --appName "Ops Console" --packageName ops-console `
--scopeName x_acme_ops --template partial.typescript.react --auth DEV
# convert an existing scoped app that lives on the instance
npx @servicenow/sdk init --from c84db0e8839f0b509f02b0d6feaad3c9 --auth DEV
The 18-character scope limit is a hard stop. x_786686_hr_leave is 17. Plan the name before you create the app, because changing scope later means recreating it.
3.3 build — compile Fluent into an installable package
Compiles everything in your fluentDir into app files and generates the installable package. It also regenerates src/fluent/generated/keys.ts — see §5.3, because that side effect matters more than anything else on this page.
source | Positional. Path to the directory containing package.json. Defaults to the current directory. |
--frozenKeys | Fail the build if keys.ts changed during it. Use this in CI. Default false. See §9. |
--errorOnConflict | Treat sys_id conflicts between Fluent and XML as errors rather than warnings. Fluent-to-Fluent conflicts are always errors regardless. Default false. |
--skipClean | Do not clean output directories before building. Faster, occasionally stale. Default false. |
--legacyChoices | Generate choice set XML with the sys_choice_set wrapper — the destructive v3 behaviour — instead of sys_choice_v2 additive merge. Only for backward compatibility. |
npx @servicenow/sdk build
npx @servicenow/sdk build --frozenKeys # CI gate
npx @servicenow/sdk build --errorOnConflict # strict mode, recommended once stable
There is no --watch flag. For a fast loop, use your own watcher, e.g. npx nodemon --watch src/fluent -e ts --exec "npx @servicenow/sdk build".
3.4 install — push the app to an instance
Installs or updates the built application on an instance. deploy is an alias, so both spellings work; the canonical name is install.
--source <path> | Project directory. Defaults to the current directory. |
-a, --auth <alias> | Credential alias. Omit to use the default set by auth --use. |
-r, --reinstall | Uninstall and reinstall so instance metadata exactly matches the package. DESTRUCTIVE — metadata created on the instance that is not in your source is lost. Default false. |
-b, --open-browser | Open the sys_app page in your default browser on success. Default false. |
-i, --info | Get information from the instance about the most recent install of this app. Default false. |
--async | Install asynchronously, falling back to synchronous if the instance does not support it. Default true. |
--demoData | Install demo data. Default true. Pass --demoData false to skip it. |
--skip-flow-activation | Do not activate (publish) flows after install. Default false — so flows are activated by default. |
npx @servicenow/sdk install --auth DEV
npx @servicenow/sdk install --auth DEV --open-browser
npx @servicenow/sdk install --auth DEV --info # what is currently installed?
npx @servicenow/sdk install --auth DEV --reinstall # nuclear: read the warning first
Two defaults to notice. --demoData defaults to true, so demo data goes in unless you say otherwise. And flows are activated on install unless you pass --skip-flow-activation — which explains why the HR Leave approval flow in this project is marked "activated by install".
3.5 download — pull application metadata from an instance
directory | Required positional. Path to expand the application into. |
--source <path> | Project directory containing package.json. Defaults to the current directory. |
--incremental | Download only what changed since the last download. Default false. |
npx @servicenow/sdk download ./downloaded
npx @servicenow/sdk download ./downloaded --incremental
Use this to see what the instance actually holds after someone edited the app outside your source. It is the diagnostic half of the drift problem described in §1.1.
3.6 transform — turn instance records into Fluent source
Downloads XML records from an instance, or reads them from a local path, and converts them into Fluent source code. This is how you migrate an existing application into the SDK one artefact type at a time instead of rewriting it.
--table <names> | Comma-separated table names to transform by table hierarchy. Combined with --id it transforms one specific record and its relationships. |
--force | Allow transforming descendant tables without their parent hierarchy. Default false. |
--from <path> | Path to a local XML file or directory to transform — no instance needed. |
--directory <path> | Project directory. Defaults to the current directory. |
-a, --auth <alias> | Credential alias. |
-f, --format | Automatically format new and updated source after transforming. Default true. |
# bring every business rule and ACL of the app into Fluent
npx @servicenow/sdk transform --table sys_script,sys_security_acl --auth DEV
# convert an update set XML you already exported
npx @servicenow/sdk transform --from ./exports/update-set.xml
3.7 dependencies — types and declared dependencies
Downloads the dependencies declared in now.config.json plus the TypeScript type definitions that make your editor autocomplete ServiceNow APIs. Run it after cloning the repo on a new machine.
sysIds.. | Positional array of sys_ids to add when using --add. Default empty. |
--directory <path> | Project directory. |
-a, --auth <alias> | Credential alias. |
--type-defs-only | Download only the script type definitions (glide.*.d.ts). |
--fluent-only | Download only Fluent types for the dependencies in now.config.json. |
--add <table> | Add a new dependency item by table name, e.g. actions, triggers, sys_security_acl. |
--scope <scope> | Scope for the dependency, e.g. global or x_my_app. Required with --add. |
# after git clone
npm install
npx @servicenow/sdk dependencies --auth DEV
# just refresh GlideRecord / GlideSystem types for editor autocomplete
npx @servicenow/sdk dependencies --type-defs-only --auth DEV
# depend on a global flow action so Fluent can reference it
npx @servicenow/sdk dependencies --add actions --scope global --auth DEV
This is the command that fixes "my editor has no autocomplete for GlideRecord". It is also the one most often forgotten after a fresh clone.
3.8 clean and pack
clean removes the build output directory. Its only options are the global three plus the positional source.
pack zips the already-built app into an installable artefact — the .zip you can hand to someone or upload. One extra flag:
--skip-package-inventory | Skip generating package_inventory.csv in the output. Default false. |
npx @servicenow/sdk clean
npx @servicenow/sdk build
npx @servicenow/sdk pack
# → target/hr_leave_sdk_1_0_0.zip (this project's artefact is 114 KB)
The package_inventory.csv that pack produces is a plain list of everything in the package. It is the fastest way to answer "did this build actually include the new ACL?" without unzipping anything.
3.9 query — read instance data from your terminal
A full Table API client built into the CLI. Every sysparm_* you know from the REST API has a flag here, and it paginates for you.
table | Required positional. Table name, e.g. incident, sys_user. |
-q, --query <encoded> | Required. Encoded query, e.g. "active=true^priority<=2". |
--limit <n> | Maximum records per page. Default 100. |
--offset <n> | Starting offset. Default 0. |
-f, --fields <list> | Comma-separated fields to return. |
--display-value <v> | true, false, or all for both raw and display. Default false. |
--exclude-reference-link | Strip reference link metadata. Default true. |
--no-count | Skip total count calculation for speed. Default false. |
--timeout <ms> | Per-request timeout; each page fetch is bounded by it. Default 30000. |
--view <name> | UI view that determines which fields come back. |
--query-category <c> | Query category for extended queries. |
--query-no-domain | Ignore domain separation. Default false. |
-a, --auth <alias> | Credential alias. |
-o, --output <json|raw> | json for a machine-readable envelope; raw to unquote the --select result. |
-s, --select <path> | Dot/bracket path to extract, e.g. "records[0].sys_id". Implies machine-readable output. |
# what is in my app's table right now?
npx @servicenow/sdk query x_786686_hr_leave_req_v2 -q "u_state=submitted" `
-f u_number,u_state,u_days_count --display-value all --auth DEV
# grab a single sys_id for a script, unquoted
npx @servicenow/sdk query sys_user -q "user_name=admin" `
-s "records[0].sys_id" -o raw --auth DEV
# did my business rule actually fire? check the audit table
npx @servicenow/sdk query x_786686_hr_leave_audit_v2 -q "ORDERBYDESCsys_created_on" `
--limit 5 -f u_old_state,u_new_state,u_event_type --auth DEV
The --select plus --output raw pair is designed for shell command substitution, which makes this command genuinely scriptable rather than just a viewer.
3.10 cicd — app repository and test suite operations
cicd install | Install an application from the application repository onto the instance. |
cicd publish | Publish an application to the application repository. |
cicd rollback | Roll back an installed application to a previous version. |
cicd testsuite <command> | sn_cicd test suite operations — run your ATF suites from the pipeline. |
cicd test <command> | sn_cicd individual test operations. |
npx @servicenow/sdk cicd publish --help
npx @servicenow/sdk cicd testsuite --help
This is the correct path to production. As the SDK's own CI guidance states plainly: do not use install from CI against production. Publish to the app repository and promote through the platform's own mechanism so change management, approvals and rollback behave the way ServiceNow expects. cicd rollback exists precisely because that path supports it and a direct install does not.
3.11 explain — covered in its own section
explain deserves more than a table. See §4.
4explain — the built-in documentation nobody uses
256 topics shipped inside the package you already installed
The SDK ships its own complete reference offline. On your installed 4.12.2 there are 256 topics: 152 API references (one per Fluent artefact type) and 97 narrative guides, plus faq, keys-file, now-config-reference and the language overviews. No network, no browser, no stale documentation site.
topic | Positional. Topic name or a keyword to search for. |
-l, --list | List topics. Combine with a topic to filter, e.g. --list flow. |
--format <pretty|raw> | pretty for the terminal, raw for plain markdown. Default pretty. |
-p, --peek | Show a brief summary instead of the full document. |
4.1 How to actually use it
# what topics exist at all?
npx @servicenow/sdk explain --list
# narrow by keyword — every flow-related topic
npx @servicenow/sdk explain --list flow
# read the full API reference for a Fluent type
npx @servicenow/sdk explain businessrule-api
npx @servicenow/sdk explain acl-api
npx @servicenow/sdk explain table-api
# read a narrative guide rather than a reference
npx @servicenow/sdk explain business-rule-guide
npx @servicenow/sdk explain security-guide
npx @servicenow/sdk explain keys-file
# quick summary when you only need to remember the shape
npx @servicenow/sdk explain scriptinclude-api --peek
# pipe clean markdown into a file, or into an AI assistant
npx @servicenow/sdk explain ci-integration --format raw > ci-notes.md
4.2 Searching works on keywords, not just names
Every topic carries a keyword list, so you can search by what you are trying to do rather than by the artefact name. Topics are tagged with ServiceNow table names, UI labels and synonyms — acl-api is findable via access control, security, permission and sys_security_acl. Try the thing you would type into the filter navigator; it usually hits.
explain --list <keyword> is your index. Run this before writing any Fluent artefact you have not written before. It is faster than searching the web and it matches the exact SDK version you have installed.4.3 The AI-assistant angle
This is the single most valuable thing in this document for anyone pairing the SDK with Cursor. Fluent's API surface is large and moves between versions, so a model answering from memory will confidently invent property names. explain --format raw emits clean markdown designed to be piped — which means you can ground the assistant in the exact API of your exact version:
# give the assistant the real API before asking it to write code
npx @servicenow/sdk explain scheduledscript-api --format raw | clip # Windows
npx @servicenow/sdk explain scheduledscript-api --format raw | pbcopy # macOS
Then paste it into the chat with "use this exact API". The repository's own Cursor rule already encodes this instinct — rule 2 in .cursor/rules/servicenow-sdk-master.mdc says to run explain when the API is uncertain. That rule is correct and worth keeping.
Validation for §4: npx @servicenow/sdk explain --list | Measure-Object -Line reports roughly 256 topic lines, and explain keys-file prints a document explaining why keys.ts must be committed — which is exactly what §5.3 covers next.
5Project anatomy
Developer · the five things that matter
5.1 Layout of a real project
This is hr-leave-sdk exactly as it exists in this repository:
hr-leave-sdk/
├─ now.config.json # scope, scopeId, app name, where Fluent lives
├─ package.json # pinned SDK version + npm scripts
├─ package-lock.json # 481 KB — commit it
├─ .gitignore
├─ .now/
│ └─ bom.json # bill of materials the build maintains
├─ src/
│ └─ fluent/ # ← everything you write lives here
│ ├─ tables.now.ts
│ ├─ roles.now.ts
│ ├─ acls.now.ts
│ ├─ script-includes.now.ts
│ ├─ business-rules.now.ts
│ ├─ client-scripts.now.ts
│ ├─ events.now.ts
│ ├─ notifications.now.ts
│ ├─ catalog.now.ts
│ ├─ properties.now.ts
│ ├─ menu.now.ts
│ ├─ flows/
│ │ └─ leave-approval.now.ts
│ └─ generated/
│ └─ keys.ts # ← GENERATED, BUT COMMIT IT. See 5.3.
├─ dist/ # build output: XML update records (do not commit)
└─ target/
└─ hr_leave_sdk_1_0_0.zip # packaged artefact, 114 KB (do not commit)
5.2 now.config.json
{
"$schema": "https://www.servicenow.com/sdk/now.config.schema.json",
"scope": "x_786686_hr_leave",
"scopeId": "e5f5bc9493a303507520703efaba104d",
"name": "HR Leave",
"fluentDir": "src/fluent",
"clientDir": ""
}
scope | The application scope. Maximum 18 characters, set at init time. |
scopeId | The sys_id of the application record on the instance. This is what ties your local project to a specific installed app. |
name | Display name. |
fluentDir | Where the build looks for *.now.ts files. |
clientDir | Client-side bundle directory, used by the UI templates. Empty here because this app has no React/Vue front end. |
| dependencies | Not present in this project, but this is the file where now-sdk dependencies --add records them. |
Run npx @servicenow/sdk explain now-config-reference for the full schema of every key this file accepts.
5.3 keys.ts — the most important file in the project
Every Fluent record can carry a $id, written as Now.ID['some_identifier']. The build maps each of those identifiers to a concrete ServiceNow sys_id and writes the mapping into src/fluent/generated/keys.ts as a TypeScript declaration:
// src/fluent/generated/keys.ts — generated, ~1,340 lines in this project
import '@servicenow/sdk/global'
declare global {
namespace Now {
namespace Internal {
interface Keys extends KeysRegistry {
explicit: {
hrlx_acl_approver_write: {
table: 'sys_security_acl'
id: 'a6b167fdbf204118a048cc5741627408'
}
hrlx_acl_audit_create: {
table: 'sys_security_acl'
id: 'c0cbf0ef04424544aa0c65b562ea6a2a'
}
// … one entry per identified record
}
}
}
}
}
It says "generated", and you must still commit it. This trips up every team exactly once, and the damage is not obvious for weeks. Here is the failure chain, straight from the SDK's own ci-integration guide:
- You add a new business rule with
$id: Now.ID['my_rule']and build. A fresh sys_id is generated and written intokeys.ts. - You commit the business rule but not
keys.ts, because "it's generated". - A teammate builds.
Now.ID['my_rule']resolves to a different freshly-generated sys_id on their machine. - ServiceNow identifies records by sys_id. So their install creates a duplicate record instead of updating yours. The target instance is now corrupted.
- Once a stale
keys.tsreachesmain, every later branch inherits the wrong IDs.
The guard is now-sdk build --frozenKeys in CI: it fails the build if keys.ts changed during it, which means someone forgot to commit. The developer's fix is trivial — run build locally, commit keys.ts, push. See §9.
5.4 What to commit and what to ignore
| Path | Commit? | Why |
|---|---|---|
src/fluent/** | Yes | Your source. |
src/fluent/generated/keys.ts | Yes | Record identity. See 5.3. |
now.config.json | Yes | Scope binding. |
package.json · package-lock.json | Yes | Reproducible SDK version. |
.now/bom.json | Yes | Bill of materials the build tracks. |
dist/ | No | Build output. Regenerated every build. |
target/ | No | Packaged zip. Attach to a release instead. |
node_modules/ | No | Obviously. |
6Full tutorial — build the HR Leave app in Fluent
Developer · 60–90 minutes · every snippet is real source from hr-leave-sdk
We build the same application the Studio chapters build, but as code. Follow in order; each file imports the one before it.
6.0 Create the project
mkdir hr-leave-sdk; cd hr-leave-sdk
npm init -y
npm install -D @servicenow/[email protected]
npx @servicenow/sdk auth --add https://dev398980.service-now.com --type basic --alias DEV
npx @servicenow/sdk init --appName "HR Leave" --packageName hr-leave-sdk `
--scopeName x_786686_hr_leave --auth DEV
npx @servicenow/sdk dependencies --auth DEV # get the type definitions
6.1 Tables — src/fluent/tables.now.ts
Four tables. Note the shared leaveTypeChoices object reused across two tables — the kind of deduplication that is impossible in Studio and free in code.
import {
Table,
StringColumn,
IntegerColumn,
ReferenceColumn,
DateColumn,
ChoiceColumn,
} from '@servicenow/sdk/core'
const leaveTypeChoices = {
annual: { label: 'Annual', sequence: 1 },
sick: { label: 'Sick', sequence: 2 },
unpaid: { label: 'Unpaid', sequence: 3 },
}
export const x_786686_hr_leave_employee_v2 = Table({
name: 'x_786686_hr_leave_employee_v2',
label: 'HR Employee',
schema: {
u_user: ReferenceColumn({ label: 'User', referenceTable: 'sys_user' }),
u_employee_id: StringColumn({ label: 'Employee ID', maxLength: 40 }),
u_department: StringColumn({ label: 'Department', maxLength: 100 }),
u_manager: ReferenceColumn({ label: 'Manager', referenceTable: 'sys_user' }),
u_join_date: DateColumn({ label: 'Join Date' }),
u_grade: StringColumn({ label: 'Grade', maxLength: 20 }),
},
})
export const x_786686_hr_leave_req_v2 = Table({
name: 'x_786686_hr_leave_req_v2',
label: 'Leave Request',
schema: {
u_number: StringColumn({ label: 'Number', maxLength: 40 }),
u_requested_for_user: ReferenceColumn({ label: 'Requested For', referenceTable: 'sys_user' }),
u_employee_ref: ReferenceColumn({
label: 'Employee',
referenceTable: 'x_786686_hr_leave_employee_v2',
}),
u_leave_type: ChoiceColumn({ label: 'Leave Type', choices: leaveTypeChoices }),
u_start_date: DateColumn({ label: 'Start Date' }),
u_end_date: DateColumn({ label: 'End Date' }),
u_days_count: IntegerColumn({ label: 'Days' }),
u_state: ChoiceColumn({
label: 'State',
choices: {
draft: { label: 'Draft', sequence: 1 },
submitted: { label: 'Submitted', sequence: 2 },
approved: { label: 'Approved', sequence: 3 },
rejected: { label: 'Rejected', sequence: 4 },
cancelled: { label: 'Cancelled', sequence: 5 },
},
}),
u_approver_user: ReferenceColumn({ label: 'Approver', referenceTable: 'sys_user' }),
u_reason: StringColumn({ label: 'Reason', maxLength: 1000 }),
},
})
export const x_786686_hr_leave_bal_v2 = Table({
name: 'x_786686_hr_leave_bal_v2',
label: 'Leave Balance',
schema: {
u_employee_ref: ReferenceColumn({
label: 'Employee',
referenceTable: 'x_786686_hr_leave_employee_v2',
}),
u_year: IntegerColumn({ label: 'Year' }),
u_leave_type: ChoiceColumn({ label: 'Leave Type', choices: leaveTypeChoices }),
u_entitled: IntegerColumn({ label: 'Entitled' }),
u_used: IntegerColumn({ label: 'Used' }),
u_remaining: IntegerColumn({ label: 'Remaining' }),
},
})
export const x_786686_hr_leave_audit_v2 = Table({
name: 'x_786686_hr_leave_audit_v2',
label: 'Leave Audit',
schema: {
u_leave_ref: ReferenceColumn({
label: 'Leave',
referenceTable: 'x_786686_hr_leave_req_v2',
}),
u_old_state: StringColumn({ label: 'Old State', maxLength: 40 }),
u_new_state: StringColumn({ label: 'New State', maxLength: 40 }),
u_event_type: StringColumn({ label: 'Event Type', maxLength: 60 }),
u_actor: ReferenceColumn({ label: 'Actor', referenceTable: 'sys_user' }),
},
})
Column types available include StringColumn, IntegerColumn, DecimalColumn, FloatColumn, BooleanColumn, DateColumn, DateTimeColumn, DurationColumn, ChoiceColumn, ReferenceColumn, DocumentIdColumn, HtmlColumn, JsonColumn, ScriptColumn, UrlColumn, EmailColumn, Password2Column, ListColumn, GuidColumn and about forty more. Run npx @servicenow/sdk explain --list column for the full set on your version.
6.2 Roles — src/fluent/roles.now.ts
import { Role } from '@servicenow/sdk/core'
export const roleEmployee = Role({
name: 'x_786686_hr_leave.employee',
description: 'Submit and view own leave requests',
grantable: true,
})
export const roleManager = Role({
name: 'x_786686_hr_leave.manager',
description: 'Approve team leave requests',
grantable: true,
})
export const roleHrAdmin = Role({
name: 'x_786686_hr_leave.hr_admin',
description: 'Full HR Leave administration',
grantable: true,
})
export const roleIntegration = Role({
name: 'x_786686_hr_leave.integration',
description: 'API / integration leave create access',
grantable: true,
})
These are plain TypeScript exports, which is what makes the next file type-safe.
6.3 ACLs — src/fluent/acls.now.ts
Here is the first real payoff of code over Studio: ACLs import the role objects rather than referencing role names as strings. Rename a role and the compiler finds every ACL that used it.
import { Acl } from '@servicenow/sdk/core'
import { roleEmployee, roleHrAdmin, roleIntegration, roleManager } from './roles.now'
Acl({
$id: Now.ID['hrlx_acl_req_read'],
type: 'record',
table: 'x_786686_hr_leave_req_v2',
operation: 'read',
adminOverrides: true,
active: true,
description: 'Read leave via security utils',
script: `answer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canReadLeave(current);`,
})
Acl({
$id: Now.ID['hrlx_acl_req_write'],
type: 'record',
table: 'x_786686_hr_leave_req_v2',
operation: 'write',
adminOverrides: true,
active: true,
description: 'Write leave via security utils',
script: `answer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canWriteLeave(current);`,
})
Acl({
$id: Now.ID['hrlx_acl_req_create'],
type: 'record',
table: 'x_786686_hr_leave_req_v2',
operation: 'create',
adminOverrides: true,
active: true,
roles: [roleEmployee, roleHrAdmin, roleIntegration], // ← typed references
description: 'Create leave for employee/hr/integration',
script: `answer = gs.hasRole('x_786686_hr_leave.employee')
|| gs.hasRole('x_786686_hr_leave.hr_admin')
|| gs.hasRole('x_786686_hr_leave.integration');`,
})
Acl({
$id: Now.ID['hrlx_acl_req_delete'],
type: 'record',
table: 'x_786686_hr_leave_req_v2',
operation: 'delete',
adminOverrides: true,
active: true,
roles: [roleHrAdmin],
description: 'Delete leave HR admin only',
})
// field-level ACL: only HR admin may change the approver
Acl({
$id: Now.ID['hrlx_acl_approver_write'],
type: 'record',
table: 'x_786686_hr_leave_req_v2',
field: 'u_approver_user',
operation: 'write',
adminOverrides: true,
active: true,
roles: [roleHrAdmin],
description: 'Approver field writeable by HR admin',
script: `answer = gs.hasRole('x_786686_hr_leave.hr_admin');`,
})
The real file continues with read/write/create/delete for the employee, balance and audit tables — sixteen ACLs in total, each with a stable $id. Writing sixteen ACLs in Studio is an afternoon of clicking; here it is a file you can review in two minutes and diff forever.
6.4 Business rules — src/fluent/business-rules.now.ts
The server script is a template literal. Keep it as the same JavaScript you would paste into Studio, so the code is portable in both directions.
import { BusinessRule } from '@servicenow/sdk/core'
export const BR1_calculate_days = BusinessRule({
$id: Now.ID['hrlx_br1_calculate_days'],
name: 'HRLX BR1 Calculate Days',
table: 'x_786686_hr_leave_req_v2',
when: 'before',
action: ['insert', 'update'],
order: 100,
active: true,
script: `(function executeRule(current, previous /*null when inserting*/) {
var util = new x_786686_hr_leave.HRLXProLeaveUtilsV2();
if (current.u_start_date.changes() || current.u_end_date.changes() || current.isNewRecord()) {
current.u_days_count = util.calculateBusinessDays(
current.getValue('u_start_date'),
current.getValue('u_end_date')
);
}
if (current.u_employee_ref.changes() || current.isNewRecord()) {
util.setApproverFromEmployee(current);
}
})(current, previous);`,
})
export const BR2_on_submit_event = BusinessRule({
$id: Now.ID['hrlx_br2_on_submit_event'],
name: 'HRLX BR2 On Submit Event',
table: 'x_786686_hr_leave_req_v2',
when: 'after',
action: ['insert', 'update'],
order: 200,
active: true,
script: `(function executeRule(current, previous) {
if (current.u_state.changesTo('submitted')) {
gs.eventQueue(
'x_786686_hr_leave.leave_v2.submitted',
current,
current.getValue('u_approver_user'),
current.getValue('u_number')
);
}
})(current, previous);`,
})
export const BR3_audit_state_change = BusinessRule({
$id: Now.ID['hrlx_br3_audit_state_change'],
name: 'HRLX BR3 Audit State Change',
table: 'x_786686_hr_leave_req_v2',
when: 'after',
action: ['update'],
order: 300,
active: true,
script: `(function executeRule(current, previous) {
if (!current.u_state.changes()) return;
var util = new x_786686_hr_leave.HRLXProLeaveUtilsV2();
util.auditStateChange(
current.getUniqueValue(),
previous ? previous.getValue('u_state') : '',
current.getValue('u_state'),
'state_change'
);
if (current.u_state.changesTo('approved') && current.u_employee_ref) {
var year = new GlideDateTime(current.getValue('u_start_date')).getYearLocalTime();
util.recalculateBalance(current.getValue('u_employee_ref'), year);
}
})(current, previous);`,
})
order is explicit
100, 200, 250, 300. In Studio these get set and forgotten; in one file the execution sequence is readable at a glance.
Logic lives in Script Includes
The rules call HRLXProLeaveUtilsV2 rather than inlining the maths. The rule stays reviewable and the logic stays unit-testable.
Scoped class references
new x_786686_hr_leave.HRLXProLeaveUtilsV2() — the scope prefix is required from a scoped business rule.
$id on every record
Without it the record gets a new sys_id on another machine. With it, identity is pinned in keys.ts.
6.5 Build and install
npx @servicenow/sdk build
npx @servicenow/sdk install --auth DEV --open-browser
git status shows keys.ts modified after a build, that change is part of your commit, not noise to discard.6.6 Verify on the instance
Do not trust "install succeeded" — check the instance, ideally without leaving the terminal:
# the app record
npx @servicenow/sdk query sys_app -q "scope=x_786686_hr_leave" `
-f name,scope,version --display-value all --auth DEV
# did all four business rules land, and in the right order?
npx @servicenow/sdk query sys_script -q "sys_scope.scope=x_786686_hr_leave^ORDERBYorder" `
-f name,table,when,order,active --auth DEV
# all sixteen ACLs?
npx @servicenow/sdk query sys_security_acl -q "sys_scope.scope=x_786686_hr_leave" `
-f name,operation,admin_overrides --auth DEV --limit 50
Or open the forms directly — replace {BASE} with your instance:
6.7 The daily loop
cd C:\SVCProject\HR_Leave_App\servicenow-ai-workspace\hr-leave-sdk
# 1. edit a .now.ts file
# 2. build
npx @servicenow/sdk build
# 3. install
npx @servicenow/sdk install --auth DEV
# 4. verify
npx @servicenow/sdk query x_786686_hr_leave_req_v2 -q "" --limit 3 --auth DEV
# 5. commit — INCLUDING keys.ts if it changed
git add -A; git commit -m "BR5: block overlapping leave requests"
Validation for §6: build and install both exit 0; query sys_app returns your app with the expected version; the four business rules come back in order 100/200/250/300; and git status is clean after you commit keys.ts.
7Cursor integration
Developer · why the SDK and an AI editor belong together
7.1 The actual reason this pairing works
An AI assistant cannot help you fill in a Studio form. It can help enormously with a TypeScript file it can read, edit and type-check. Moving platform development into source is what makes AI assistance possible at all — that, not any Cursor feature, is the whole argument.
The second-order effect matters just as much: because the SDK ships explain, you can feed the assistant the exact API of your exact version instead of letting it guess from training data. A grounded assistant writing Fluent is genuinely productive; an ungrounded one invents plausible property names that fail at build time.
7.2 Open the workspace
cursor C:\SVCProject\HR_Leave_App\servicenow-ai-workspace
Open the workspace root, not a single app folder, so the assistant can see all three projects and the shared rules.
7.3 The rules file
This project already has .cursor/rules/servicenow-sdk-master.mdc. Rules files use front matter to control when they apply:
---
description: ServiceNow SDK Fluent — Cursor Agent rules (HR + CRM + Portal)
globs: "**/*.now.ts,**/now.config.json,**/package.json"
alwaysApply: true
---
# ServiceNow SDK + Cursor (Auto / Agent mode)
## Before any Fluent code
1. Read `package.json` for `@servicenow/sdk` version.
2. Run `npx @servicenow/sdk explain "<ArtifactType>"` when API is uncertain.
3. After edits: `npx @servicenow/sdk build`.
## Scopes (three apps in this workspace)
- **HR Leave:** `x_786686_hr_leave` — tables suffix `_v2`, SI prefix `HRLX`
- **CRM Suite:** `x_snc_crm_suite` — SI prefix `CRM`
- **Portal Website:** `x_snc_sp_website` — widgets in `portal-website-sdk/src/portal/`
## Conventions
- Custom fields: `u_` prefix only.
- Server scripts: `getValue()` / `setValue()`, `gs.nil()`, `setLimit()` on queries.
- User text: `gs.getMessage()`. Logging: `gs.info('[scope] ...')`.
- Never hard-code sys_ids. Never commit passwords or API keys.
- Windows terminal: PowerShell commands only.
- Always commit `src/fluent/generated/keys.ts` when a build changes it.
## Deploy (ask user before production)
- Dev PDI: `npx @servicenow/sdk install --auth DEV`
- All three: `.\scripts\deploy-all.ps1`
Two edits applied above versus the file currently in the repo: step 3 no longer tells the agent to run npx @servicenow/sdk validate (that command does not exist — §10), and a line about committing keys.ts was added, because an agent that discards "generated" files will cause the duplicate-record failure described in §5.3.
7.4 The four rules that pay for themselves
Explain before writing
Ground the model in the real API for any artefact type it has not written in this session. One command, zero invented properties.
Build after every edit
The build is the type check. An agent that edits three files and never builds leaves you to find the errors.
Never hard-code a sys_id
Use Now.ID['identifier']. A hard-coded sys_id works on one instance and breaks silently on the next.
Ask before any install
install mutates a live instance and --reinstall destroys data. Treat it as a human decision, always.
7.5 A workflow that actually works
- Describe the artefact, not the code. "Add a business rule on
req_v2that blocks submitting a request overlapping an existing approved one for the same employee" beats pasting half an implementation. - Point at the pattern. "Follow the style of
BR3_audit_state_changeinbusiness-rules.now.ts." The existing file is better context than any prompt you can write. - Make it ground itself. "Run
explain businessrule-apifirst and use that exact API." - Require a build. "Then run the build and fix anything it reports."
- Review the diff, not the chat. Judge the
.now.tschange in the editor. The explanation in the chat window is not the deliverable. - Install yourself. You run
install --auth DEV, after reading the diff.
7.6 Give the assistant live instance access
Two options, in increasing order of power:
| Approach | Setup | What it unlocks |
|---|---|---|
now-sdk query in the terminal | Nothing — it is already installed | The agent can run a query and read the result as terminal output. Enough for most debugging. |
| An MCP server over the instance | See Part 8 of the AI full-demo guide | Structured, allow-listed tools the assistant calls directly, with ACL enforcement and an audit trail on the instance. |
Start with query. It costs nothing, it is already authenticated through your stored alias, and it covers "what does the instance actually look like right now" — which is 90% of what the assistant needs.
7.7 What to keep out of the repository
Credentials live in the OS keychain via auth --add. They must never appear in .cursor/rules, now.config.json, a chat message, or a committed .env. If you need the assistant to make an authenticated call, have it use --auth DEV — the alias, never the secret behind it.
8Bringing existing applications into the SDK
Developer · you do not need a greenfield app
Three commands cover adoption, and they are not interchangeable:
| Command | Use it to | Scale |
|---|---|---|
init --from <sys_id> | Convert a whole legacy scoped application into a new Fluent project in one shot. | Whole app, once |
transform --table <tables> | Convert specific artefact types into Fluent source inside an existing project. | Incremental, repeatable |
download <dir> | Pull the instance's current metadata to inspect it. Does not produce Fluent source. | Diagnostic |
8.1 The low-risk adoption sequence
- Step 1 On a clone of your instance, not the real one, run
init --from <app sys_id> --auth DEV. - Step 2 Build immediately:
npx @servicenow/sdk build. Whatever fails to compile is your conversion gap list. - Step 3 Install onto a second clean instance and compare against the original. This is the only honest test that the conversion is faithful.
- Step 4 Commit everything, including
keys.ts, before touching a single line. - Step 5 Only now declare the app SDK-managed and tell the team to stop editing it in Studio.
8.2 Incremental adoption with transform
# week 1: bring the security model under source control
npx @servicenow/sdk transform --table sys_security_acl,sys_user_role --auth DEV
# week 2: server logic
npx @servicenow/sdk transform --table sys_script,sys_script_include --auth DEV
# week 3: client side
npx @servicenow/sdk transform --table sys_script_client,sys_ui_policy --auth DEV
# convert an update set XML you exported earlier — no instance required
npx @servicenow/sdk transform --from ./exports/HR_Leave_Build.xml
Transform one artefact type per pull request. A single commit that converts an entire application is unreviewable, and conversion issues are exactly the kind you want a reviewer to catch.
9CI/CD done correctly
DevOps · from the SDK's own ci-integration topic
9.1 The two things CI must do
- Gate on
--frozenKeys. Catch the uncommittedkeys.tsbefore merge, not after it corrupts an instance. - Authenticate without a keychain. CI has no OS keychain, so the CLI reads credentials from environment variables instead — but only when you explicitly switch it into CI mode.
9.2 CI mode and its environment variables
CI mode is gated by a single variable. Without it the CLI looks for a keychain and fails:
SN_SDK_NODE_ENV | Must be exactly SN_SDK_CI_INSTALL. This is the switch. |
SN_SDK_AUTH_TYPE | oauth (recommended) or basic. Unset defaults to basic. Any other value is rejected before any network call. |
SN_SDK_INSTANCE_URL | Full instance URL. |
SN_SDK_OAUTH_CLIENT_ID | OAuth Application Registry client_id. Required when type is oauth. |
SN_SDK_OAUTH_CLIENT_SECRET | OAuth client_secret. Required when type is oauth. |
SN_SDK_USER · SN_SDK_USER_PWD | Used only when type is basic. |
Prefer OAuth. The client secret authenticates the CI system itself rather than impersonating a person, so a leak can be rotated without touching anyone's account, and there is no live user password sitting in a pipeline variable for a compromised job to exfiltrate.
9.3 Instance-side OAuth setup — all three steps are mandatory
- Service user. Must have the
adminrole, and must have Identity Type = Human on itssys_userrecord. The SDK's installer processor rejects non-human accounts. - Enable the grant type. The system property
glide.oauth.inbound.client.credential.grant_type.enabledmust exist and betrue. Create it insys_propertiesif it is not there. - Register the OAuth client in the Application Registry and map the service user from step 1 as the OAuth application user.
The CLI calls ${SN_SDK_INSTANCE_URL}/oauth_token.do once at startup with grant_type=client_credentials and uses the returned token for everything after. There is no refresh token in this grant; each run gets a fresh token.
9.4 A corrected GitHub Actions workflow
The workflow currently in this repository will not run — it calls validate and passes auth flags that do not exist. Here is the working version:
name: Deploy ServiceNow Apps
on:
push:
branches: [main, develop]
paths: ['HR_Leave_App/servicenow-ai-workspace/**']
workflow_dispatch:
jobs:
build-and-install:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
app: [hr-leave-sdk, crm-suite-sdk, portal-website-sdk]
defaults:
run:
working-directory: HR_Leave_App/servicenow-ai-workspace/${{ matrix.app }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
# The gate. Fails if someone forgot to commit keys.ts.
- name: Build and verify keys.ts is committed
run: npx @servicenow/sdk build --frozenKeys
# Install to the TEST instance on main only. Never to production.
- name: Install to test instance
if: github.ref == 'refs/heads/main'
env:
SN_SDK_NODE_ENV: SN_SDK_CI_INSTALL
SN_SDK_AUTH_TYPE: oauth
SN_SDK_INSTANCE_URL: ${{ secrets.SN_INSTANCE_URL }}
SN_SDK_OAUTH_CLIENT_ID: ${{ secrets.SN_OAUTH_CLIENT_ID }}
SN_SDK_OAUTH_CLIENT_SECRET: ${{ secrets.SN_OAUTH_CLIENT_SECRET }}
run: npx @servicenow/sdk install
npm ci, not npm install
Installs exactly the lockfile. npm install can quietly drift the SDK version between runs.
No auth --add step
In CI mode credentials come from the environment. Calling auth --add in a pipeline is the wrong model entirely.
No --auth flag on install
With SN_SDK_NODE_ENV set, the CLI uses the env credentials. The alias is a keychain concept.
Test, never production
Production goes through cicd publish and the App Repository so change management and rollback work properly.
9.5 A corrected deploy-all.ps1
# Deploy all three SDK apps to a dev PDI (run from the workspace root)
param(
[string]$AuthAlias = "DEV"
)
$ErrorActionPreference = "Stop"
$root = Split-Path $PSScriptRoot -Parent # scripts/ lives one level below the root
$apps = @("hr-leave-sdk", "crm-suite-sdk", "portal-website-sdk")
foreach ($app in $apps) {
Write-Host "`n========== $app ==========" -ForegroundColor Cyan
Push-Location (Join-Path $root $app)
npm ci
if ($LASTEXITCODE -ne 0) { Pop-Location; throw "npm ci failed: $app" }
npx @servicenow/sdk build
if ($LASTEXITCODE -ne 0) { Pop-Location; throw "Build failed: $app" }
npx @servicenow/sdk install --auth $AuthAlias
if ($LASTEXITCODE -ne 0) { Pop-Location; throw "Install failed: $app" }
Pop-Location
}
Write-Host "`nAll 3 apps installed on '$AuthAlias'." -ForegroundColor Green
Three fixes versus the current script: the validate step is gone, $root now resolves to the workspace root instead of the scripts/ folder it was pointing at, and Pop-Location runs before each throw so a failure does not leave your shell in the wrong directory.
10Corrections — four files in this repository are wrong
Found by diffing the repo against the installed CLI
Verifying this documentation against SDK 4.12.2 surfaced real defects in files that are already in the project. They are not cosmetic: seven files call a command that does not exist, and that command sits in the middle of every deploy path this workspace has.
10.1 The validate problem — all seven call sites
npx @servicenow/sdk validate returns Unknown command: validate and exits non-zero. Every one of these runs it:
| # | File | Line | Consequence |
|---|---|---|---|
| 1 | scripts/deploy-all.ps1 | 16–17 | Hard failure. Guarded by if ($LASTEXITCODE -ne 0) { throw "Validate failed: $app" }, so the script throws on the first app. It has never reached a deploy. |
| 2 | .github/workflows/deploy-all-apps.yml | 34–35 | Hard failure. A non-zero step fails the job. The pipeline has never deployed anything. |
| 3 | .cursor/rules/servicenow-sdk-master.mdc | 12 | Tells the Cursor agent to run it after every build. The agent runs a failing command, then tries to debug the failure. |
| 4 | .claude/commands/deploy.md | 11 | Same, as step 4 of the deploy command. |
| 5 | .claude/skills/deploy-to-pdi.md | 3, 12 | Same, plus the skill's own description promises "Build, validate, and deploy". |
| 6 | crm-suite-sdk/package.json | 8 | "validate": "now-sdk validate" — npm run validate always fails. |
| 7 | portal-website-sdk/package.json | 8 | Same script. (hr-leave-sdk/package.json is clean — it has no validate script.) |
The fix is the same everywhere: delete the validate step. Validation already happens inside build. Where a stricter gate is wanted, that is build --frozenKeys --errorOnConflict, not a separate command.
10.2 Three more defects
| File | The problem | The fix |
|---|---|---|
.github/workflows/deploy-all-apps.yml line 44 |
auth --add $env:SN_INSTANCE --alias ci --oauth --client-id ... --client-secret .... None of --oauth, --client-id or --client-secret exist. Beyond the flags, the whole approach is wrong: CI has no OS keychain for auth --add to write to. |
Drop the auth step entirely and pass the SN_SDK_* environment variables on the install step. Working workflow in §9.4. |
scripts/deploy-all.ps1 line 7 |
$root = $PSScriptRoot resolves to the scripts/ folder, so Join-Path $root "hr-leave-sdk" points at scripts/hr-leave-sdk, which does not exist. The apps are one level up. Separately, each throw fires without a Pop-Location, leaving your shell in the wrong directory after any failure. |
$root = Split-Path $PSScriptRoot -Parent, and pop before throwing. Corrected script in §9.5. |
html/AI_SERVICENOW_FULL_DEMO_MASTER.html Part 10 |
Command table written from general knowledge rather than the CLI. It lists upgrade and build --watch, which do not exist, and omits download, clean, pack, explain, query and cicd. |
A pointer to this page has been added there. Treat §3 of this document as the reference. |
Worth noticing why this happened at all: validate is a perfectly reasonable thing for a CLI to have, which is exactly why it propagated into seven files unchallenged. Every one of them was written from what the SDK ought to look like. That is the argument for explain and --help over recollection, and it applies to humans as much as to assistants.
10.3 The commands that do not exist in 4.12.2
| Not a command | What people mean | What to run instead |
|---|---|---|
validate | Check the project compiles | build — validation happens inside it. For CI strictness add --frozenKeys and --errorOnConflict. |
upgrade | Move to a newer SDK | Edit the version in package.json, then npm install. Re-read --help afterwards; flags change. |
build --watch | Rebuild on save | No such flag. Use nodemon --watch src/fluent -e ts --exec "npx @servicenow/sdk build". |
auth --oauth | OAuth credential | auth --add <url> --type oauth --alias <name>. |
deploy --reinstall | Force a clean install | Valid — deploy is an alias of install and -r/--reinstall is real. Just know it is destructive. |
How to keep this page honest. After any SDK upgrade, re-run npx @servicenow/sdk --help and npx @servicenow/sdk <command> --help for each of the twelve commands, and update §3. That is the whole maintenance procedure, and it takes five minutes.
11Troubleshooting
Symptom first — find your error message in the left column
| What you see | Cause | Fix |
|---|---|---|
Unknown command: validate |
No such command in 4.12.2. | Use build. See §10 — seven files in this repo need this change. |
MODULE_NOT_FOUND pointing at bin/now-sdk.js |
Guessed binary path. The real entry point is node_modules/@servicenow/sdk/bin/index.js. |
Call it as npx @servicenow/sdk or npx now-sdk and let npm resolve the bin map. |
now-sdk: command not found |
Installed as a dev dependency, not globally — which is correct. | Prefix with npx, or add an npm script. Never install the SDK globally; the version must be pinned per project. |
Build fails with keys.ts in the diff after --frozenKeys |
Someone added or changed a $id and did not commit the regenerated keys file. |
Run build locally without the flag, git add src/fluent/generated/keys.ts, commit, push. §5.3. |
| Install creates duplicate records instead of updating | Stale or missing keys.ts. Your $id resolved to a different sys_id than the one on the instance. |
Restore the correct keys.ts from the branch that last installed successfully, rebuild, reinstall. Delete the duplicates on the instance by hand. |
| Records vanished after an install | --reinstall. It uninstalls first, so anything created on the instance but absent from your source is gone. |
Restore from a backup or update set. Then remove --reinstall from whatever script used it. |
| Auth fails in CI but works locally | CI has no OS keychain, and SN_SDK_NODE_ENV was not set, so the CLI never looked at the environment variables. |
Set SN_SDK_NODE_ENV=SN_SDK_CI_INSTALL plus the rest of the SN_SDK_* set. §9.2. |
| OAuth token request rejected | One of the three mandatory instance-side steps is missing — most often the property, which is not present by default. | Confirm glide.oauth.inbound.client.credential.grant_type.enabled = true, the service user has admin, and its Identity Type is Human. §9.3. |
Scope name too long at init |
Scope limit is 18 characters including the x_ prefix and vendor code. |
Shorten it. x_786686_hr_leave is 17 — that is the budget you are working with. |
| A Fluent property is rejected that you are sure exists | Property names differ between SDK versions, and model-generated code is a common source of plausible-but-wrong names. | npx @servicenow/sdk explain <artifact>-api. That output is your version's truth, not the docs site and not a chat window. |
| Changes in Studio disappear after the next install | Working as designed. Install makes the instance match your source. | Pick one owner per artefact type. If something must be edited in Studio, transform it back into source before your next install. §1. |
| Install hangs or returns before finishing | --async defaults to true. The command returns while the instance is still working. |
Pass --async false when a later step depends on the install being complete. |
| Unexpected demo data on a clean instance | --demoData also defaults to true. |
install --demoData false. |
Stale artefacts in dist/ after renaming a file |
Output from the deleted source is still on disk. | npx @servicenow/sdk clean, then rebuild. |
11.1 When nothing above matches
npx @servicenow/sdk <command> --help— confirm the flag you used is real on your version.npx @servicenow/sdk explain faq— the SDK's own FAQ, offline.npx @servicenow/sdk --debug <command>— verbose logging.npx @servicenow/sdk query sys_update_xml -q "sys_created_on>javascript:gs.hoursAgo(1)" --auth DEV— see what the last install actually wrote.
12Appendix — all 256 explain topics
Every topic available offline on SDK 4.12.2 · captured from your CLI
Read any of these with npx @servicenow/sdk explain <topic>. Add --format raw to get markdown suitable for piping into an assistant (§4).
Where to start
If you read four topics in your whole career with this SDK, read these:
fluent-overviewfluent-language-overviewkeys-fileci-integrationCore & reference 7
Start here. These seven are the SDK explaining itself.
ci-integrationfaqfluent-language-overviewfluent-overviewkeys-filenow-config-referenceservice-portal-ootb-referenceGuides 97
How and why — patterns, trade-offs, worked examples.
alias-guidealias-template-guideapplication-menu-guideassessment-guideassignment-rule-guideatf-guideatf-ui-test-script-guideauth-token-guidebuilding-ai-agents-advanced-guidebuilding-ai-agents-guidebuilding-ai-agents-tools-guidebusiness-rule-guidechoiceset-guideclient-script-guideclient-side-api-guidecreating-workspaces-guidecross-scope-privilege-guidedashboard-filters-guidedashboard-guidedata-helpers-guidedata-lookup-guidedata-policy-guidedependencies-guidedeveloping-apps-guideemail-notification-guideencoded-query-guideexternal-services-guidefield-styles-guideform-formatters-guideform-layout-guidegraphql-api-guideimporting-data-guideinbound-email-action-guideinstance-scan-guidejavascript-compatibility-guideknowledge-base-access-guidelist-control-guidelist-guidemodule-guidenow-attach-guidenow-del-guidenow-id-guidenow-include-guidenow-ref-guidenowassist-skills-advanced-guidenowassist-skills-guidenowassist-skills-tools-guidenpm-libraries-guideoverride-column-guideoverride-guideplaybook-activities-guideplaybook-anti-patterns-guideplaybook-datapills-guideplaybook-guideplaybook-lanes-guideplaybook-patterns-guideplaybook-permissions-guideplaybook-triggers-guideplaybook-unsupported-features-guideproperty-guidequery-guideregistering-events-guiderelationship-guiderest-message-guideretry-policy-guiderisk-assessment-guidescheduled-script-guidescript-include-guidescripted-rest-api-guidesdlc-guidesecurity-guideservice-catalog-client-script-guideservice-catalog-guideservice-catalog-ui-policy-guideservice-catalog-variables-guideservice-portal-advanced-guideservice-portal-components-guideservice-portal-guidestate-model-guidetable-augments-guidetable-guideui-action-guideui-page-guideui-page-patterns-guideui-page-theming-guideui-policy-guideuser-criteria-examples-guideuser-criteria-guideview-guideview-rule-guidewfa-custom-action-guidewfa-flow-actions-guidewfa-flow-guidewfa-flow-logic-guidewfa-flow-stages-guidewfa-subflow-guidewfa-trigger-guideAPI references 152
Exact Fluent signatures, every property, every default.
acl-apiaction-apiaiagent-apiaiagenticworkflow-apialias-apialiastemplate-apiapplicability-apiapplicationmenu-apiapprovalrulescolumn-apiassessment-apiattachmentvariable-apibasicdatetimecolumn-apibasicimagecolumn-apibooleancolumn-apibreakvariable-apibusinessrule-apicalendardatetimecolumn-apicatalogclientscript-apicatalogitem-apicatalogitemrecordproducer-apicataloguipolicy-apicheckboxvariable-apichoicecolumn-apichoiceset-apiclientscript-apicolumntypecheck-apiconditionscolumn-apicontainerendvariable-apicontainersplitvariable-apicontainerstartvariable-apicrossscopeprivilege-apicustom-action-apicustomvariable-apicustomwithlabelvariable-apidashboard-apidatalookup-apidatapolicy-apidatecolumn-apidatetimecolumn-apidatetimevariable-apidatevariable-apidayofweekcolumn-apidaysofweekcolumn-apidecimalcolumn-apidocumentidcolumn-apidomainidcolumn-apidomainpathcolumn-apiduedatecolumn-apidurationcolumn-apidurationvariable-apiemailcolumn-apiemailnotification-apiemailvariable-apifieldlistcolumn-apifieldnamecolumn-apifloatcolumn-apiflow-apiflow-stages-apiform-apigenericcolumn-apigraphqlapi-apiguidcolumn-apihtmlcolumn-apihtmlvariable-apiimportset-apiinboundemailaction-apiintegercolumn-apiintegerdatecolumn-apiipaddressvariable-apijsoncolumn-apilabelvariable-apilintercheck-apilist-apilistcollectorvariable-apilistcolumn-apilookupmultiplechoicevariable-apilookupselectboxvariable-apimaskedvariable-apimultilinetextcolumn-apimultilinetextvariable-apimultiplechoicevariable-apinamevaluepairscolumn-apinowassistskillconfig-apinumericscalevariable-apiotherdatecolumn-apioverridecolumn-apipassword2column-apiplaybook-apiproperty-apiradiocolumn-apirecord-apirecordscolumn-apireferencecolumn-apireferencevariable-apirequestedforvariable-apirestapi-apirestmessage-apiretrypolicy-apirichtextlabelvariable-apiriskassessment-apirole-apischeduledatetimecolumn-apischeduledscript-apiscriptaction-apiscriptcolumn-apiscriptinclude-apiscriptonlycheck-apiselectboxvariable-apiserviceportal-apisinglelinetextvariable-apisla-apislushbucketcolumn-apispangularprovider-apispheaderfooter-apispmenu-apisppage-apisppageroutemap-apisptheme-apispwidget-apispwidgetdependency-apistatemodel-apistringcolumn-apisubflow-apisystemclassnamecolumn-apitable-apitablecheck-apitablenamecolumn-apitemplatevaluecolumn-apitest-apitestsuite-apitimecolumn-apitranslatedfieldcolumn-apitranslatedtextcolumn-apitrigger-apiuiaction-apiuipage-apiuipagevariable-apiuipolicy-apiurlcolumn-apiurlvariable-apiusercriteria-apiuserimagecolumn-apiuserpreference-apiuserrolescolumn-apiuxlistmenuconfig-apivariableset-apiversioncolumn-apiwfa-apiwfa-flow-logic-apiwidesinglelinetextvariable-apiworkspace-apiyesnovariable-api13Quick reference card
Print this one page and keep it next to the keyboard
13.1 Everything you type daily
# --- setup, once per machine ---
npm install -D @servicenow/[email protected]
npx @servicenow/sdk auth --add https://devXXXXXX.service-now.com --type basic --alias DEV
npx @servicenow/sdk auth --list
# --- new project ---
npx @servicenow/sdk init --appName "My App" --packageName my-app \
--scopeName x_mycode_myapp --auth DEV
npx @servicenow/sdk dependencies --auth DEV
# --- adopt an existing app ---
npx @servicenow/sdk init --from <app_sys_id> --auth DEV # whole app
npx @servicenow/sdk transform --table sys_script --auth DEV # one artefact type
npx @servicenow/sdk transform --from ./update-set.xml # from XML, offline
# --- the loop ---
npx @servicenow/sdk build
npx @servicenow/sdk install --auth DEV
npx @servicenow/sdk query sys_app -q "scope=x_mycode_myapp" -f name,version --auth DEV
git add -A ; git commit -m "..." # keys.ts included
# --- getting unstuck ---
npx @servicenow/sdk explain --list <keyword>
npx @servicenow/sdk explain <topic>
npx @servicenow/sdk <command> --help
# --- CI ---
npx @servicenow/sdk build --frozenKeys --errorOnConflict
SN_SDK_NODE_ENV=SN_SDK_CI_INSTALL SN_SDK_AUTH_TYPE=oauth npx @servicenow/sdk install
# --- housekeeping ---
npx @servicenow/sdk clean
npx @servicenow/sdk pack
13.2 The six things that actually bite
Commit keys.ts
It says generated. Commit it anyway, or your teammate's install creates duplicate records. §5.3.
--reinstall deletes
It uninstalls first. Anything on the instance but not in your source is gone.
--async and --demoData default to true
Both surprise people. Set them explicitly in scripts.
There is no validate
And no upgrade, and no build --watch. §10.
One owner per artefact
SDK or Studio, never both for the same record. Install overwrites.
18-character scope limit
Including the prefix. Chosen at init and painful to change later.
13.3 All twelve commands, one line each
auth | Store, list, switch and print instance credentials in the OS keychain. |
init create | Scaffold a new project, or convert an existing app with --from. |
download | Pull instance metadata to a directory for inspection. |
build | Compile Fluent to update XML. This is also the validator. |
install deploy | Push the built app to an instance. |
dependencies | Fetch type definitions and manage app dependencies. |
transform | Convert instance records or update-set XML into Fluent source. |
clean | Delete build output. |
pack | Package the app into a distributable zip. |
explain | 256 offline topics. Your best debugging tool and the way to ground an AI assistant. |
query | Run a GlideRecord-style query against an instance from the terminal. |
cicd | Publish to the App Repository, install from it, roll back. |
How to keep this document true. Everything in §2, §3 and §12 was captured from the CLI installed on this machine on 8 October 2026 — SDK 4.12.2, Node v20.20.2. After any SDK upgrade, re-run --help for all twelve commands and explain --list, then update §3 and §12. Five minutes, and it keeps this page from becoming the thing it was written to replace.
Windows install → auth → all commands → deploy → CAN/CANNOT → VR/VRO cycle: use the upgraded companion SDK_WINDOWS_FULL_CYCLE_CAN_CANNOT.html (PowerShell-first). Practical labs: SecOps/VRO · Flow Designer · AI with ServiceNow.