ServiceNow SDK Application Builder
Read this SKILL.md first for prerequisites, CLI commands, and Key rules (tables, flows, inbound email).
For exhaustive API examples, read references/fluent-patterns.md.
For subflows, parallel blocks, for-each, and email pipelines, read references/flow-advanced.md.
For the official runnable samples (clone list, sample index, how to diff against your code), read references/sdk-examples-repo.md — companion to ServiceNow/sdk-examples.
For typed Fluent API reference (version-matched llmsFull / llms from ServiceNow), read references/sdk-api-docs-agents.md — links servicenow.github.io/sdk agent files and versions.json.
If a reference example conflicts with the Key rules sections below or with sdk-examples on main, prefer Key rules first, then reconcile using the matching sample file from sdk-examples; for API signatures, prefer version-matched llmsFull from versions.json.
This skill enables the agent to generate production-ready ServiceNow applications using the ServiceNow SDK (now-sdk) and the Fluent DSL. Fluent is a TypeScript-based domain-specific language that compiles to ServiceNow metadata XML. It covers tables, flows, business rules, script includes, ACLs, REST APIs, and more.
Important: Authentication is handled externally
The user has already authenticated with their ServiceNow instance using the SDK CLI.
Auth credentials are stored in the system keychain — NOT in project files.
You do NOT need to create .env files or handle credentials.
If the user needs to set up a new auth profile, point them to:
now-sdk auth save <alias> --host https://<instance>.service-now.com --username <user> --default
But do NOT run auth commands yourself. The user handles this manually.
Prerequisites — Verify Before Generating Code
Before writing any Fluent code, quickly check the project has the basics:
now.config.jsonmust exist in the project root with at minimum:{ "scope": "x_<vendor>_<app>", "scopeId": "<sys_id_of_app>" }For TypeScript projects, also needs:
{ "transpiledSourceDir": "dist/src" }package.jsonmust have SDK dependencies:{ "type": "module", "scripts": { "build": "now-sdk build", "install-app": "now-sdk install", "transform": "now-sdk transform", "dependencies": "now-sdk dependencies" }, "devDependencies": { "@servicenow/sdk": "^4.0.0", "@servicenow/glide": "^27.0.0" } }For TypeScript, add:
{ "scripts": { "build": "rm -rf dist && tsc -b && now-sdk build" }, "devDependencies": { "typescript": "^5.5.0" } }If the project doesn't exist yet, the user should scaffold it with:
npx @servicenow/sdk initThis walks through template selection and links the auth profile.
If prerequisites are met, proceed directly to writing Fluent code.
Project Structure
All Fluent files go in src/fluent/ with the .now.ts extension.
Organize by concern:
my-sn-app/
├── now.config.json
├── package.json
├── tsconfig.json # if TypeScript
├── src/
│ ├── fluent/
│ │ ├── tables/
│ │ │ └── email-data.now.ts
│ │ ├── flows/
│ │ │ └── inbound-email-parser.now.ts
│ │ ├── business-rules/
│ │ ├── script-includes/
│ │ ├── acls/
│ │ └── index.now.ts # barrel exports (optional)
│ ├── server/ # JS/TS modules (.server.js or .server.ts)
│ └── client/ # front-end code (if fullstack)
├── @types/ # auto-generated type defs (via `now-sdk dependencies`)
└── metadata/ # auto-generated XML (build output, do not edit)
SDK CLI Commands Reference (v3.0+ / v4.x)
These are the commands Claude Code should use or instruct the user to run:
| Command | Purpose | Example |
|---|---|---|
now-sdk build |
Compile Fluent → metadata XML | now-sdk build |
now-sdk install |
Build + pack + deploy to instance | now-sdk install --auth <alias> |
now-sdk transform |
Download/sync metadata from instance to local | now-sdk transform --auth <alias> |
now-sdk dependencies |
Fetch table defs and type info from instance | now-sdk dependencies --auth <alias> |
now-sdk init |
Scaffold a new app or convert existing | npx @servicenow/sdk init |
now-sdk pack |
Package build output into installable archive | now-sdk pack |
now-sdk auth save |
Store auth credentials in system keychain | User handles manually |
Build and Install Workflow
The standard development cycle is:
# 1. Write or modify Fluent code in src/fluent/*.now.ts
# 2. Build — compiles Fluent to metadata XML
now-sdk build
# 3. Install — packages and deploys to the instance
now-sdk install --auth <alias>
Build flags:
--debug true— include debug info--generate-deletes true— remove records from instance that were deleted locally--lint true— run linting during build--frozenKeys— fail if keys.ts needs updating (useful for CI)
Install flags:
--auth <alias>— which auth profile to use--open-browser true— open the app in browser after install
Syncing from Instance
To pull changes made on the instance back to local:
now-sdk transform --auth <alias>
This downloads metadata and converts supported types to Fluent .now.ts files
in src/fluent/generated/.
Fetching Type Definitions
To get table schemas and type info for IntelliSense:
now-sdk dependencies --auth <alias>
This populates @types/servicenow/ with type definitions for GlideRecord etc.
Core Fluent Patterns
Read references/fluent-patterns.md for the full API reference with examples
covering Tables, Flows, Business Rules, Script Includes, ACLs, Records, and more.
The most important patterns for email parsing apps are summarized below.
Table Definition
import {
Table, StringColumn, ReferenceColumn, IntegerColumn,
DateTimeColumn, HtmlColumn, ChoiceColumn
} from '@servicenow/sdk/core'
export const x_vendor_app_email_data = Table({
name: 'x_vendor_app_email_data',
label: 'Parsed Email Data',
schema: {
x_vendor_app_sender: StringColumn({
label: 'Sender',
maxLength: 255,
mandatory: true
}),
x_vendor_app_subject: StringColumn({
label: 'Subject',
maxLength: 500
}),
x_vendor_app_received_date: DateTimeColumn({
label: 'Received Date'
}),
x_vendor_app_body_text: HtmlColumn({
label: 'Email Body'
}),
x_vendor_app_status: ChoiceColumn({
label: 'Processing Status',
// choices is an OBJECT (keys = values), NOT an array
choices: {
new: { label: 'New' },
parsed: { label: 'Parsed' },
error: { label: 'Error' },
},
default: 'new' // use 'default', NOT 'defaultValue'
})
}
})
Key rules for tables:
- Table name MUST start with the app scope prefix:
x_<vendor>_<app>_ - Column names MUST also be prefixed with the scope
- Available column types:
StringColumn,IntegerColumn,BooleanColumn,DateTimeColumn,ReferenceColumn,ChoiceColumn,HtmlColumn,DecimalColumn,URLColumn,DurationColumn,TimeColumn,FieldListColumn,GlideListColumn HtmlColumn— NOTHTMLColumn(casing matters, build will fail)ChoiceColumnchoices must be an object{ key: { label } }, NOT an arrayChoiceColumndefault value usesdefault:, NOTdefaultValue:- Use
ReferenceColumnwithreference: '<table_name>'for foreign keys
Flow with Inbound Email Trigger
import { action, Flow, wfa, trigger } from '@servicenow/sdk/automation'
export const emailParserFlow = Flow(
{
$id: Now.ID['email_parser_flow'],
name: 'Email Parser Flow',
// description must be a single string literal — NO string concatenation with +
description: 'Parses inbound emails and extracts data to custom table',
},
wfa.trigger(
trigger.application.inboundEmail,
{ $id: Now.ID['inbound_email_trigger'] },
{
// Use 'email_conditions' (NOT 'condition' or 'record_type')
// Encoded query format: NO spaces around operator — 'subjectLIKEorder%'
email_conditions: 'subjectLIKEorder%',
target_table: 'incident', // optional: table for reply record
}
),
(params) => {
// Inbound email trigger data pills — direct on params.trigger (NOT params.trigger.email.*)
// params.trigger.from_address — sender email address
// params.trigger.subject — email subject
// params.trigger.body_text — plain-text body
// params.trigger.inbound_email — reference to sys_email record (for .body, .sys_id, etc.)
// params.trigger.user — reference to sys_user who sent the email
// Log for debugging
wfa.action(
action.core.log,
{ $id: Now.ID['log_email_received'] },
{
log_level: 'info',
log_message: `Processing email from: ${wfa.dataPill(params.trigger.from_address, 'string')}`,
}
)
// Create record in custom table
// createRecord uses 'table_name' (NOT 'table') and values: TemplateValue({}) (NOT a plain object)
wfa.action(
action.core.createRecord,
{ $id: Now.ID['create_parsed_record'] },
{
table_name: 'x_vendor_app_email_data',
values: TemplateValue({
x_vendor_app_sender: wfa.dataPill(params.trigger.from_address, 'string'),
x_vendor_app_subject: wfa.dataPill(params.trigger.subject, 'string'),
x_vendor_app_body_text: wfa.dataPill(params.trigger.body_text, 'string'),
x_vendor_app_status: 'new',
}),
}
)
}
)
Key rules for flows:
- Every element needs a unique
$idusingNow.ID['unique_key'] - The
$idkeys must be unique across the entire application descriptionmust be a single string literal — string concatenation with+will cause a build error- Trigger types:
trigger.record.created,trigger.record.updated,trigger.application.inboundEmail,trigger.schedule.daily, etc. - Inbound email trigger config: use
email_conditionswith encoded query (no spaces around operator). Valid fields:email_conditions,target_table,order,stop_condition_evaluation - Inbound email data pills — accessed directly on
params.trigger:params.trigger.from_address— sender address (string)params.trigger.subject— subject line (string)params.trigger.body_text— plain-text body (string)params.trigger.inbound_email— thesys_emailrecord reference (use.body,.sys_id, etc.)params.trigger.user—sys_userreference for the sender- WRONG:
params.trigger.email.subject/params.trigger.email.from— these do NOT exist
createRecordinputs:table_name(NOTtable) +values: TemplateValue({...})(NOT a plain object)TemplateValueis a global — no import needed- Use
wfa.dataPill()to reference runtime values from trigger or prior actions - Use
wfa.flowLogic.if()/wfa.flowLogic.elseIf()/wfa.flowLogic.else()for branching - Actions:
action.core.log,action.core.createRecord,action.core.createTask,action.core.lookUpRecord(capital U),action.core.sendEmail,action.core.getAttachmentsOnRecord,action.core.copyAttachment
Flow Logic — Branching
wfa.flowLogic.if(
{
$id: Now.ID['check_condition'],
condition: `${wfa.dataPill(params.trigger.current.severity, 'string')}=1`
},
() => {
// actions for when condition is true
}
)
wfa.flowLogic.else(
{ $id: Now.ID['else_block'] },
() => {
// actions for all other cases
}
)
Email Parsing Workflow — Step by Step
When the user asks to build an email parsing application, follow this sequence:
Step 1: Analyze the Sample Email
Ask the user for a sample email (or they may have already provided one). Extract the fields that should be captured. Common fields include:
- Sender address
- Subject line
- Date/time received
- Key data from the body (order numbers, amounts, names, IDs, etc.)
- Attachments (flag presence)
Step 2: Design the Table Schema
Based on the extracted fields, generate a Table definition with appropriate column types. Use the app scope prefix throughout. Explain your column choices.
Step 3: Generate the Inbound Email Flow
Create the Flow with:
- An inbound email trigger with appropriate conditions
- Parsing logic (via script actions or direct field mapping)
- Record creation in the custom table
- Error handling with a status field
- Optional: notification on parse failure
Step 4: Add Supporting Elements (if needed)
Depending on complexity, also generate:
- Business Rule: for post-insert processing or validation
- Script Include: for reusable parsing logic (regex extraction, etc.)
- ACL: to control who can read/write the parsed data table
Step 5: Build and Install
Run the build and install commands:
now-sdk build
now-sdk install --auth <alias>
If there are errors, help debug. Common issues:
- Scope prefix mismatch between
now.config.jsonand Fluent code - Missing
$idor duplicate$idvalues - Node version too old (requires v20+)
- Build lint errors — check imports and type usage
Naming Conventions
These are mandatory for ServiceNow SDK apps:
| Element | Pattern | Example |
|---|---|---|
| Table name | x_<vendor>_<app>_<name> |
x_acme_emailp_parsed_data |
| Column name | x_<vendor>_<app>_<field> |
x_acme_emailp_sender_address |
| Flow $id | Unique snake_case key | email_parser_flow |
| Action $id | Descriptive snake_case | create_parsed_email_record |
| File name | kebab-case with .now.ts extension |
inbound-email-parser.now.ts |
Always derive <vendor> and <app> from the scope field in now.config.json.
Security Reminders
- NEVER put credentials in source files — auth is in the system keychain
- Ensure
.gitignoreincludesmetadata/,dist/,node_modules/,.now/ - Inbound email flows run as the email sender by default. If the sender is unknown, the flow runs as Guest. For elevated operations, use a subflow that runs as System
- Always apply ACLs to custom tables to restrict access appropriately
- Enable the Email Filter plugin (
com.glide.email_filter) for reliable inbound email trigger execution
Troubleshooting Quick Reference
| Problem | Solution |
|---|---|
Auth profile not found |
User runs: now-sdk auth save <alias> --host <url> --username <user> --default |
Scope mismatch |
Table/column name prefixes must match now.config.json scope |
Duplicate $id |
Every Now.ID['key'] must be unique across the entire app |
Build fails on .ts |
Check tsconfig.json has "outDir": "dist/src" and transpiledSourceDir is set in now.config.json |
Install 403 |
Verify user has admin or app_developer role on the instance |
Flow not triggering |
Check Email Filter plugin is active; verify email_conditions encoded query is correct (e.g. subjectLIKEfoo) |
TS2724: no exported member 'HTMLColumn' |
Use HtmlColumn — the correct export name (casing matters) |
TS2353: 'value' does not exist in ChoiceConfig |
ChoiceColumn choices must be an object { key: { label } } not an array; and use default: not defaultValue: |
TS2769: 'table' does not exist on createRecord |
Use table_name: and values: TemplateValue({...}) — not table: and not a plain object |
TS243: Unknown instance type in Flow |
Trigger data pills are on params.trigger.subject / .from_address / .body_text directly — not params.trigger.email.* |
TS303: Failed to parse property on description |
description must be a single string literal — multi-line + concatenation is not supported in Fluent |
Guest user errors |
Use subflow with run_as: 'system' for privileged operations |
Transform conflicts |
Generated files in src/fluent/generated/ — migrate to proper files if needed |
Missing types |
Run now-sdk dependencies --auth <alias> to fetch type defs |
When Build Fails — Check SDK Examples First
IMPORTANT: If now-sdk build fails with an error you are not sure about, fetch the
relevant official SDK example file below using WebFetch before guessing at a fix.
The examples are the authoritative source of correct Fluent syntax.
Official SDK Example Files (fetch raw content on demand)
| What you're building | File to fetch |
|---|---|
| Inbound email flow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/flow-trigger-inbound-email.now.ts |
| Record trigger flow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/flow-trigger-record.now.ts |
| Service catalog flow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/flow-trigger-catalog-item.now.ts |
| SLA task flow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/flow-trigger-sla-task.now.ts |
| Knowledge mgmt flow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/flow-trigger-knowledge-management.now.ts |
| Remote table query flow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/flow-trigger-remote-table-query.now.ts |
| Subflow | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/flow-sample/src/fluent/subflow-sample.now.ts |
| Simple table | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/table-sample/src/fluent/table-simple.now.ts |
| Table with custom columns | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/table-sample/src/fluent/table-custom-column.now.ts |
| Table extending another | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/table-sample/src/fluent/table-extends.now.ts |
| Business rule | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/businessrule-sample/src/fluent/business-rule-1.now.ts |
| ACL | https://raw.githubusercontent.com/ServiceNow/sdk-examples/main/acl-sample/src/fluent/index.now.ts |
How to use these during debugging
- Build fails with an unfamiliar error → identify which example type matches the failing code
- Use
WebFetchto fetch the raw URL from the table above - Compare the example's exact syntax against the failing code and fix accordingly
- Re-run
now-sdk build
The full examples index and workflow (clone, pnpm, sample table) are in references/sdk-examples-repo.md. Repo: github.com/ServiceNow/sdk-examples.
Additional References
For deeper patterns beyond email parsing, read:
references/sdk-api-docs-agents.md— Official agent-oriented API text: versions.json,llms/llmsFullper SDK version, Context7 vs fetchreferences/sdk-examples-repo.md— Official sdk-examples repo: sample index, prerequisites, how to align local code with upstream Fluent filesreferences/fluent-patterns.md— Full Table, Flow, BusinessRule, ScriptInclude, ACL, Record, and RestApi patterns with examplesreferences/flow-advanced.md— Advanced flow patterns: subflows, parallel blocks, for-each loops, error handling, and approval flows- SDK API reference (human + agent
llms*): https://servicenow.github.io/sdk/ - ServiceNow SDK docs: https://www.servicenow.com/docs/r/application-development/servicenow-sdk/define-metadata-code-fluent-sdk.html
- Build & install docs: https://www.servicenow.com/docs/r/application-development/servicenow-sdk/build-deploy-application-now-sdk.html
- Fluent MCP server (for AI-assisted dev): https://github.com/modesty/fluent-mcp
- SDK releases & changelog: https://github.com/ServiceNow/sdk/releases