NowAIKit
Get started
Use cases Pricing Docs Contact Get started

SDK & Fluent Query Guide

Three power tools that give AI agents a modern, expressive way to interact with ServiceNow: fluent_query for GlideQuery-style chained queries, batch_request for multi-operation HTTP calls, and execute_script for server-side script execution. Together they reduce round-trips by 50-70% and unlock analytics that plain REST cannot express.

Quick Start

These tools are included in the platform_developer and ai_developer tool packages. Enable them by setting your tool package and write permissions:

.env
SERVICENOW_INSTANCE=https://your-instance.service-now.com
SERVICENOW_USERNAME=admin
SERVICENOW_PASSWORD=your-password
WRITE_ENABLED=true
MCP_TOOL_PACKAGE=platform_developer

The fluent_query and batch_request tools work with read-only access. The execute_script tool and any batch write operations require WRITE_ENABLED=true.

ToolPurposeWrite Required
fluent_queryGlideQuery-style chained query builderNo (read-only)
batch_requestMulti-operation REST in one HTTP callOnly for POST/PATCH/DELETE
execute_scriptServer-side Background ScriptYes (always)
fluent_sdk_queryRead-only now-sdk query via the SDK CLI (4.8)No (read-only)
fluent_versionInstalled @servicenow/sdk version & upgrade hintsNo

Fluent Query

The fluent_query tool provides a GlideQuery-inspired interface that mirrors the chained query patterns ServiceNow developers use on the platform. Instead of building encoded query strings manually, you pass structured parameters and the tool constructs the optimal API call.

Parameter Reference

ParameterTypeRequiredDescription
tablestringYesTable name (e.g., "incident", "cmdb_ci")
wherearrayNoAND conditions: [field, operator, value]. Two-element arrays default to =
orWherearrayNoOR conditions (same format as where)
selectarrayNoFields to return. Supports dot-walking (e.g., "caller_id.email")
aggregatestringNoAggregate operation: COUNT, AVG, SUM, MIN, MAX
aggregateFieldstringNoField to aggregate on (required for AVG, SUM, MIN, MAX)
groupBystringNoField to group aggregate results by
orderBystringNoSort field. Prefix with "-" for descending
limitnumberNoMax records (default: 20, max: 200)
displayValuebooleanNoReturn display values instead of sys_ids

Supported Operators

= != > >= < <= LIKE STARTSWITH CONTAINS IN NOT IN ISEMPTY ISNOTEMPTY

Examples

1. Find high-priority open incidents

fluent_query
{
  "table": "incident",
  "where": [
    ["active", "=", true],
    ["priority", "<", 3],
    ["state", "!=", 6]
  ],
  "select": ["number", "short_description", "priority", "assigned_to", "sys_updated_on"],
  "orderBy": "-priority",
  "limit": 25
}

2. Count incidents by category

fluent_query
{
  "table": "incident",
  "where": [["active", "=", true]],
  "aggregate": "COUNT",
  "groupBy": "category"
}

3. Average resolution time by assignment group

fluent_query
{
  "table": "incident",
  "where": [
    ["state", "=", 6],
    ["resolved_at", "ISNOTEMPTY"]
  ],
  "aggregate": "AVG",
  "aggregateField": "business_duration",
  "groupBy": "assignment_group"
}

4. Complex OR conditions

fluent_query
{
  "table": "change_request",
  "where": [
    ["state", "!=", 3],
    ["type", "=", "normal"]
  ],
  "orWhere": [
    ["risk", "=", "high"],
    ["priority", "=", 1]
  ],
  "select": ["number", "short_description", "risk", "state", "assignment_group"],
  "orderBy": "-sys_created_on",
  "limit": 50
}

5. Dot-walking for caller information

fluent_query
{
  "table": "incident",
  "where": [
    ["active", "=", true],
    ["caller_id", "ISNOTEMPTY"]
  ],
  "select": [
    "number",
    "short_description",
    "caller_id.name",
    "caller_id.email",
    "caller_id.department",
    "caller_id.location"
  ],
  "displayValue": true,
  "limit": 15
}

