Hybrid ServiceNow Development with NowSDK Fluent
SkillDev toolsHybrid ServiceNow development using the three-tier approach - NowSDK Fluent for metadata-as-code, MCP REST for runtime operations, and fix scripts as manual fallback
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Hybrid ServiceNow Development with NowSDK Fluent skill
What this skill tells your AI
The instructions your AI receives, as published by happy-technologies-llc/happy-platform-skills in skills/development/fluent-sdk/SKILL.md and read by ahel’s review.
Overview
This skill implements a three-tier development strategy for ServiceNow, routing each operation to the most capable tool:
TIER 1: NowSDK Fluent (metadata-as-code)
Flows, tables, business rules, catalog items, ACLs, scoped apps
TypeScript → build → deploy as scoped application
↓ falls through when...
- Runtime data operations needed
- Live instance queries required
- Existing record manipulation
TIER 2: MCP REST Tools (runtime operations)
CRUD on live data, queries, update set management, background scripts
Direct REST API calls to instance
↓ falls through when...
- REST API has documented limitations
- UI-only operations (Flow compilation, UI Policy action linking)
- Complex server-side operations not exposed via REST
TIER 3: Fix Scripts (manual fallback)
SN-Create-Fix-Script for operations neither tier can handle
Generated scripts for manual execution in instance
Used for: Flow compilation, UI Policy setValue(), complex GlideRecord ops
When to use this skill:
- "Build a new flow for incident escalation"
- "Create a scoped app with tables and business rules"
- "Set up a catalog item with approval workflow"
- "Deploy metadata changes with version control"
- "I need a flow but REST can't create flows"
Foundation: ServiceNow Official Fluent Skills
This skill builds on ServiceNow's official AI skills from github.com/ServiceNow/sdk:
vendor/now-sdk-setup.md— Environment setup (Node 20+, SDK install, auth). Run this first ifnow-sdkcommands fail.vendor/now-sdk-explain.md— Self-documenting SDK reference vianpx @servicenow/sdk explain. Always consult this before writing Fluent code — it covers every metadata type, convention, and project structure detail.
Before writing any Fluent code, follow the explain skill's protocol:
npx @servicenow/sdk explain <search-term> --list --format=raw— find relevant topicsnpx @servicenow/sdk explain <topic> --peek --format=raw— preview before committingnpx @servicenow/sdk explain <topic> --format=raw— read the full topic
This ensures you're using the latest API signatures and conventions, not stale examples.
Prerequisites
- Node.js: v20+ (
node --version) - NowSDK: v4.6.0+ (
npm install -g @servicenow/sdk@latest) - Instance: ServiceNow SDK plugin enabled, user has
adminorsn_sdk.userrole - MCP Server: happy-platform-mcp configured and connected
- Auth:
now-sdk authcompleted for target instance - Related Skills:
development/mcp-serverfor MCP tool reference,vendor/now-sdk-explain.mdfor Fluent API docs
Tier Decision Matrix
Use this matrix to determine which tier handles each operation:
| Operation | Tier 1 (Fluent) | Tier 2 (MCP REST) | Tier 3 (Fix Script) |
|---|---|---|---|
| Create flow with logic | YES - full Flow DSL | No - cannot create flows | No |
| Compile/publish flow | Automatic on deploy | No | YES - manual in UI |
| Create table + schema | YES - Table() with typed columns | Yes - but no version control | No |
| Business rules | YES - BusinessRule() with scripts | Yes - SN-Create-Record on sys_script | No |
| Catalog items | YES - CatalogItem() with variables | Yes - but UI policies limited | Partial |
| UI Policy actions | YES - deployed with app | No - REST can't link actions | YES - setValue() script |
| ACLs | YES - Acl() definition | Yes - SN-Create-Record on sys_security_acl | No |
| Script includes | YES - with server modules | Yes - SN-Create-Record on sys_script_include | No |
| Query live data | No - not for runtime | YES - SN-Query-Table | No |
| Update existing records | No - creates new metadata | YES - SN-Update-Record | No |
| Update set management | Automatic on deploy | YES - SN-Set-Update-Set, inspect, move | No |
| Background script execution | No | YES - SN-Execute-Background-Script | No |
| Scoped app creation | YES - now.config.json defines scope | Yes - SN-Create-Record on sys_app | No |
| Deploy to instance | YES - now-sdk deploy | No - records created individually | No |
| Complex GlideRecord ops | No | Partial | YES - full server-side API |
Procedure
Step 1: Environment Setup
Follow vendor/now-sdk-setup.md to verify NowSDK is installed and working:
# Check Node version (must be 20+)
node --version
# Check SDK version (must be 4.6.0+)
npx @servicenow/sdk --version
# If not installed or outdated:
npm install -g @servicenow/sdk@latest
# Authenticate to your instance
now-sdk auth
# Follow prompts: instance URL, credentials
Verify with explain command:
npx @servicenow/sdk explain quickstart --list --format=raw
Then familiarize yourself with the Fluent API for whatever you're about to build:
# Example: about to create flows
npx @servicenow/sdk explain Flow --list --peek --format=raw
npx @servicenow/sdk explain Flow --format=raw
Step 2: Determine the Right Tier
Before writing any code, classify the request:
Route to Tier 1 (Fluent) when:
- Creating new metadata from scratch (flows, tables, business rules, catalog items)
- Building a scoped application
- Need version-controlled, reproducible deployments
- Flow Designer operations (REST API cannot create flows)
- Complex catalog items with variable sets, pricing, access controls
Route to Tier 2 (MCP REST) when:
- Querying or modifying live data
- Managing update sets (inspect, move, clone)
- One-off record updates on existing records
- Running background scripts
- Verifying Tier 1 deployments
Route to Tier 3 (Fix Script) when:
- Flow compilation (must happen in UI after deploy)
- UI Policy action linking via
setValue() - Complex GlideRecord operations not exposed via REST
- Bulk data operations requiring server-side performance
Step 3: Scaffold a Fluent Project (Tier 1)
Create the project structure for a new scoped application.
Project Structure:
my-sn-app/
├── now.config.json # Scope and instance config
├── package.json # Dependencies
├── tsconfig.json # TypeScript config (optional)
├── src/
│ └── fluent/
│ ├── generated/
│ │ └── keys.ts # Auto-generated sys_id mappings
│ ├── tables.now.ts # Table definitions
│ ├── rules.now.ts # Business rules
│ ├── flows.now.ts # Flow definitions
│ ├── catalog.now.ts # Catalog items
│ ├── acls.now.ts # Access controls
│ └── *.server.js # External script files
└── src/
└── server/ # Server-side TypeScript modules
now.config.json:
{
"scope": "x_myapp",
"scopeId": "<sys_id from instance or generated on first deploy>"
}
package.json:
{
"name": "my-sn-app",
"version": "1.0.0",
"scripts": {
"build": "now-sdk build",
"deploy": "now-sdk deploy",
"explain": "now-sdk explain"
},
"devDependencies": {
"@servicenow/glide": "catalog:",
"@servicenow/sdk": "catalog:",
"typescript": "catalog:"
}
}
Step 4: Write Fluent Definitions
Tables
// src/fluent/tables.now.ts
import {
Table, StringColumn, IntegerColumn, BooleanColumn,
DateColumn, ReferenceColumn
} from '@servicenow/sdk/core'
export const x_myapp_request = Table({
name: 'x_myapp_request',
schema: {
title: StringColumn({ mandatory: true, label: 'Title', maxLength: 200 }),
priority: IntegerColumn({ mandatory: true, label: 'Priority' }),
active: BooleanColumn({ mandatory: true, label: 'Active', default: true }),
due_date: DateColumn({ label: 'Due Date' }),
assigned_to: ReferenceColumn({
label: 'Assigned To',
referenceTable: 'sys_user',
}),
},
})
Business Rules
// src/fluent/rules.now.ts
import { BusinessRule } from '@servicenow/sdk/core'
export const validatePriority = BusinessRule({
$id: Now.ID['validate_priority'],
name: 'Validate Priority on Create',
active: true,
table: 'x_myapp_request',
when: 'before',
insert: true,
script: Now.include('./validate-priority.server.js'),
})
// src/fluent/validate-priority.server.js
(function executeRule(current, previous) {
if (!current.priority || current.priority < 1 || current.priority > 5) {
current.priority = 3; // Default to medium
}
if (!current.assigned_to && current.priority == 1) {
gs.addErrorMessage('Critical requests must have an assignee');
current.setAbortAction(true);
}
})(current, previous);
Flows
// src/fluent/flows.now.ts
import { action, Flow, wfa, trigger } from '@servicenow/sdk/automation'
export const escalationFlow = Flow(
{
$id: Now.ID['request_escalation_flow'],
name: 'Request Escalation Flow',
description: 'Escalates high-priority requests when unassigned after creation',
},
wfa.trigger(
trigger.record.created,
{ $id: Now.ID['new_request_trigger'] },
{
table: 'x_myapp_request',
condition: 'priority=1^assigned_toISEMPTY',
run_flow_in: 'background',
run_on_extended: 'false',
run_when_setting: 'both',
run_when_user_setting: 'any',
run_when_user_list: [],
}
),
(params) => {
// Log the escalation
wfa.action(
action.core.log,
{ $id: Now.ID['log_escalation'] },
{
log_level: 'warn',
log_message: `Escalating unassigned critical request: ${wfa.dataPill(params.trigger.current.title, 'string')}`,
}
)
// Look up the on-call manager
const manager = wfa.action(
action.core.lookUpRecord,
{ $id: Now.ID['lookup_oncall_manager'] },
{
table: 'sys_user',
conditions: 'active=true^roles=manager',
sort_type: 'sort_asc',
if_multiple_records_are_found_action: 'use_first_record',
}
)
// Auto-assign to manager
wfa.action(
action.core.updateRecord,
{ $id: Now.ID['assign_to_manager'] },
{
table_name: 'x_myapp_request',
record: wfa.dataPill(params.trigger.current.sys_id, 'reference'),
values: TemplateValue({
assigned_to: wfa.dataPill(manager.Record.sys_id, 'reference'),
}),
}
)
// Send notification
wfa.action(
action.core.sendEmail,
{ $id: Now.ID['notify_manager'] },
{
table_name: 'x_myapp_request',
watermark_email: true,
ah_subject: `Critical Request Escalation: ${wfa.dataPill(params.trigger.current.title, 'string')}`,
ah_body: `A critical request has been auto-assigned to you.`,
record: wfa.dataPill(params.trigger.current.sys_id, 'reference'),
ah_to: wfa.dataPill(manager.Record.email, 'string'),
}
)
}
)
Catalog Items
// src/fluent/catalog.now.ts
import {
CatalogItem, VariableSet, SingleLineTextVariable,
MultiLineTextVariable, SelectBoxVariable, ReferenceVariable,
RequestedForVariable
} from '@servicenow/sdk/core'
const requestInfoVarSet = VariableSet({
$id: Now.ID['request_info_varset'],
title: 'Request Details',
description: 'Core information for the request',
internalName: 'request_info_varset',
type: 'singleRow',
layout: 'normal',
order: 100,
displayTitle: true,
version: 1,
variables: {
requestTitle: SingleLineTextVariable({
question: 'Request Title',
mandatory: true,
order: 100,
}),
requestDescription: MultiLineTextVariable({
question: 'Description',
mandatory: true,
order: 200,
}),
requestPriority: SelectBoxVariable({
question: 'Priority',
mandatory: true,
order: 300,
choices: {
critical: { label: '1 - Critical' },
high: { label: '2 - High' },
medium: { label: '3 - Medium' },
low: { label: '4 - Low' },
},
defaultValue: 'medium',
}),
},
name: 'Request Details',
})
export const newRequestCatalogItem = CatalogItem({
$id: Now.ID['new_request_catalog_item'],
name: 'New Service Request',
shortDescription: 'Submit a new service request',
description: 'Use this form to submit a service request. Critical requests trigger automatic escalation.',
catalogs: ['e0d08b13c3330100c8b837659bba8fb4'], // Service Catalog
categories: ['d258b953c611227a0146101fb1be7c31'], // Hardware (or your category)
variableSets: [{ variableSet: requestInfoVarSet, order: 100 }],
executionPlan: '523da512c611228900811a37c97c2014',
variables: {
requestedFor: RequestedForVariable({
order: 1,
question: 'Requested For',
}),
assignmentGroup: ReferenceVariable({
order: 2,
question: 'Assignment Group',
referenceTable: 'sys_user_group',
}),
},
})
Step 5: Build and Deploy (Tier 1)
# Build the project (validates TypeScript, generates metadata)
now-sdk build
# Deploy to instance (creates/updates scoped app and all metadata)
now-sdk deploy
Step 6: Verify with MCP Tools (Tier 2)
After Fluent deployment, use MCP tools to verify everything landed correctly.
Tool: SN-Query-Table
Parameters:
table_name: sys_update_xml
query: application=<app_sys_id>
fields: sys_id,type,name,sys_created_on
limit: 50
Tool: SN-Get-Table-Schema
Parameters:
table_name: x_myapp_request
Tool: SN-Query-Table
Parameters:
table_name: sys_script
query: collection=x_myapp_request^active=true
fields: name,when,active
Step 7: Handle Fallthrough to Fix Scripts (Tier 3)
For operations that neither Fluent nor REST can handle, generate fix scripts.
Flow Compilation (post-deploy):
Tool: SN-Create-Fix-Script
Parameters:
name: "Compile Escalation Flow"
description: "Compile the request escalation flow after Fluent deployment"
script: |
// Navigate to Flow Designer and compile:
// 1. Open Flow Designer
// 2. Find "Request Escalation Flow"
// 3. Click "Activate" to compile and enable
//
// This step cannot be automated - Flow Designer compilation
// requires the UI runtime environment.
gs.info('Manual step: Compile flow in Flow Designer UI');
UI Policy Action Linking (if needed):
Tool: SN-Execute-Background-Script
Parameters:
script: |
var gr = new GlideRecord('sys_ui_policy_action');
gr.addQuery('ui_policy', '<ui_policy_sys_id>');
gr.addQuery('catalog_variable', '');
gr.query();
while (gr.next()) {
gr.catalog_variable = 'IO:<variable_sys_id>';
gr.update();
}
description: "Link UI Policy actions to catalog variables"
execution_method: trigger
Bulk Data Operations:
Tool: SN-Create-Fix-Script
Parameters:
name: "Seed Sample Data"
description: "Create initial sample records for testing"
script: |
var records = [
{ title: 'Test Request 1', priority: 1 },
{ title: 'Test Request 2', priority: 3 },
{ title: 'Test Request 3', priority: 5 }
];
records.forEach(function(data) {
var gr = new GlideRecord('x_myapp_request');
gr.initialize();
gr.title = data.title;
gr.priority = data.priority;
gr.active = true;
gr.insert();
});
gs.info('Created ' + records.length + ' sample records');
Hybrid Workflow Example
A complete end-to-end example combining all three tiers:
GOAL: Build a scoped app with escalation flow, catalog item, and seed data
TIER 1 (Fluent):
1. Create now.config.json with scope
2. Define table schema in tables.now.ts
3. Define business rules in rules.now.ts
4. Define escalation flow in flows.now.ts
5. Define catalog item in catalog.now.ts
6. now-sdk build && now-sdk deploy
TIER 2 (MCP REST):
7. SN-Query-Table → verify all records in update set
8. SN-Get-Table-Schema → confirm table columns created
9. SN-Query-Table on sys_script → verify business rules active
10. SN-Execute-Background-Script → seed sample data
TIER 3 (Fix Script):
11. SN-Create-Fix-Script → flow compilation instructions
12. SN-Create-Fix-Script → any UI Policy action linking
13. Manual: compile flow in Flow Designer UI
NowSDK Explain Reference
The SDK has built-in documentation accessible via CLI:
# List all available topics
npx @servicenow/sdk explain --list --format=raw
# Search for a topic
npx @servicenow/sdk explain flow --list --peek --format=raw
# Read full topic (only after previewing)
npx @servicenow/sdk explain flow --format=raw
# Key topics:
# quickstart - Getting started
# Table - Table definitions
# Flow - Flow automation
# BusinessRule - Business rule definitions
# CatalogItem - Service catalog items
# Acl - Access control lists
# ScriptInclude - Script includes
# naming - Naming conventions
# structure - Project structure
Important: Always use --peek first before reading a full topic to avoid wasting context on irrelevant content.
Fluent API Quick Reference
Imports by Module
// Core metadata
import {
Table, StringColumn, IntegerColumn, BooleanColumn,
DateColumn, ReferenceColumn, Record, BusinessRule,
ScriptInclude, ClientScript, Acl, Role
} from '@servicenow/sdk/core'
// Flow automation
import {
Flow, Subflow, action, wfa, trigger
} from '@servicenow/sdk/automation'
// Catalog
import {
CatalogItem, VariableSet, SingleLineTextVariable,
MultiLineTextVariable, SelectBoxVariable, ReferenceVariable,
CheckBoxVariable, DateTimeVariable, RequestedForVariable,
EmailVariable
} from '@servicenow/sdk/core'
Key Helpers
| Helper | Purpose | Example |
|---|---|---|
Now.ID['key'] | Reference a record by key name | $id: Now.ID['my_rule'] |
Now.include('./file.js') | Include external script file | script: Now.include('./rule.server.js') |
Now.ref(export) | Cross-reference another Fluent export | table: Now.ref(myTable) |
Now.attach('./file') | Attach file to record | attachment: Now.attach('./logo.png') |
TemplateValue({}) | Set field values in flow actions | values: TemplateValue({ state: '2' }) |
wfa.dataPill(ref, type) | Reference flow data pills | wfa.dataPill(params.trigger.current.sys_id, 'reference') |
Flow Logic Blocks
// Conditional
wfa.flowLogic.if({ $id, condition, annotation }, () => { ... })
wfa.flowLogic.elseIf({ $id, condition, annotation }, () => { ... })
wfa.flowLogic.else({ $id }, () => { ... })
// Loops
wfa.flowLogic.forEach(dataPill, { $id }, () => { ... })
// Subflow outputs
wfa.flowLogic.assignSubflowOutputs({ $id }, params.outputs, { ... })
Flow Actions (action.core.*)
| Action | Purpose |
|---|---|
action.core.log | Log message with level |
action.core.lookUpRecord | Query for a single record |
action.core.updateRecord | Update a record |
action.core.createRecord | Create a new record |
action.core.sendEmail | Send email notification |
action.core.sendSms | Send SMS |
action.core.sendNotification | Send notification via template |
Flow Triggers (trigger.record.*)
| Trigger | Fires When |
|---|---|
trigger.record.created | Record created |
trigger.record.updated | Record updated |
trigger.record.createdOrUpdated | Record created or updated |
trigger.record.deleted | Record deleted |
Best Practices
- Consult
vendor/now-sdk-explain.mdbefore writing Fluent metadata. - Keep generated metadata in source control and review the resulting platform changes.
- Use the tier decision matrix to avoid mixing deployment models without a clear need.
Troubleshooting
NowSDK Not Found
npm install -g @servicenow/sdk@latest
# Verify: npx @servicenow/sdk --version (must be 4.6.0+)
Auth Failures
# Re-authenticate
now-sdk auth
# Ensure instance has SDK plugin enabled
# Ensure user has admin or sn_sdk.user role
Build Errors
# Check explain docs for the specific type
npx @servicenow/sdk explain <TypeName> --format=raw
# Common issues:
# - Missing $id on records → add Now.ID['unique_key']
# - Invalid column types → check Table column type imports
# - Script file not found → verify Now.include() path is relative to .now.ts file
Deploy Creates Wrong Scope
Verify now.config.json has the correct scope and scopeId. The scopeId should match the sys_id of the sys_app record on your instance.
Fallthrough: When Fluent Fails
If now-sdk deploy fails for specific record types, fall through:
- Check if MCP REST can create the record directly (
SN-Create-Record) - If REST also fails, generate a fix script (
SN-Create-Fix-Script) - Document the limitation for future reference
Signals
- GitHub stars
- 37
- Forks
- 13
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
fluent-sdk- Source
- github.com/happy-technologies-llc/happy-platform-skills