Hybrid ServiceNow Development with NowSDK Fluent

SkillDev tools

Hybrid 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.

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 if now-sdk commands fail.
  • vendor/now-sdk-explain.md — Self-documenting SDK reference via npx @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:

  1. npx @servicenow/sdk explain <search-term> --list --format=raw — find relevant topics
  2. npx @servicenow/sdk explain <topic> --peek --format=raw — preview before committing
  3. npx @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 admin or sn_sdk.user role
  • MCP Server: happy-platform-mcp configured and connected
  • Auth: now-sdk auth completed for target instance
  • Related Skills: development/mcp-server for MCP tool reference, vendor/now-sdk-explain.md for Fluent API docs

Tier Decision Matrix

Use this matrix to determine which tier handles each operation:

OperationTier 1 (Fluent)Tier 2 (MCP REST)Tier 3 (Fix Script)
Create flow with logicYES - full Flow DSLNo - cannot create flowsNo
Compile/publish flowAutomatic on deployNoYES - manual in UI
Create table + schemaYES - Table() with typed columnsYes - but no version controlNo
Business rulesYES - BusinessRule() with scriptsYes - SN-Create-Record on sys_scriptNo
Catalog itemsYES - CatalogItem() with variablesYes - but UI policies limitedPartial
UI Policy actionsYES - deployed with appNo - REST can't link actionsYES - setValue() script
ACLsYES - Acl() definitionYes - SN-Create-Record on sys_security_aclNo
Script includesYES - with server modulesYes - SN-Create-Record on sys_script_includeNo
Query live dataNo - not for runtimeYES - SN-Query-TableNo
Update existing recordsNo - creates new metadataYES - SN-Update-RecordNo
Update set managementAutomatic on deployYES - SN-Set-Update-Set, inspect, moveNo
Background script executionNoYES - SN-Execute-Background-ScriptNo
Scoped app creationYES - now.config.json defines scopeYes - SN-Create-Record on sys_appNo
Deploy to instanceYES - now-sdk deployNo - records created individuallyNo
Complex GlideRecord opsNoPartialYES - 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

HelperPurposeExample
Now.ID['key']Reference a record by key name$id: Now.ID['my_rule']
Now.include('./file.js')Include external script filescript: Now.include('./rule.server.js')
Now.ref(export)Cross-reference another Fluent exporttable: Now.ref(myTable)
Now.attach('./file')Attach file to recordattachment: Now.attach('./logo.png')
TemplateValue({})Set field values in flow actionsvalues: TemplateValue({ state: '2' })
wfa.dataPill(ref, type)Reference flow data pillswfa.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.*)

ActionPurpose
action.core.logLog message with level
action.core.lookUpRecordQuery for a single record
action.core.updateRecordUpdate a record
action.core.createRecordCreate a new record
action.core.sendEmailSend email notification
action.core.sendSmsSend SMS
action.core.sendNotificationSend notification via template

Flow Triggers (trigger.record.*)

TriggerFires When
trigger.record.createdRecord created
trigger.record.updatedRecord updated
trigger.record.createdOrUpdatedRecord created or updated
trigger.record.deletedRecord deleted

Best Practices

  • Consult vendor/now-sdk-explain.md before 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:

  1. Check if MCP REST can create the record directly (SN-Create-Record)
  2. If REST also fails, generate a fix script (SN-Create-Fix-Script)
  3. 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