6. SLA breached incidents

fluent_query
{
  "table": "task_sla",
  "where": [
    ["has_breached", "=", true],
    ["task.active", "=", true],
    ["sla.name", "CONTAINS", "Resolution"]
  ],
  "select": ["task.number", "task.short_description", "sla.name", "business_percentage", "breach_time"],
  "orderBy": "business_percentage",
  "limit": 30
}

7. Find all servers in a specific data center

fluent_query
{
  "table": "cmdb_ci_server",
  "where": [
    ["operational_status", "=", 1],
    ["location.name", "CONTAINS", "US-East"]
  ],
  "select": ["name", "ip_address", "os", "ram", "cpu_count", "location.name"],
  "orderBy": "name",
  "limit": 100
}

8. MAX incident reassignment count

fluent_query
{
  "table": "incident",
  "where": [["sys_created_on", ">=", "2025-01-01"]],
  "aggregate": "MAX",
  "aggregateField": "reassignment_count",
  "groupBy": "assignment_group"
}

Batch Request

The batch_request tool executes multiple ServiceNow REST API operations in a single HTTP call. This reduces network round-trips by 50-70%, making it ideal for dashboards, reports, and any workflow that needs data from multiple tables.

Parameter Reference

ParameterTypeRequiredDescription
operationsarrayYesArray of REST operations (max 50)

Operation Object

FieldTypeRequiredDescription
idstringYesUnique ID to correlate responses
methodstringYesGET, POST, PATCH, or DELETE
urlstringYesAPI URL path (e.g., /api/now/table/incident?sysparm_limit=5)
bodyobjectNoRequest body for POST/PATCH operations

Examples

1. Multi-table data fetch (incidents + changes + problems)

batch_request
{
  "operations": [
    {
      "id": "active_incidents",
      "method": "GET",
      "url": "/api/now/table/incident?sysparm_query=active=true^priority<=2&sysparm_fields=number,short_description,priority,assigned_to&sysparm_limit=10"
    },
    {
      "id": "open_changes",
      "method": "GET",
      "url": "/api/now/table/change_request?sysparm_query=state=2&sysparm_fields=number,short_description,risk,type&sysparm_limit=10"
    },
    {
      "id": "recent_problems",
      "method": "GET",
      "url": "/api/now/table/problem?sysparm_query=state!=4^ORDERBYDESCsys_created_on&sysparm_fields=number,short_description,priority,problem_state&sysparm_limit=10"
    }
  ]
}

2. Bulk status updates (close multiple incidents)

batch_request
{
  "operations": [
    {
      "id": "close_inc_001",
      "method": "PATCH",
      "url": "/api/now/table/incident/a1b2c3...d6",
      "body": {
        "state": 7,
        "close_code": "Resolved",
        "close_notes": "Issue resolved via automated remediation"
      }
    },
    {
      "id": "close_inc_002",
      "method": "PATCH",
      "url": "/api/now/table/incident/b2c3d4...a7",
      "body": {
        "state": 7,
        "close_code": "Resolved",
        "close_notes": "Duplicate of INC0012345"
      }
    },
    {
      "id": "close_inc_003",
      "method": "PATCH",
      "url": "/api/now/table/incident/c3d4e5...b8",
      "body": {
        "state": 7,
        "close_code": "Not Solved",
        "close_notes": "User no longer requires assistance"
      }
    }
  ]
}

3. Mixed read/write operations (dashboard + update)

batch_request
{
  "operations": [
    {
      "id": "my_tasks",
      "method": "GET",
      "url": "/api/now/table/task?sysparm_query=assigned_to=javascript:gs.getUserID()^active=true&..."
    },
    {
      "id": "team_workload",
      "method": "GET",
      "url": "/api/now/stats/incident?sysparm_query=active=true&sysparm_count=true&sysparm_group_by=assignment_group"
    },
    {
      "id": "update_preference",
      "method": "POST",
      "url": "/api/now/table/sys_user_preference",
      "body": { "name": "dashboard.last_refresh", "value": "2025-12-01 09:00:00" }
    }
  ]
}

