What It Is
GlideRecord Query is ServiceNow's server-side database access mechanism that sits between your JavaScript code and the underlying MySQL database. It's the abstraction layer that converts JavaScript method calls into SQL queries, executes them against the platform's database, and returns results as traversable JavaScript objects. Without this pattern, you cannot read, filter, or manipulate records from server-side scripts — making it the foundational skill for any ServiceNow automation.
This executes exclusively on the ServiceNow application server, never in the browser. When you write a GlideRecord query in a business rule, script include, or workflow script, that code runs on ServiceNow's Java application servers in their data centers. The browser never sees your query logic, only the results if you explicitly return them through APIs like GlideAjax or REST endpoints.
Under the hood, ServiceNow translates your addQuery() calls into SQL WHERE clauses, respects ACL security rules by injecting additional conditions, applies business rule filters, and handles table inheritance automatically. The platform maintains a connection pool to the underlying MySQL database, manages transactions, and converts the SQL result set back into JavaScript objects you can iterate through with next(). This abstraction shields you from SQL injection vulnerabilities and database-specific syntax while providing a consistent JavaScript interface.
You cannot implement meaningful business logic in ServiceNow without GlideRecord queries. Every approval workflow that checks user roles, every integration that syncs external data, every dashboard that displays filtered records, every automated assignment rule — they all depend on this pattern. Client-side scripts can display and manipulate individual records, but any operation requiring database queries, cross-table lookups, or bulk data processing requires server-side GlideRecord access.
Administrators use GlideRecord queries in business rules for automated field updates and notifications. Developers rely on them heavily in script includes for reusable data access functions and in scheduled jobs for bulk operations. Architects design integration patterns around GlideRecord performance characteristics and use advanced features like chooseWindow() and setLimit() to manage query performance across large datasets.
GlideRecord queries relate closely to GlideAggregate for counting and statistical operations, which uses the same query building pattern but returns aggregated results instead of individual records. They also connect to GlideAjax calls, where client-side code triggers server-side script includes that execute GlideRecord queries and return filtered data to the browser. Understanding the relationship between these three concepts — client-side requests, server-side queries, and aggregated responses — forms the foundation of ServiceNow's data access architecture.
How It Works Under the Hood
When you execute a GlideRecord query, ServiceNow's Rhino JavaScript engine processes your script on the application server and converts each method call into internal Java objects that represent database operations. The addQuery() methods build a query tree structure that gets translated into SQL when you call query(). The platform injects additional WHERE conditions based on your ACLs, domain restrictions, and any business rule filters defined on the table.
The generated SQL executes against ServiceNow's MySQL database through a connection pool managed by the platform. Results stream back as database rows, which ServiceNow wraps in JavaScript objects that expose getValue() and getDisplayValue() methods. Reference fields get special handling — ServiceNow can either return just the sys_id or execute additional queries to resolve display values, depending on how you access the field.
Most developers don't realize that ServiceNow optimizes query execution by analyzing your iteration pattern. If you only call next() once, the platform may only fetch the first batch of results. If you access reference field display values, it may execute JOIN operations or separate queries behind the scenes. The GlideRecord object maintains state about your current position in the result set, which fields have been accessed, and what reference lookups have been resolved.
The Query Execution Lifecycle
- JavaScript engine instantiates GlideRecord object and validates table access permissions
- Each
addQuery()call builds internal query tree structure with field, operator, and value components - Platform analyzes ACLs for current user context and injects additional security conditions
- Query tree converts to SQL with proper table inheritance handling and domain restrictions
- SQL executes against MySQL database through managed connection pool
- Result set wraps in JavaScript objects with lazy-loaded reference field resolution
- Iterator pattern through
next()calls advances cursor and populates current record context
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Core Pattern
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') return;
// Client-side cannot query database directly
// Must call server-side script include via GlideAjax
var ga = new GlideAjax('IncidentQueryUtils');
ga.addParam('sysparm_name', 'getRelatedIncidents');
ga.addParam('sysparm_caller_id', newValue); // caller sys_id
ga.addParam('sysparm_days_back', 30);
ga.getXMLAnswer(function(answer) {
var count = parseInt(answer) || 0;
if (count > 5) {
g_form.showErrorBox('caller_id', 'This caller has ' + count + ' incidents in last 30 days');
}
// Update related list or field with results
g_form.setValue('u_related_incident_count', count);
});
}var IncidentQueryUtils = Class.create();
IncidentQueryUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getRelatedIncidents: function() {
var callerId = this.getParameter('sysparm_caller_id');
var daysBack = parseInt(this.getParameter('sysparm_days_back')) || 7;
// Core GlideRecord pattern: instantiate, condition, query, iterate
var incidentGR = new GlideRecord('incident');
incidentGR.addQuery('caller_id', callerId); // Filter by caller
incidentGR.addQuery('sys_created_on', '>=', gs.daysAgoStart(daysBack)); // Date range
incidentGR.addQuery('state', '!=', 7); // Exclude closed incidents
incidentGR.query(); // Execute the database query
var count = 0;
while (incidentGR.next()) { // Iterate through results
count++; // Could also access incidentGR.getValue('number') here
}
return count.toString(); // Must return string for GlideAjax
}
});Real-World Scenarios
Automated Assignment Based on Workload
Assignment groups need to distribute incoming incidents evenly among available team members. This business rule queries active incidents per user to find the person with the lightest workload.
(function executeRule(current, previous /*null when async*/) {
// Only run on insert when assignment group is set but assigned_to is empty
if (!current.assignment_group || current.assigned_to) return;
// Get all active members of the assignment group
var memberGR = new GlideRecord('sys_user_grmember');
memberGR.addQuery('group', current.assignment_group);
memberGR.query();
var availableUsers = [];
while (memberGR.next()) {
var userGR = new GlideRecord('sys_user');
if (userGR.get(memberGR.user) && userGR.active) {
availableUsers.push(memberGR.user.toString());
}
}
if (availableUsers.length == 0) return; // No available users
// Find user with least active incidents
var lightestWorkload = findUserWithLeastIncidents(availableUsers);
current.assigned_to = lightestWorkload;
})(current, previous);function findUserWithLeastIncidents(userSysIds) {
var minCount = 999;
var selectedUser = userSysIds[0]; // Fallback to first user
for (var i = 0; i < userSysIds.length; i++) {
var userId = userSysIds[i];
// Count active incidents assigned to this user
var incidentGR = new GlideRecord('incident');
incidentGR.addQuery('assigned_to', userId);
incidentGR.addQuery('state', 'IN', '1,2,6'); // New, In Progress, Resolved
incidentGR.query();
var currentCount = incidentGR.getRowCount();
if (currentCount < minCount) {
minCount = currentCount;
selectedUser = userId;
}
}
return selectedUser;
}Watch out for performance issues with large assignment groups — this pattern executes one query per user. Consider using GlideAggregate with groupBy() for groups larger than 10 members. Also remember that getRowCount() executes the full query just to count results.
Cascade Status Updates to Related Records
When a major incident gets resolved, all related child incidents should automatically close. This business rule finds and updates dependent records when the parent incident status changes.
(function executeRule(current, previous /*null when async*/) {
// Only trigger when incident state changes to resolved or closed
if (current.state != 6 && current.state != 7) return;
if (previous && (previous.state == 6 || previous.state == 7)) return;
// Find all child incidents that reference this parent
var childIncidentGR = new GlideRecord('incident');
childIncidentGR.addQuery('parent_incident', current.sys_id);
childIncidentGR.addQuery('state', 'NOT IN', '6,7'); // Not already resolved/closed
childIncidentGR.query();
var updateCount = 0;
while (childIncidentGR.next()) {
// Preserve original resolution info while updating state
childIncidentGR.state = 6; // Resolved
childIncidentGR.close_code = 'Resolved by parent incident';
childIncidentGR.close_notes = 'Auto-closed due to parent incident ' +
current.number + ' resolution';
childIncidentGR.update(); // Triggers business rules on child records
updateCount++;
}
if (updateCount > 0) {
gs.info('Cascade update: closed ' + updateCount + ' child incidents for ' + current.number);
}
})(current, previous);Each update() call in the loop triggers business rules on child records, which could cause performance issues or infinite loops. Consider using updateMultiple() for bulk operations or adding conditions to prevent recursive updates.
Dynamic Service Catalog Filtering
Service catalog items need to show different options based on the requestor's location and department. This catalog client script calls a server-side function to populate a choice list with location-appropriate hardware.
function onLoad() {
// Get requestor's location from session
var userLocation = g_user.getRecord().location;
if (!userLocation) return;
// Call server to get available hardware for this location
var ga = new GlideAjax('CatalogQueryUtils');
ga.addParam('sysparm_name', 'getAvailableHardware');
ga.addParam('sysparm_location', userLocation);
ga.addParam('sysparm_category', 'laptop'); // Could be dynamic
ga.getXMLAnswer(function(answer) {
if (answer) {
var options = JSON.parse(answer);
var choiceField = g_form.getControl('hardware_model');
// Clear existing options and populate with location-specific items
choiceField.options.length = 1; // Keep 'None' option
for (var i = 0; i < options.length; i++) {
choiceField.options[choiceField.options.length] =
new Option(options[i].display, options[i].value);
}
}
});
}getAvailableHardware: function() {
var locationId = this.getParameter('sysparm_location');
var category = this.getParameter('sysparm_category') || 'laptop';
// Query hardware assets available at the specified location
var assetGR = new GlideRecord('alm_hardware');
assetGR.addQuery('install_status', 6); // In stock
assetGR.addQuery('location', locationId);
assetGR.addQuery('model_category.name', 'CONTAINS', category);
assetGR.addQuery('assigned_to', ''); // Not assigned to anyone
assetGR.orderBy('model_id'); // Group by model
assetGR.query();
var models = {}; // Deduplicate by model
while (assetGR.next()) {
var modelId = assetGR.getValue('model_id');
if (!models[modelId]) {
models[modelId] = {
value: modelId,
display: assetGR.model_id.getDisplayValue() + ' (' +
assetGR.location.getDisplayValue() + ')'
};
}
}
return JSON.stringify(Object.values(models));
}This pattern can become slow with large asset databases — consider adding database indexes on install_status and location fields. Also be careful with getDisplayValue() calls in loops — each one may trigger additional database queries for reference field resolution.
The Classic Mistake
Calling query() inside a loop that processes other GlideRecord results creates exponential database hits and will destroy performance.
// Business Rule: Update related requests when incident closes
var incident = new GlideRecord('incident');
incident.addQuery('state', '7'); // Closed
incident.query();
while (incident.next()) {
// DISASTER: Query inside a loop
var requests = new GlideRecord('sc_req_item');
requests.addQuery('request.opened_by', incident.caller_id.toString());
requests.addQuery('stage', 'waiting_for_approval');
requests.query(); // Each iteration hits database again
while (requests.next()) {
requests.stage = 'closed_cancelled';
requests.update(); // More database writes per loop
}
}This code executes one query() call for every closed incident, creating N+1 query problems that scale exponentially. With 100 closed incidents, you're making 101 database round trips instead of 2. The browser console shows "Transaction cancelled due to excessive database activity" and server logs record "Script execution exceeded maximum database query limit". ServiceNow's query governor kicks in after 10,000 total queries per transaction and kills the entire operation. Your Business Rule fails silently, and users see stale data with no error message explaining why their updates didn't work.
// Business Rule: Update related requests when incident closes
var incident = new GlideRecord('incident');
incident.addQuery('state', '7'); // Closed
incident.query();
// Collect all caller IDs first
var callerIds = [];
while (incident.next()) {
callerIds.push(incident.caller_id.toString());
}
// Single query for all related requests
if (callerIds.length > 0) {
var requests = new GlideRecord('sc_req_item');
requests.addQuery('request.opened_by', 'IN', callerIds.join(','));
requests.addQuery('stage', 'waiting_for_approval');
requests.query();
while (requests.next()) {
requests.stage = 'closed_cancelled';
requests.update();
}
}Never call query() inside a while(next()) loop. Always collect your filter values first, then make a single query with IN operators or ORConditions.
Performance Rules
- Always call
setLimit()when you don't need all records. Without it, ServiceNow loads every matching record into memory. Queries returning over 1,000 records cause browser timeouts and make sys admins complain about slow system performance. - Use
chooseWindow()instead ofsetLimit()when paginating.setLimit(1000)still processes 1,000 records internally.chooseWindow(10, 20)skips the first 10 and returns 20 records, avoiding unnecessary processing. - Never use
getRowCount()on tables with over 10,000 records. It forces a full table scan and can timeout after 30 seconds. UsehasNext()ornext()to check for record existence instead. - Add indexes before using
addQuery()on custom fields. Queries on non-indexed fields cause full table scans. Check System Definition > Tables & Columns to verify your query fields have indexes, or response times exceed 10 seconds per query. - Use
addEncodedQuery()for complex conditions instead of multipleaddQuery()calls. Seven or more separateaddQuery()calls prevent the database from optimizing the query plan, leading to sequential scans instead of index usage. - Avoid
CONTAINSandENDSWITHoperators on tables with over 50,000 records. These operators can't use indexes and force full table scans. The query governor kills transactions that scan more than 500,000 rows, causing "maximum query time exceeded" errors. - Call
setWorkflow(false)when bulk updating records. Workflow engines processing 100+ records simultaneously consume massive CPU and memory, causing system-wide slowdowns that affect all users. Your update operation completes 5-10x faster with workflows disabled. - Use
updateMultiple()instead of looping through records withupdate()when changing the same field on many records.updateMultiple()executes as a single SQL statement, while loops create one database transaction per record, overwhelming the connection pool.
Side Effects & Platform Behavior
- Every
query()call triggers ACL evaluation on every field you access. If the current user lacks read permission,getValue()returns null instead of the actual field value, breaking your logic silently. - Calling
insert(),update(), ordeleteRecord()fires all Business Rules, Workflows, and Notifications configured for that table. Your "simple" record update might send dozens of emails and trigger cascading updates across related tables. - Every database write creates entries in
sys_audittable if auditing is enabled. Bulk operations can fill up your audit table with millions of records, consuming significant database storage and slowing future queries. - Using GlideRecord in UI Actions creates synchronous database calls that block the browser until completion. Users see frozen interfaces with no loading indicators. Always use GlideAjax for client-side database access instead.
- Reference field dot-walking (like
incident.caller_id.department.name) creates hidden database queries for each dot. Three levels of dot-walking generates four separate SQL queries, multiplying your database load without warning. - Domain separation applies automatically to all queries. Your script might have admin rights, but if the current user's domain doesn't match the record's domain,
next()returns false even when matching records exist. - Dictionary overrides and field-level read ACLs can make the same query return different results for different users. What works in testing as admin might fail completely for end users with limited permissions.
- Background script execution bypasses normal session limits but still respects role-based security. Your script runs with elevated database permissions but field access still depends on the user context you're impersonating.
- Using
gs.generateGUID()to setsys_idbeforeinsert()breaks import set transformations and can create duplicate records if your GUID generation logic has any flaws. - Date/time queries automatically adjust for the user's timezone. A query for "today's records" returns different results depending on whether the user is in EST, PST, or GMT, making debugging time-based logic extremely difficult.
Debugging When It Breaks
The most common failure is the silent failure: your code runs without errors, but next() returns false when you expect records. Users report "the system isn't updating my tickets" but see no error messages. Developers see their script execute successfully in logs, but the while loop never runs. This usually means ACLs are blocking field access, domain separation is filtering records, or your query conditions don't match what you think they match.
Performance failures show up as browser timeouts with "504 Gateway Timeout" errors or "Transaction cancelled due to excessive database activity" in System Log > All. Users see spinning loading indicators that never complete. The Script Debugger shows your script starting but never finishing, and database response times in System Diagnostics > Stats exceed 30 seconds. These symptoms always indicate either missing database indexes, queries scanning too many records, or N+1 query patterns.
Security-related failures manifest as null values from getValue() calls that should return data. Check System Security > Access Controls (ACL) to verify the current user has read access to every field your query touches. Use System Log > Security to see which ACLs are denying access. The easiest diagnostic is running your exact code in a Background Script while impersonating the affected user.
Quick diagnostic checklist:
- Add
gs.info(gr.getEncodedQuery())beforequery()to see the exact SQL conditions being generated - Use
gs.info(gr.getRowCount())to verify your query matches the expected number of records (but only on small result sets) - Check System Definition > Tables to confirm your target table exists and your field names are spelled correctly
- Run the same query manually in the list view to see if domain separation or ACLs are filtering your results
Quick Reference
- Always call
query()beforenext()— forgetting this makesnext()always return false - Use
getValue('field')notgr.fieldto avoid display value vs database value confusion - Reference fields require
.toString()when used in conditions:addQuery('assigned_to', user.sys_id.toString()) - Empty string conditions need special handling:
addQuery('field', '=')oraddNullQuery('field') - Date queries use
gs.daysAgoEnd(0)for "today" orjavascript:gs.beginningOfLastMonth()for relative dates - Multiple values use
INoperator:addQuery('state', 'IN', '1,2,3')— comma-separated, no spaces - OR conditions need
addOrCondition()or encoded queries — multipleaddQuery()calls create AND logic - Use
canRead()andcanWrite()to check permissions before accessing fields programmatically - Call
setLimit(1)when you only need to check if records exist — don't load thousands to use one - GlideRecord only works server-side — use GlideAjax or REST APIs for client-side database access from UI scripts