skills/reporting/query-generation/SKILL.md
Generate ServiceNow queries, GlideAggregate scripts, and reporting filters from natural language descriptions
npx skillsauth add happy-technologies-llc/happy-servicenow-skills query-generationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill covers translating natural language descriptions into ServiceNow queries, scripts, and report configurations:
When to use: When users describe data needs in natural language, when building report filters, when creating script-based data retrieval, or when constructing complex encoded queries for lists and modules.
itil, report_user, admin, or any role with read access to target tablesreporting/analytics-generation for dashboard creationServiceNow encoded queries use a specific syntax for filtering records:
Operators Reference:
| Operator | Syntax | Example |
|----------|--------|---------|
| Equals | = | priority=1 |
| Not equals | != | state!=6 |
| Contains | LIKE | short_descriptionLIKEnetwork |
| Does not contain | NOT LIKE | short_descriptionNOT LIKEtest |
| Starts with | STARTSWITH | numberSTARTSWITHINC |
| Ends with | ENDSWITH | [email protected] |
| Is empty | ISEMPTY | assignment_groupISEMPTY |
| Is not empty | ISNOTEMPTY | resolved_atISNOTEMPTY |
| Greater than | > | priority>2 |
| Less than | < | priority<3 |
| Greater or equal | >= | sys_created_on>=2025-01-01 |
| Less or equal | <= | sys_created_on<=2025-12-31 |
| In list | IN | state IN 1,2,3 |
| Not in list | NOT IN | priority NOT IN 4,5 |
| Between | BETWEEN | sys_created_onBETWEEN2025-01-01@2025-06-30 |
| Instance of | INSTANCEOF | sys_class_nameINSTANCEOFtask |
| AND | ^ | active=true^priority=1 |
| OR | ^OR | priority=1^ORpriority=2 |
| New query (OR group) | ^NQ | priority=1^NQstate=6 |
Date Operators: | Function | Syntax | Meaning | |----------|--------|---------| | Today | javascript:gs.daysAgo(0) | Current day | | Last 7 days | javascript:gs.daysAgo(7) | Past week | | Last 30 days | javascript:gs.daysAgo(30) | Past month | | This month start | javascript:gs.beginningOfThisMonth() | First day of current month | | This quarter start | javascript:gs.beginningOfThisQuarter() | First day of current quarter | | This year start | javascript:gs.beginningOfThisYear() | First day of current year | | Last month start | javascript:gs.beginningOfLastMonth() | First day of previous month | | End of last month | javascript:gs.endOfLastMonth() | Last day of previous month |
Examples of natural language to query translation:
| Natural Language | Encoded Query |
|-----------------|---------------|
| "All open critical incidents" | active=true^priority=1 |
| "Incidents assigned to Service Desk opened this week" | assignment_group.name=Service Desk^sys_created_on>=javascript:gs.daysAgo(7) |
| "Unresolved P1 or P2 incidents" | active=true^priority=1^ORpriority=2 |
| "Changes approved but not implemented" | state=authorized^ORstate=scheduled |
| "Users without a manager" | managerISEMPTY^active=true |
| "Incidents created in Q1 2025" | sys_created_on>=2025-01-01^sys_created_on<2025-04-01 |
| "High priority incidents not updated in 3 days" | priority IN 1,2^sys_updated_on<javascript:gs.daysAgo(3)^active=true |
Before using a query, verify the fields exist on the target table.
MCP Approach:
Tool: SN-Get-Table-Schema
Parameters:
table_name: incident
REST Alternative:
curl -u "$SN_USER:$SN_PASS" \
"$SN_INSTANCE/api/now/table/sys_dictionary?sysparm_query=name=incident^elementISNOTEMPTY^active=true&sysparm_fields=element,column_label,internal_type&sysparm_limit=200" \
-H "Accept: application/json"
MCP Approach:
Tool: SN-Query-Table
Parameters:
table_name: incident
query: active=true^priority=1^assignment_groupISEMPTY
fields: sys_id,number,short_description,priority,state,sys_created_on
limit: 10
REST Alternative:
curl -u "$SN_USER:$SN_PASS" \
"$SN_INSTANCE/api/now/table/incident?sysparm_query=active=true^priority=1^assignment_groupISEMPTY&sysparm_fields=sys_id,number,short_description,priority,state&sysparm_limit=10" \
-H "Accept: application/json"
Basic GlideRecord query:
// Natural Language: "Get all active incidents assigned to the Network team"
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.addQuery('assignment_group.name', 'Network');
gr.orderByDesc('sys_created_on');
gr.setLimit(100);
gr.query();
while (gr.next()) {
gs.info('Incident: {0} - {1} (Priority: {2})',
gr.getValue('number'),
gr.getValue('short_description'),
gr.getDisplayValue('priority'));
}
GlideRecord with complex conditions:
// Natural Language: "Find incidents that are either P1 or P2,
// created in the last 7 days, and not yet assigned"
var gr = new GlideRecord('incident');
var qc = gr.addQuery('priority', '1');
qc.addOrCondition('priority', '2');
gr.addQuery('sys_created_on', '>=', gs.daysAgo(7));
gr.addQuery('assigned_to', '');
gr.addActiveQuery();
gr.query();
gs.info('Found {0} unassigned high-priority incidents', gr.getRowCount());
GlideRecord with encoded query:
// Natural Language: "All resolved incidents from this month
// where the resolution was a workaround"
var gr = new GlideRecord('incident');
gr.addEncodedQuery('state=6^sys_resolved_on>=javascript:gs.beginningOfThisMonth()^close_notesLIKEworkaround');
gr.query();
while (gr.next()) {
gs.info('{0}: {1}', gr.getValue('number'), gr.getValue('close_notes'));
}
Count with grouping:
// Natural Language: "How many incidents are open per priority?"
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.orderBy('priority');
ga.query();
while (ga.next()) {
gs.info('Priority {0}: {1} incidents',
ga.getDisplayValue('priority'),
ga.getAggregate('COUNT'));
}
Average calculation:
// Natural Language: "What is the average resolution time by category?"
var ga = new GlideAggregate('incident');
ga.addQuery('state', 6); // Resolved
ga.addQuery('sys_resolved_on', '>=', gs.daysAgo(90));
ga.addAggregate('AVG', 'business_duration');
ga.groupBy('category');
ga.query();
while (ga.next()) {
var avgDuration = ga.getAggregate('AVG', 'business_duration');
var hours = (parseInt(avgDuration, 10) / 3600).toFixed(1);
gs.info('Category: {0} - Avg Resolution: {1} hours',
ga.getDisplayValue('category'), hours);
}
Sum with filtering:
// Natural Language: "Total cost of all hardware assets purchased this year"
var ga = new GlideAggregate('alm_hardware');
ga.addQuery('purchase_date', '>=', gs.beginningOfThisYear());
ga.addAggregate('SUM', 'cost');
ga.query();
if (ga.next()) {
gs.info('Total hardware cost this year: ${0}',
ga.getAggregate('SUM', 'cost'));
}
Multiple aggregates:
// Natural Language: "For each assignment group, show count,
// average priority, and oldest open incident"
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.addAggregate('AVG', 'priority');
ga.addAggregate('MIN', 'sys_created_on');
ga.groupBy('assignment_group');
ga.orderByAggregate('COUNT');
ga.query();
while (ga.next()) {
gs.info('Group: {0} | Count: {1} | Avg Priority: {2} | Oldest: {3}',
ga.getDisplayValue('assignment_group'),
ga.getAggregate('COUNT'),
parseFloat(ga.getAggregate('AVG', 'priority')).toFixed(1),
ga.getAggregate('MIN', 'sys_created_on'));
}
Create a report with the generated query:
MCP Approach:
Tool: SN-Create-Record
Parameters:
table_name: sys_report
data:
title: "Unassigned Critical Incidents"
table: "incident"
type: "list"
filter: "active=true^priority=1^assigned_toISEMPTY"
is_published: true
active: true
Create a chart report with query:
MCP Approach:
Tool: SN-Create-Record
Parameters:
table_name: sys_report
data:
title: "Monthly Incident Trend by Priority"
table: "incident"
type: "line"
field: "priority"
filter: "sys_created_on>=javascript:gs.monthsAgo(12)"
aggregate: "COUNT"
trend_field: "sys_created_on"
interval: "month"
is_published: true
active: true
REST Alternative:
curl -u "$SN_USER:$SN_PASS" \
"$SN_INSTANCE/api/now/table/sys_report" \
-H "Content-Type: application/json" \
-X POST \
-d '{
"title": "Monthly Incident Trend by Priority",
"table": "incident",
"type": "line",
"field": "priority",
"filter": "sys_created_on>=javascript:gs.monthsAgo(12)",
"aggregate": "COUNT",
"trend_field": "sys_created_on",
"interval": "month",
"is_published": true
}'
Dot-walking (related table fields):
// Natural Language: "Incidents where the caller's department is IT"
query: caller_id.department.name=IT^active=true
Dynamic reference qualifiers:
// Natural Language: "Incidents assigned to the current user's group"
query: assignment_group=javascript:getMyGroups()^active=true
Relative date queries:
// Natural Language: "Changes scheduled for next week"
query: start_date>=javascript:gs.daysAgo(-1)^start_date<javascript:gs.daysAgo(-8)
INSTANCEOF for hierarchy queries:
// Natural Language: "All tasks including incidents, changes, and problems"
query: sys_class_nameINSTANCEOFtask^active=true
| Action | MCP Tool | REST Endpoint | |--------|----------|---------------| | Test query | SN-Query-Table | GET /api/now/table/{table}?sysparm_query={query} | | Get table schema | SN-Get-Table-Schema | GET /api/now/table/sys_dictionary | | Create report | SN-Create-Record | POST /api/now/table/sys_report | | Create PA indicator | SN-Create-Record | POST /api/now/table/pa_indicators | | Validate field | SN-Query-Table | GET /api/now/table/sys_dictionary |
priority IN 1,2,3 is cleaner than chained OR conditionsSymptom: Encoded query returns empty set when data should exist Causes:
Tool: SN-Get-Table-Schema
Parameters:
table_name: [target_table]
Then test a simplified version of the query:
Tool: SN-Query-Table
Parameters:
table_name: [target_table]
query: [single_condition_only]
fields: sys_id
limit: 1
Symptom: Query is too broad, returning excessive records Causes:
Tool: SN-Query-Table
Parameters:
table_name: incident
query: active=true^priority=1^sys_created_on>=javascript:gs.daysAgo(30)
fields: sys_id
limit: 1
Symptom: Date-based filters include unexpected records Causes:
// Use gs.daysAgo() instead of hardcoded dates
query: sys_created_on>=javascript:gs.daysAgo(30)
// For specific date ranges, include full datetime
query: sys_created_on>=2025-01-01 00:00:00^sys_created_on<2025-02-01 00:00:00
Symptom: Aggregate counts do not match manual counts Causes:
// Verify with GlideRecord count
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1');
gr.query();
gs.info('GlideRecord count: ' + gr.getRowCount());
Symptom: Query with dot-walking is very slow Cause: Multiple levels of dot-walking create expensive JOINs Solution: Limit dot-walk depth and add indexed conditions first:
// BAD: Deep dot-walk as primary filter
query: caller_id.department.company.name=Acme
// GOOD: Add indexed conditions first, limit dot-walk depth
query: active=true^priority IN 1,2^caller_id.department.name=IT
Natural Language: "Show me all P1 and P2 incidents from the last 30 days
that are still open, assigned to either the Network or Server team,
and have not been updated in the last 48 hours"
Encoded Query:
active=true^priority IN 1,2^sys_created_on>=javascript:gs.daysAgo(30)^assignment_group.nameLIKENetwork^ORassignment_group.nameLIKEServer^sys_updated_on<javascript:gs.daysAgo(2)
Test:
Tool: SN-Query-Table
Parameters:
table_name: incident
query: active=true^priority IN 1,2^sys_created_on>=javascript:gs.daysAgo(30)^sys_updated_on<javascript:gs.daysAgo(2)
fields: sys_id,number,priority,assignment_group,sys_updated_on
limit: 20
Natural Language: "Create a summary showing the number of incidents
created each month this year, broken down by category"
GlideAggregate Script:
var ga = new GlideAggregate('incident');
ga.addQuery('sys_created_on', '>=', gs.beginningOfThisYear());
ga.addAggregate('COUNT');
ga.groupBy('category');
ga.addTrend('sys_created_on', 'month');
ga.query();
while (ga.next()) {
gs.info('Month: {0} | Category: {1} | Count: {2}',
ga.getValue('timeref'),
ga.getDisplayValue('category'),
ga.getAggregate('COUNT'));
}
Natural Language: "I need a pie chart showing the distribution
of change requests by risk level for the current quarter"
Tool: SN-Create-Record
Parameters:
table_name: sys_report
data:
title: "Change Requests by Risk Level (Current Quarter)"
table: "change_request"
type: "pie"
field: "risk"
filter: "sys_created_on>=javascript:gs.beginningOfThisQuarter()"
aggregate: "COUNT"
is_published: true
active: true
reporting/analytics-generation - Build full dashboards with PA indicatorsdevelopment/code-assist - Generate complete scripts with query logicitsm/incident-management - Incident data model and field referenceadmin/list-configuration - Apply queries to list views and filterstesting
Manage supplier onboarding, qualification, performance monitoring, and offboarding with auditable lifecycle controls
tools
Identify emerging risks, prioritize intake signals, and route candidates into formal GRC risk assessment workflows
documentation
Screen inbound documents for completeness, policy risk, and routing readiness before extraction or case workflows
testing
Generate concise task summaries with status, timeline, blockers, SLA risk, and recommended next actions