4. Fetch user, groups, and CI in parallel

batch_request
{
  "operations": [
    {
      "id": "user_info",
      "method": "GET",
      "url": "/api/now/table/sys_user?sysparm_query=user_name=admin&sysparm_fields=sys_id,name,email,department,location"
    },
    {
      "id": "user_groups",
      "method": "GET",
      "url": "/api/now/table/sys_user_grmember?sysparm_query=user.user_name=admin&sysparm_fields=group.name,group.sys_id&sysparm_limit=50"
    },
    {
      "id": "managed_cis",
      "method": "GET",
      "url": "/api/now/table/cmdb_ci?sysparm_query=managed_by.user_name=admin&sysparm_fields=name,sys_class_name,operational_status&sysparm_limit=20"
    }
  ]
}

Execute Script

The execute_script tool runs server-side JavaScript on your ServiceNow instance, equivalent to the Background Script module. It supports GlideRecord, GlideQuery, GlideAggregate, and all server-side APIs. This is the most powerful tool in the SDK, reserved for complex operations that REST cannot express.

Parameter Reference

ParameterTypeRequiredDescription
scriptstringYesServer-side JavaScript. Use gs.print() or gs.info() for output.
scopestringNoApplication scope (default: global)

Requirement: WRITE_ENABLED=true must be set in your environment.

Examples

1. GlideRecord query with encoded queries

execute_script
{
  "script": "var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1^assignment_groupISEMPTY');
gr.query();
var results = [];
while (gr.next()) {
  results.push({
    number: gr.getValue('number'),
    description: gr.getValue('short_description'),
    opened: gr.getValue('opened_at'),
    caller: gr.getDisplayValue('caller_id')
  });
}
gs.print(JSON.stringify({ unassigned_p1: results, count: results.length }));"
}

2. GlideAggregate for complex analytics

execute_script
{
  "script": "var ga = new GlideAggregate('incident');
ga.addEncodedQuery('sys_created_on>=javascript:gs.beginningOfLast12Months()');
ga.addAggregate('COUNT');
ga.addAggregate('AVG', 'reassignment_count');
ga.groupBy('priority');
ga.query();
var stats = [];
while (ga.next()) {
  stats.push({
    priority: ga.getValue('priority'),
    total: ga.getAggregate('COUNT'),
    avg_reassignments: ga.getAggregate('AVG', 'reassignment_count')
  });
}
gs.print(JSON.stringify({ incident_stats_12mo: stats }));"
}

3. Bulk data cleanup (deactivate stale business rules)

execute_script
{
  "script": "var gr = new GlideRecord('sys_script');
gr.addEncodedQuery('active=true^sys_updated_on<javascript:gs.daysAgoStart(365)^scriptISEMPTY');
gr.query();
var deactivated = [];
while (gr.next()) {
  deactivated.push({
    name: gr.getValue('name'),
    table: gr.getValue('collection'),
    last_updated: gr.getValue('sys_updated_on')
  });
}
gs.print(JSON.stringify({
  stale_empty_rules: deactivated,
  count: deactivated.length,
  action: 'review_before_deactivating'
}));"
}

4. Custom report generation (MTTR by priority)

execute_script
{
  "script": "var ga = new GlideAggregate('incident');
ga.addEncodedQuery('state=6^resolved_atISNOTEMPTY^sys_created_on>=javascript:gs.beginningOfLastMonth()');
ga.addAggregate('AVG', 'calendar_duration');
ga.addAggregate('COUNT');
ga.addAggregate('MIN', 'calendar_duration');
ga.addAggregate('MAX', 'calendar_duration');
ga.groupBy('priority');
ga.query();
var report = [];
while (ga.next()) {
  var avgSec = parseInt(ga.getAggregate('AVG', 'calendar_duration'));
  report.push({
    priority: ga.getValue('priority'),
    count: ga.getAggregate('COUNT'),
    avg_mttr_hours: (avgSec / 3600).toFixed(1),
    min_hours: (parseInt(ga.getAggregate('MIN', 'calendar_duration')) / 3600).toFixed(1),
    max_hours: (parseInt(ga.getAggregate('MAX', 'calendar_duration')) / 3600).toFixed(1)
  });
}
gs.print(JSON.stringify({ mttr_report_last_month: report }));"
}

