Counting records in ServiceNow correctly requires understanding GlideAggregate. Using GlideRecord to count records works but is significantly less efficient — GlideRecord loads every matching record into memory before counting. GlideAggregate pushes the COUNT operation to the database and returns only the number.
Basic COUNT
// Count all active incidents
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT');
ga.addEncodedQuery('active=true');
ga.query();
ga.next();
var count = ga.getAggregate('COUNT');
gs.log('Active incidents: ' + count);
// Output: "Active incidents: 247"
COUNT with groupBy — Counts Per Group
The most useful pattern — count records broken down by a field value:
// Count incidents grouped by priority
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT');
ga.addEncodedQuery('active=true');
ga.groupBy('priority');
ga.orderBy('priority');
ga.query();
while (ga.next()) {
var priority = ga.getDisplayValue('priority');
var count = ga.getAggregate('COUNT');
gs.log(priority + ': ' + count);
}
// Output:
// "1 - Critical: 3"
// "2 - High: 12"
// "3 - Moderate: 48"
COUNT on a Specific Field
You can count a specific field rather than all rows. This is useful for counting non-null values:
// Count incidents where assigned_to is populated
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT', 'assigned_to');
ga.addEncodedQuery('active=true');
ga.query();
ga.next();
var assignedCount = ga.getAggregate('COUNT', 'assigned_to');
gs.log('Assigned incidents: ' + assignedCount);
COUNT DISTINCT — Unique Values
// Count how many unique assignment groups have active incidents
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT DISTINCT', 'assignment_group');
ga.addEncodedQuery('active=true');
ga.query();
ga.next();
var uniqueGroups = ga.getAggregate('COUNT DISTINCT', 'assignment_group');
gs.log('Groups with active incidents: ' + uniqueGroups);
Multiple Aggregates in One Query
// Get count, min, and max in a single database call
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT');
ga.addAggregate('MIN', 'sys_created_on');
ga.addAggregate('MAX', 'sys_created_on');
ga.addEncodedQuery('active=true^priority=1');
ga.query();
if (ga.next()) {
gs.log('Count: ' + ga.getAggregate('COUNT'));
gs.log('Oldest: ' + ga.getAggregate('MIN', 'sys_created_on'));
gs.log('Newest: ' + ga.getAggregate('MAX', 'sys_created_on'));
}
GlideAggregate vs GlideRecord for Counting
| Method | How it works | Performance |
| GlideAggregate COUNT | Database-level COUNT(*) | Extremely fast — one DB call |
| GlideRecord + getRowCount() | Loads all matching records | Slow on large tables |
| GlideRecord loop counter | Iterates every record | Slowest — avoid entirely |
On a table with 100,000 records, GlideAggregate COUNT returns in milliseconds. GlideRecord getRowCount() may take several seconds.
Using GlideAggregate in a Script Include
// Reusable function to get record count
function getRecordCount(table, encodedQuery) {
var ga = new GlideAggregate(table);
ga.addAggregate('COUNT');
if (encodedQuery) {
ga.addEncodedQuery(encodedQuery);
}
ga.query();
if (ga.next()) {
return parseInt(ga.getAggregate('COUNT'));
}
return 0;
}
// Usage
var p1Count = getRecordCount('incident', 'active=true^priority=1');
gs.log('P1 count: ' + p1Count);
Common Mistake — Calling getAggregate Before next()
// ❌ Wrong — getAggregate called before next()
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT');
ga.query();
var count = ga.getAggregate('COUNT'); // Returns null
// ✅ Correct — always call next() first
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT');
ga.query();
ga.next(); // Must call this before getAggregate
var count = ga.getAggregate('COUNT'); // Returns the count
COUNT DISTINCT — counting unique values
// How many distinct users have submitted incidents this month?
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('opened_atONThis month@javascript:gs.beginningOfThisMonth()@javascript:gs.endOfThisMonth()');
ga.addAggregate('COUNT DISTINCT', 'caller_id');
ga.query();
if (ga.next()) {
var uniqueCallers = ga.getAggregate('COUNT DISTINCT', 'caller_id');
gs.log('Unique callers this month: ' + uniqueCallers);
}
COUNT DISTINCT is particularly useful for metrics like unique users, unique assets affected, or distinct categories without duplicates inflating the count.
SUM — totalling numeric fields
// Total cost of all completed change requests this quarter
var ga = new GlideAggregate('change_request');
ga.addEncodedQuery('state=3^close_code!=Unsuccessful');
ga.addAggregate('SUM', 'u_implementation_cost');
ga.query();
if (ga.next()) {
var totalCost = ga.getAggregate('SUM', 'u_implementation_cost');
gs.log('Total change implementation cost: ' + totalCost);
}
AVG — calculating averages
// Average resolution time for P1 incidents this month (in seconds)
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('priority=1^state=6^resolved_atONThis month@javascript:gs.beginningOfThisMonth()@javascript:gs.endOfThisMonth()');
ga.addAggregate('AVG', 'u_resolution_minutes');
ga.query();
if (ga.next()) {
var avgMinutes = parseFloat(ga.getAggregate('AVG', 'u_resolution_minutes'));
gs.log('Average P1 resolution time: ' + avgMinutes.toFixed(1) + ' minutes');
}
MIN and MAX
// Oldest unresolved incident
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('active=true^state!=6');
ga.addAggregate('MIN', 'opened_at');
ga.query();
if (ga.next()) {
gs.log('Oldest open incident was opened: ' + ga.getAggregate('MIN', 'opened_at'));
}
// Most recently created CI
var ga2 = new GlideAggregate('cmdb_ci');
ga2.addEncodedQuery('operational_status=1');
ga2.addAggregate('MAX', 'sys_created_on');
ga2.query();
if (ga2.next()) {
gs.log('Most recent CI created: ' + ga2.getAggregate('MAX', 'sys_created_on'));
}
Multiple aggregates in one query
You can request multiple aggregate functions in a single GlideAggregate query — avoiding multiple round trips to the database:
// Get count, min, and max open time in one query
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('priority=1^active=true');
ga.addAggregate('COUNT');
ga.addAggregate('MIN', 'opened_at');
ga.addAggregate('MAX', 'opened_at');
ga.query();
if (ga.next()) {
gs.log('P1 open count: ' + ga.getAggregate('COUNT'));
gs.log('Oldest: ' + ga.getAggregate('MIN', 'opened_at'));
gs.log('Newest: ' + ga.getAggregate('MAX', 'opened_at'));
}
Using GlideAggregate in Script Includes for dashboards
GlideAggregate is the engine behind most Performance Analytics indicators and custom dashboards. Wrapping aggregate queries in a Script Include keeps the logic reusable and testable:
var IncidentMetrics = Class.create();
IncidentMetrics.prototype = {
initialize: function() {},
getOpenCountByPriority: function() {
var result = {};
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('active=true');
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.orderBy('priority');
ga.query();
while (ga.next()) {
var p = ga.getDisplayValue('priority');
result[p] = parseInt(ga.getAggregate('COUNT'));
}
return result;
},
type: 'IncidentMetrics'
};
GlideAggregate vs GlideRecord counting — the performance case
The performance difference between GlideAggregate and counting with a GlideRecord loop is not academic — it is the difference between a query that returns in milliseconds and one that loads thousands of objects into memory. GlideRecord loads the full record object for every row it retrieves. GlideAggregate pushes the COUNT to the database engine and returns a single number. With RaptorDB's columnar index, GlideAggregate operations are even faster on large tables because the column-store is specifically optimised for aggregation queries.
The Instance Scan "GlideRecord count" finding specifically catches the pattern of using a GlideRecord loop to count records. If you see that finding in your instance, replacing every occurrence with GlideAggregate is one of the highest-ROI performance fixes available.
Related guides: GlideAggregate complete guide · GlideRecord performance · Script Includes · Performance Analytics · Scheduled Jobs
Ordering aggregate results
// Order by the aggregate value (highest count first)
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('active=true');
ga.addAggregate('COUNT');
ga.groupBy('assignment_group');
ga.orderByAggregateDesc('COUNT');
ga.setLimit(10); // top 10 groups
ga.query();
while (ga.next()) {
gs.log(ga.getDisplayValue('assignment_group') + ': ' + ga.getAggregate('COUNT'));
}
// Order by a field value (alphabetical by group name)
ga.orderBy('assignment_group');
ga.orderByAggregateDesc('COUNT'); // then by count desc
Combining GlideAggregate with a Script Include for a Gauge widget
A common pattern is exposing aggregate data to a Service Portal widget through a GlideAjax-callable Script Include:
// Script Include: DashboardMetrics (Client callable: true)
var DashboardMetrics = Class.create();
DashboardMetrics.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getITSMSummary: function() {
var result = {
openP1: 0, openP2: 0,
resolvedToday: 0, slaBreached: 0
};
// P1 count
var ga1 = new GlideAggregate('incident');
ga1.addEncodedQuery('active=true^priority=1');
ga1.addAggregate('COUNT');
ga1.query();
if (ga1.next()) result.openP1 = parseInt(ga1.getAggregate('COUNT'));
// P2 count
var ga2 = new GlideAggregate('incident');
ga2.addEncodedQuery('active=true^priority=2');
ga2.addAggregate('COUNT');
ga2.query();
if (ga2.next()) result.openP2 = parseInt(ga2.getAggregate('COUNT'));
// Resolved today
var ga3 = new GlideAggregate('incident');
ga3.addEncodedQuery('state=6^resolved_atONToday@javascript:gs.beginningOfToday()@javascript:gs.endOfToday()');
ga3.addAggregate('COUNT');
ga3.query();
if (ga3.next()) result.resolvedToday = parseInt(ga3.getAggregate('COUNT'));
return JSON.stringify(result);
},
type: 'DashboardMetrics'
});
Handling null results safely
// Always check ga.next() before reading aggregate — returns false if no matching records
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('active=true^priority=1^state=6'); // may return 0 records
ga.addAggregate('COUNT');
ga.query();
var p1ResolvedCount = 0;
if (ga.next()) {
var raw = ga.getAggregate('COUNT');
p1ResolvedCount = raw ? parseInt(raw) : 0;
}
// Also safe to use || 0 pattern
var count = (ga.next() ? parseInt(ga.getAggregate('COUNT')) : 0) || 0;
When GlideAggregate is not the right tool
GlideAggregate is for aggregating data across multiple records. For checking existence (does at least one record match this condition?), a GlideRecord with setLimit(1) is simpler and equally performant. For fetching individual record fields, GlideRecord is correct. GlideAggregate shines specifically for COUNT, SUM, AVG, MIN, MAX operations across record sets — the operations that would otherwise require loading every record into memory.
Using GlideAggregate in Scheduled Jobs
GlideAggregate combined with Scheduled Jobs is the standard pattern for generating daily summary metrics. The job runs at a set time, executes aggregate queries across multiple tables, and writes results to a custom reporting table or triggers a notification. This pattern scales to any data volume because it never loads records into memory — only the aggregated numbers travel up the call stack.
// Daily ITSM summary job
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('active=true');
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.query();
var summary = {};
while (ga.next()) {
var p = ga.getValue('priority');
summary['p' + p] = parseInt(ga.getAggregate('COUNT'));
}
gs.log('P1: ' + (summary.p1 || 0) + ', P2: ' + (summary.p2 || 0), 'DailySummary');
GlideAggregate in technical interviews
GlideAggregate appears in almost every mid-level ServiceNow technical interview. The typical question: "How would you count active P1 incidents without loading every record into memory?" The expected answer describes GlideAggregate, addAggregate('COUNT'), query(), next(), and getAggregate(). The follow-up that separates strong candidates: explaining that GlideRecord iterates application-side while GlideAggregate pushes the COUNT to the database engine — the same distinction that drives the performance difference. Combine this with knowledge of RaptorDB's columnar index optimising aggregate queries, and you demonstrate architectural understanding beyond syntax familiarity.
Related: GlideAggregate complete guide · GlideRecord performance · Script Includes · Performance Analytics · Interview questions
Combining GlideAggregate with gs utility methods
GlideAggregate queries often need time-bound filters. The gs utility methods provide the date boundaries — gs.beginningOfThisMonth(), gs.daysAgo(7), gs.endOfToday() — which combine naturally with GlideAggregate encoded query syntax to produce metrics for specific time windows:
// Incidents opened in the last 30 days, grouped by category
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('opened_at>javascript:gs.daysAgo(30)');
ga.addAggregate('COUNT');
ga.groupBy('category');
ga.orderByAggregateDesc('COUNT');
ga.query();
var categoryData = [];
while (ga.next()) {
categoryData.push({
category: ga.getDisplayValue('category'),
count: parseInt(ga.getAggregate('COUNT'))
});
}
// categoryData is ready for a Script Include to return to a widget or notification
This pattern — GlideAggregate + gs date methods + groupBy + a Script Include wrapper — is the standard building block for operational dashboards, weekly digest emails, and Performance Analytics custom indicators. Learn it once and apply it across every metrics use case you encounter.
For comprehensive coverage of all ServiceNow aggregation patterns, review your whole metrics strategy: GlideAggregate complete guide · Performance Analytics · Scheduled Jobs
Want the complete reference?
This article is part of the NowSpectrum knowledge library. Browse all products for cheat sheets, interview prep, and deep-dive reference guides.
Browse All Products →
Free Weekly Newsletter
One practical ServiceNow tip every week.
Written by working professionals. No fluff. Free forever.
Subscribe Free →