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:
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.
| Tool | Purpose | Write Required |
|---|---|---|
fluent_query | GlideQuery-style chained query builder | No (read-only) |
batch_request | Multi-operation REST in one HTTP call | Only for POST/PATCH/DELETE |
execute_script | Server-side Background Script | Yes (always) |
fluent_sdk_query | Read-only now-sdk query via the SDK CLI (4.8) | No (read-only) |
fluent_version | Installed @servicenow/sdk version & upgrade hints | No |
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
| Parameter | Type | Required | Description |
|---|---|---|---|
table | string | Yes | Table name (e.g., "incident", "cmdb_ci") |
where | array | No | AND conditions: [field, operator, value]. Two-element arrays default to = |
orWhere | array | No | OR conditions (same format as where) |
select | array | No | Fields to return. Supports dot-walking (e.g., "caller_id.email") |
aggregate | string | No | Aggregate operation: COUNT, AVG, SUM, MIN, MAX |
aggregateField | string | No | Field to aggregate on (required for AVG, SUM, MIN, MAX) |
groupBy | string | No | Field to group aggregate results by |
orderBy | string | No | Sort field. Prefix with "-" for descending |
limit | number | No | Max records (default: 20, max: 200) |
displayValue | boolean | No | Return display values instead of sys_ids |
Supported Operators
= != > >= < <= LIKE STARTSWITH CONTAINS IN NOT IN ISEMPTY ISNOTEMPTY
Examples
1. Find high-priority open incidents
{
"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
{
"table": "incident",
"where": [["active", "=", true]],
"aggregate": "COUNT",
"groupBy": "category"
}
3. Average resolution time by assignment group
{
"table": "incident",
"where": [
["state", "=", 6],
["resolved_at", "ISNOTEMPTY"]
],
"aggregate": "AVG",
"aggregateField": "business_duration",
"groupBy": "assignment_group"
}
4. Complex OR conditions
{
"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
{
"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
{
"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
{
"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
{
"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
| Parameter | Type | Required | Description |
|---|---|---|---|
operations | array | Yes | Array of REST operations (max 50) |
Operation Object
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique ID to correlate responses |
method | string | Yes | GET, POST, PATCH, or DELETE |
url | string | Yes | API URL path (e.g., /api/now/table/incident?sysparm_limit=5) |
body | object | No | Request body for POST/PATCH operations |
Examples
1. Multi-table data fetch (incidents + changes + problems)
{
"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)
{
"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)
{
"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
{
"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
| Parameter | Type | Required | Description |
|---|---|---|---|
script | string | Yes | Server-side JavaScript. Use gs.print() or gs.info() for output. |
scope | string | No | Application scope (default: global) |
Requirement: WRITE_ENABLED=true must be set in your environment.
Examples
1. GlideRecord query with encoded queries
{
"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
{
"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)
{
"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)
{
"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)
{
"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.
# 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"
}
]
}
# Count yesterday's incidents by assignment group
{
"table": "incident",
"where": [["sys_created_on", ">=", "javascript:gs.daysAgoStart(1)"]],
"aggregate": "COUNT",
"groupBy": "assignment_group"
}
# 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.
# 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
}
# Aggregate stale business rules by table
{
"table": "sys_script",
"where": [
["active", "=", true],
["sys_updated_on", "<", "javascript:gs.daysAgoStart(730)"]
],
"aggregate": "COUNT",
"groupBy": "collection"
}
# 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.
# Count SLA breaches by SLA definition
{
"table": "task_sla",
"where": [
["has_breached", "=", true],
["task.active", "=", true]
],
"aggregate": "COUNT",
"groupBy": "sla"
}
# 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"
}
]
}
# 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.
| Tool | Purpose | Write Required |
|---|---|---|
fluent_init | Scaffold a new Fluent project with now.config.json, component templates, and directory structure | Yes |
fluent_explain | Get documentation for Fluent SDK APIs, types, and component patterns | No (read-only) |
fluent_build | Compile Fluent components into deployable artifacts with optional minification and source maps | Yes |
fluent_validate | Validate a Fluent project — check now.config.json, component schemas, and dependency integrity | No (read-only) |
fluent_init
Scaffolds a new Fluent project directory with all required configuration and starter templates.
{
"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.
{
"topic": "FluentComponent lifecycle hooks"
}
fluent_build
Compiles the Fluent project into deployable artifacts. Supports minification, source maps, and output directory configuration.
{
"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.
{
"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:
import type {
NowConfig,
FluentComponent,
FluentPage,
FluentTheme,
FluentValidationResult,
FluentBuildOptions,
FluentInitOptions
} from 'nowaikit-sdk/fluent';
Best Practices
Performance Tips
- Use
selectto 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_requestinstead of 3 sequentialfluent_querycalls. This saves 2 round-trips. - Set appropriate
limitvalues. The default is 20 records. If you need only a count, useaggregate: "COUNT"instead of fetching records. - Use
orderBywithlimit. Sorting server-side is far more efficient than fetching all records and sorting client-side. - Prefer
fluent_queryaggregates overexecute_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_requestreturns 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_queryreturns 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
0for COUNT andnullfor AVG/SUM/MIN/MAX. Handle these cases gracefully.
Security Considerations
execute_scriptruns with admin privileges. Only enableWRITE_ENABLED=trueon 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 inexecute_script. - Scope your scripts. When working with scoped applications, pass the
scopeparameter toexecute_scriptto run in the correct application context. - ACL enforcement still applies.
fluent_queryandbatch_requestresults are filtered by the authenticated user's ACLs. The tools respect ServiceNow's security model.
When to Use Each Tool
| Use Case | Best Tool | Why |
|---|---|---|
| Simple filtered queries | fluent_query | Cleanest syntax, automatic encoding |
| COUNT/AVG/SUM/MIN/MAX | fluent_query | Native aggregate support, no scripting needed |
| Dashboard data (multi-table) | batch_request | One HTTP call for all data sources |
| Bulk record updates | batch_request | Atomic batch with individual error handling |
| Cross-table joins/correlations | execute_script | REST cannot express joins; GlideRecord can |
| Complex business logic | execute_script | Full server-side API access (GlideSystem, etc.) |
| Data migration/cleanup | execute_script | Loop-and-update patterns with transaction control |