5. Cross-table analysis (incidents vs. changes correlation)

execute_script
{
  "script": "var incGA = new GlideAggregate('incident');
incGA.addEncodedQuery('sys_created_on>=javascript:gs.daysAgoStart(30)');
incGA.addAggregate('COUNT');
incGA.groupBy('cmdb_ci');
incGA.orderByAggregate('COUNT');
incGA.query();
var hotspots = [];
while (incGA.next() && hotspots.length < 10) {
  var ciId = incGA.getValue('cmdb_ci');
  var ciGR = new GlideRecord('cmdb_ci');
  if (ciGR.get(ciId)) {
    var chGA = new GlideAggregate('change_request');
    chGA.addQuery('cmdb_ci', ciId);
    chGA.addEncodedQuery('sys_created_on>=javascript:gs.daysAgoStart(30)');
    chGA.addAggregate('COUNT');
    chGA.query();
    var changeCount = 0;
    if (chGA.next()) changeCount = parseInt(chGA.getAggregate('COUNT'));
    hotspots.push({
      ci_name: ciGR.getValue('name'),
      ci_class: ciGR.getValue('sys_class_name'),
      incident_count: parseInt(incGA.getAggregate('COUNT')),
      change_count: changeCount
    });
  }
}
gs.print(JSON.stringify({ ci_hotspots_30d: hotspots }));"
}

Real-World Scenarios

These end-to-end scenarios demonstrate how to combine all three tools for complex workflows.

Scenario 1: Morning Standup Automation

Automate the daily standup data gathering: batch-fetch yesterday's incidents, aggregate by team, then run a script for a custom digest.

Step 1 — batch_request
# Fetch all standup data in a single HTTP call
{
  "operations": [
    {
      "id": "yesterday_incidents",
      "method": "GET",
      "url": "/api/now/table/incident?sysparm_query=sys_created_on>=javascript:gs.daysAgoStart(1)&sysparm_fields=number,short_description,priority,state,assignment_group&sysparm_limit=100"
    },
    {
      "id": "open_changes_today",
      "method": "GET",
      "url": "/api/now/table/change_request?sysparm_query=start_date>=javascript:gs.daysAgoStart(1)^start_date<=javascript:gs.daysAgoEnd(0)&sysparm_fields=number,short_description,risk,type&sysparm_limit=50"
    },
    {
      "id": "p1_open",
      "method": "GET",
      "url": "/api/now/stats/incident?sysparm_query=active=true^priority=1&sysparm_count=true"
    }
  ]
}
Step 2 — fluent_query
# Count yesterday's incidents by assignment group
{
  "table": "incident",
  "where": [["sys_created_on", ">=", "javascript:gs.daysAgoStart(1)"]],
  "aggregate": "COUNT",
  "groupBy": "assignment_group"
}
Step 3 — execute_script
# Build a formatted standup digest
{
  "script": "var digest = { date: new GlideDateTime().getDisplayValue(), teams: {} };
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('sys_created_on>=javascript:gs.daysAgoStart(1)');
ga.addAggregate('COUNT');
ga.groupBy('assignment_group');
ga.groupBy('priority');
ga.query();
while (ga.next()) {
  var team = ga.getDisplayValue('assignment_group') || 'Unassigned';
  if (!digest.teams[team]) digest.teams[team] = { total: 0, by_priority: {} };
  var count = parseInt(ga.getAggregate('COUNT'));
  digest.teams[team].total += count;
  digest.teams[team].by_priority['P' + ga.getValue('priority')] = count;
}
gs.print(JSON.stringify(digest));"
}

Scenario 2: Technical Debt Scan

Identify stale business rules, find duplicates by table, and analyze script complexity.

Step 1 — fluent_query
# Find business rules not updated in 2+ years
{
  "table": "sys_script",
  "where": [
    ["active", "=", true],
    ["sys_updated_on", "<", "javascript:gs.daysAgoStart(730)"]
  ],
  "select": ["name", "collection", "when", "sys_updated_on", "sys_created_by"],
  "orderBy": "sys_updated_on",
  "limit": 100
}
Step 2 — fluent_query
# Aggregate stale business rules by table
{
  "table": "sys_script",
  "where": [
    ["active", "=", true],
    ["sys_updated_on", "<", "javascript:gs.daysAgoStart(730)"]
  ],
  "aggregate": "COUNT",
  "groupBy": "collection"
}
Step 3 — execute_script
# Analyze script complexity: line count and deprecated API usage
{
  "script": "var gr = new GlideRecord('sys_script');
gr.addEncodedQuery('active=true^sys_updated_on<javascript:gs.daysAgoStart(730)');
gr.query();
var analysis = { total: 0, deprecated_apis: 0, long_scripts: 0, details: [] };
while (gr.next()) {
  analysis.total++;
  var script = gr.getValue('script') || '';
  var lines = script.split('\\n').length;
  var hasDeprecated = script.indexOf('GlideEncrypter') > -1
    || script.indexOf('current.update()') > -1;
  if (hasDeprecated) analysis.deprecated_apis++;
  if (lines > 100) analysis.long_scripts++;
  if (hasDeprecated || lines > 100) {
    analysis.details.push({
      name: gr.getValue('name'),
      table: gr.getValue('collection'),
      lines: lines,
      has_deprecated: hasDeprecated
    });
  }
}
gs.print(JSON.stringify(analysis));"
}

Scenario 3: SLA Compliance Dashboard

Aggregate SLA data across the organization, batch-fetch breached items, and generate a remediation plan.

Step 1 — fluent_query
# Count SLA breaches by SLA definition
{
  "table": "task_sla",
  "where": [
    ["has_breached", "=", true],
    ["task.active", "=", true]
  ],
  "aggregate": "COUNT",
  "groupBy": "sla"
}
Step 2 — batch_request
# Fetch breached SLAs for incidents and changes in parallel
{
  "operations": [
    {
      "id": "breached_incidents",
      "method": "GET",
      "url": "/api/now/table/task_sla?sysparm_query=has_breached=true^task.sys_class_name=incident^task.active=true&sysparm_fields=task.number,task.short_description,sla.name,business_percentage&sysparm_limit=50"
    },
    {
      "id": "breached_changes",
      "method": "GET",
      "url": "/api/now/table/task_sla?sysparm_query=has_breached=true^task.sys_class_name=change_request^task.active=true&sysparm_fields=task.number,task.short_description,sla.name,business_percentage&sysparm_limit=50"
    }
  ]
}
Step 3 — execute_script
# Generate SLA remediation plan with owner assignments
{
  "script": "var ga = new GlideAggregate('task_sla');
ga.addEncodedQuery('has_breached=true^task.active=true');
ga.addAggregate('COUNT');
ga.groupBy('task.assignment_group');
ga.orderByAggregate('COUNT');
ga.query();
var plan = [];
while (ga.next()) {
  var groupId = ga.getValue('task.assignment_group');
  var grp = new GlideRecord('sys_user_group');
  var manager = 'No manager assigned';
  if (grp.get(groupId) && grp.getValue('manager'))
    manager = grp.getDisplayValue('manager');
  plan.push({
    team: ga.getDisplayValue('task.assignment_group'),
    breached_count: parseInt(ga.getAggregate('COUNT')),
    manager: manager,
    action: 'Review and reassign or escalate breached tasks'
  });
}
gs.print(JSON.stringify({ remediation_plan: plan, generated: new GlideDateTime().getDisplayValue() }));"
}

Fluent SDK Tools (v2.0.0)

SDK v2.0.0 introduces four new Fluent tools that streamline the full lifecycle of Fluent component development — from scaffolding to validation to build. These tools are available via the MCP integration and the SDK CLI.

ToolPurposeWrite Required
fluent_initScaffold a new Fluent project with now.config.json, component templates, and directory structureYes
fluent_explainGet documentation for Fluent SDK APIs, types, and component patternsNo (read-only)
fluent_buildCompile Fluent components into deployable artifacts with optional minification and source mapsYes
fluent_validateValidate a Fluent project — check now.config.json, component schemas, and dependency integrityNo (read-only)

fluent_init

Scaffolds a new Fluent project directory with all required configuration and starter templates.

fluent_init
{
  "name": "my-fluent-app",
  "scope": "x_myapp",
  "instance": "your-instance.service-now.com",
  "template": "workspace"
}

fluent_explain

Retrieves documentation for any Fluent SDK concept, type, or pattern. Useful for interactive AI assistance.

fluent_explain
{
  "topic": "FluentComponent lifecycle hooks"
}

fluent_build

Compiles the Fluent project into deployable artifacts. Supports minification, source maps, and output directory configuration.

fluent_build
{
  "projectDir": "./my-fluent-app",
  "outDir": "./dist",
  "minify": true,
  "sourceMaps": true
}

fluent_validate

Validates the project structure, now.config.json, component schemas, and dependency integrity. Returns errors, warnings, and passed checks.

fluent_validate
{
  "projectDir": "./my-fluent-app"
}

Fluent Type Definitions

The v2.0.0 SDK exports TypeScript types for all Fluent concepts. Import them from nowaikit-sdk/fluent:

TypeScript
import type {
  NowConfig,
  FluentComponent,
  FluentPage,
  FluentTheme,
  FluentValidationResult,
  FluentBuildOptions,
  FluentInitOptions
} from 'nowaikit-sdk/fluent';

Best Practices

Performance Tips

  • Use select to limit fields. Returning only the fields you need reduces response size by 60-80%. Never fetch * when you only need 5 fields.
  • Batch related reads. If you need data from 3 tables, use batch_request instead of 3 sequential fluent_query calls. This saves 2 round-trips.
  • Set appropriate limit values. The default is 20 records. If you need only a count, use aggregate: "COUNT" instead of fetching records.
  • Use orderBy with limit. Sorting server-side is far more efficient than fetching all records and sorting client-side.
  • Prefer fluent_query aggregates over execute_script. REST-based aggregation is faster and uses fewer instance resources than running GlideAggregate in a Background Script.

Error Handling

  • Batch partial failures. Each operation in a batch_request returns its own status code. Check individual responses even if the batch call succeeds.
  • Script timeouts. Background Scripts have a 30-second timeout on most instances. Break long-running scripts into smaller batches.
  • Invalid table names. fluent_query returns a clear error if the table does not exist. Always validate table names before complex queries.
  • Empty results. Aggregate queries on empty result sets return 0 for COUNT and null for AVG/SUM/MIN/MAX. Handle these cases gracefully.

Security Considerations

  • execute_script runs with admin privileges. Only enable WRITE_ENABLED=true on instances where script execution is acceptable. Never enable it on production without review.
  • Batch write operations require WRITE_ENABLED. GET-only batches work in read-only mode. Any POST, PATCH, or DELETE operation in a batch requires write permissions.
  • Never embed credentials in scripts. Use gs.getProperty() or system properties to retrieve sensitive values instead of hardcoding them in execute_script.
  • Scope your scripts. When working with scoped applications, pass the scope parameter to execute_script to run in the correct application context.
  • ACL enforcement still applies. fluent_query and batch_request results are filtered by the authenticated user's ACLs. The tools respect ServiceNow's security model.

When to Use Each Tool

Use CaseBest ToolWhy
Simple filtered queriesfluent_queryCleanest syntax, automatic encoding
COUNT/AVG/SUM/MIN/MAXfluent_queryNative aggregate support, no scripting needed
Dashboard data (multi-table)batch_requestOne HTTP call for all data sources
Bulk record updatesbatch_requestAtomic batch with individual error handling
Cross-table joins/correlationsexecute_scriptREST cannot express joins; GlideRecord can
Complex business logicexecute_scriptFull server-side API access (GlideSystem, etc.)
Data migration/cleanupexecute_scriptLoop-and-update patterns with transaction control