What It Is
GlideQuery is ServiceNow's attempt to drag database querying into the modern era, replacing the decades-old GlideRecord API with something that doesn't make you want to throw your laptop out the window. It's a server-side only, functional programming approach to database operations that eliminates the null pointer exceptions, confusing state management, and imperative spaghetti code that makes traditional GlideRecord scripts a maintenance nightmare. Instead of mutating objects and checking if (gr.next()) everywhere, you chain methods and get predictable results every time.
Architecturally, GlideQuery sits exactly where GlideRecord does—it's server-side only, executing in the Rhino JavaScript engine on the application nodes. You cannot use it in client scripts, UI policies, or catalog client scripts, period. It's designed for business rules, script includes, scheduled jobs, and server-side APIs where you need to query the database without the ceremony and potential runtime explosions of traditional record manipulation. The key difference is that GlideQuery returns immutable results and uses method chaining instead of stateful iteration, making it impossible to accidentally query the wrong record or forget to call next().
Under the hood, ServiceNow translates your GlideQuery chains into optimized database queries, similar to how modern ORMs work. When you call new GlideQuery('incident'), you're not immediately hitting the database—you're building a query plan. Each chained method like where() or orderBy() adds to that plan without executing anything. Only when you call a terminal method like select() or selectOne() does ServiceNow actually execute the SQL against the database and return results. This lazy evaluation means you can build complex queries programmatically without worrying about performance until you actually need the data.
What you cannot do without GlideQuery is write maintainable, error-resistant database queries in modern ServiceNow development. Every senior developer has war stories about GlideRecord scripts that worked perfectly in development but exploded in production when a record was unexpectedly null, or when someone forgot to check isValidRecord() before accessing field values. GlideQuery eliminates entire categories of runtime errors by returning Optional objects for single records and never returning null values for collections. You also can't efficiently build dynamic queries with traditional GlideRecord without creating a mess of conditional logic—GlideQuery's method chaining makes dynamic query construction trivial.
Developers and architects use GlideQuery primarily in server-side automation where data reliability matters more than rapid prototyping. It's not necessarily better for simple, one-off scripts where you just need to grab a single record and update it—GlideRecord is actually more concise for basic CRUD operations. But for complex business logic, integration code, or anywhere you need to chain multiple query conditions and transformations, GlideQuery becomes invaluable. Administrators typically don't interact with it directly unless they're writing advanced business rules or scheduled jobs. It's particularly powerful in REST APIs, complex approval workflows, and data migration scripts where you need to process large datasets without worrying about edge cases crashing the entire operation.
GlideQuery relates most closely to GlideAggregate for analytical queries and GlideRecord for basic database operations, but it occupies a middle ground that didn't exist before Orlando. Unlike GlideAggregate, you get full record details, not just aggregated statistics. Unlike GlideRecord, you get functional composition and built-in error handling. It also pairs naturally with ServiceNow's newer Flow Designer and REST API frameworks where immutable data structures and predictable error handling are essential for reliable automation.
How It Works Under the Hood
GlideQuery operates on a lazy evaluation model borrowed from functional programming languages. When you instantiate a GlideQuery object and chain methods, you're not executing database operations—you're building an abstract syntax tree that represents your query intentions. The ServiceNow platform holds this query plan in memory and only translates it to SQL when you invoke a terminal operation like select() or selectOne(). This deferred execution allows ServiceNow's query optimizer to analyze the entire operation holistically and generate more efficient database queries than traditional imperative approaches.
The immutable result objects that GlideQuery returns are fundamentally different from GlideRecord instances. Instead of maintaining active database cursors with mutable state, GlideQuery creates snapshot objects that represent the data as it existed at query time. These snapshots include built-in null checking and type coercion, which eliminates the runtime exceptions that occur when GlideRecord tries to access fields on invalid or deleted records. The Optional wrapper for single records forces developers to explicitly handle the case where no record matches, preventing the silent failures that plague production systems.
Internally, ServiceNow translates GlideQuery operations into optimized SQL that takes advantage of database-specific features like index hints and join strategies. The platform maintains metadata about table relationships, field types, and access controls, applying these constraints during the query translation phase rather than after results are returned. This means ACL enforcement, field-level security, and domain separation happen at the database level, not in JavaScript post-processing, resulting in better performance and more predictable security behavior than traditional GlideRecord queries that filter results after retrieval.
The Query Execution Lifecycle
- Query Plan Construction: Each chained method (
where(),orderBy(),limit()) adds nodes to an internal query tree without touching the database. - Security Context Application: ServiceNow applies ACLs, domain separation, and field-level security rules to determine what data the current user can access.
- Query Optimization: The platform analyzes the query tree, determines optimal join strategies, and selects appropriate database indexes based on table statistics.
- SQL Generation: The abstract query is translated into database-specific SQL with parameterized values to prevent injection attacks.
- Database Execution: The SQL executes against the MySQL/Oracle/SQL Server backend, returning raw result sets to the application tier.
- Result Wrapping: Raw database rows are wrapped in immutable GlideQuery result objects with built-in null checking and type conversion, then returned to your script as
StreamorOptionalcollections.
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
var IncidentQueryUtils = Class.create();
IncidentQueryUtils.prototype = {
getHighPriorityIncidents: function() {
// GlideQuery eliminates null pointer exceptions with Optional wrapper
return new GlideQuery('incident')
.where('priority', '<=', 2) // P1 and P2 incidents only
.where('state', 'IN', [1, 2, 3]) // New, In Progress, On Hold
.where('active', true)
.orderBy('priority') // Highest priority first
.orderBy('sys_created_on') // Then by age
.select(); // Returns Stream of immutable records
},
getIncidentById: function(sysId) {
// Returns Optional<GlideQueryRecord> - never null
return new GlideQuery('incident')
.where('sys_id', sysId)
.selectOne(); // Optional wrapper forces null checking
},
type: 'IncidentQueryUtils'
};// Using the GlideQuery-based Script Include in a business rule
(function executeRule(current, previous /*null when async*/) {
var queryUtils = new IncidentQueryUtils();
// Get high priority incidents - no null checking needed
var highPriorityIncidents = queryUtils.getHighPriorityIncidents();
// Stream API allows functional composition
highPriorityIncidents
.filter(function(incident) {
return incident.getValue('assigned_to').isEmpty();
})
.forEach(function(incident) {
// Process unassigned P1/P2 incidents
gs.log('Unassigned high priority: ' + incident.getValue('number'));
});
// Optional wrapper prevents runtime errors
var specificIncident = queryUtils.getIncidentById(current.sys_id.toString());
if (specificIncident.isPresent()) {
var incident = specificIncident.get();
gs.log('Found incident: ' + incident.getValue('number'));
}
})(current, previous);Real-World Scenarios
Dynamic Service Catalog Filtering
A service catalog needs to display different hardware options based on the requesting user's department, location, and role. Traditional GlideRecord requires complex conditional logic and multiple database queries.
getAvailableHardware: function(userId, category) {
var user = this._getUserDetails(userId);
var query = new GlideQuery('sc_cat_item')
.where('category', category)
.where('active', true);
// Chain conditions based on user attributes - no conditional query building
if (user.department === 'Engineering') {
query = query.where('u_engineering_approved', true);
}
if (user.location.startsWith('Remote')) {
query = query.where('u_shippable', true);
}
// Executive bypass for approval requirements
if (!user.hasRole('executive')) {
query = query.where('u_requires_approval', false);
}
return query.orderBy('order').limit(50).select();
},Watch for role-based filtering being cached inappropriately—always verify security context in the query execution environment. The chained approach makes it easy to accidentally apply filters that should be user-specific across multiple requests. Consider the performance impact of dynamic where() conditions that prevent effective database index usage.
Bulk Data Validation for Integrations
An integration endpoint receives batches of user updates from HR systems and needs to validate existing records before processing. The validation must handle missing records gracefully without breaking the entire batch operation.
validateUserBatch: function(hrRecords) {
var employeeIds = hrRecords.map(function(record) {
return record.employee_id;
});
// Single query for all users - no N+1 problem
var existingUsers = new GlideQuery('sys_user')
.where('employee_number', 'IN', employeeIds)
.where('active', true)
.select()
.reduce(function(acc, user) {
acc[user.getValue('employee_number')] = user;
return acc;
}, {});
return hrRecords.map(function(hrRecord) {
var user = existingUsers[hrRecord.employee_id];
return {
employee_id: hrRecord.employee_id,
exists: !!user,
needs_update: user && user.getValue('department') !== hrRecord.department,
current_user: user // Will be undefined for missing users, not null
};
});
},Be careful with large IN clauses—most databases have limits around 1000 items, and ServiceNow may chunk these automatically but unpredictably. The reduce() operation happens in memory, so massive result sets can cause performance issues. Consider paginating large batches or using temporary tables for very large datasets.
Complex Approval Chain Resolution
A change approval process needs to find all pending approvals for changes in specific categories, then identify approvers who haven't responded within SLA timeframes. The query involves multiple table joins and complex date calculations that would create a mess with traditional GlideRecord iteration.
findOverdueApprovals: function(categoryList, slaDays) {
var cutoffDate = new GlideDateTime();
cutoffDate.addDays(-slaDays);
return new GlideQuery('sysapproval_approver')
.where('state', 'requested')
.where('sys_created_on', '<=', cutoffDate.getDisplayValue())
.where('sysapproval.category', 'IN', categoryList)
.where('sysapproval.state', 'requested')
.select(['approver.name', 'approver.email', 'sysapproval.number',
'sysapproval.short_description', 'sys_created_on'])
.map(function(approval) {
return {
approver_name: approval.getValue('approver.name'),
approver_email: approval.getValue('approver.email'),
change_number: approval.getValue('sysapproval.number'),
change_description: approval.getValue('sysapproval.short_description'),
days_pending: gs.dateDiff(approval.getValue('sys_created_on'),
gs.nowDateTime(), true)
};
})
.toArray(); // Convert Stream to Array for easier processing
},Cross-table dot-walking in where() clauses can create expensive joins—test performance with realistic data volumes. The select() field list optimization only works if you actually use the restricted field set; accessing other fields later triggers additional queries. Date comparisons in GlideQuery require careful timezone handling—always use GlideDateTime objects rather than string literals.
The Classic Mistake
Chaining .get() after conditional methods without checking if the query actually returned results.
// This will blow up in production
function assignIncidentToManager(incidentId, managerId) {
// Query for the incident
var incident = new GlideQuery('incident')
.where('number', incidentId)
.where('state', '1')
.get(); // This might return null
// Query for the manager
var manager = new GlideQuery('sys_user')
.where('sys_id', managerId)
.where('active', true)
.get(); // This might also return null
// Boom - null pointer exception waiting to happen
incident.assigned_to = manager.sys_id;
incident.update();
}This fails because get() returns null when no records match your conditions, but developers chain method calls as if a record always exists. You'll see "Cannot read property 'sys_id' of null" in the browser console or "java.lang.NullPointerException" in the server logs. ServiceNow's JavaScript engine can't access properties on null objects, causing the entire script execution to halt. The incident assignment never happens, users see generic error messages, and your Business Rule or Script Include silently fails without any indication of what went wrong.
// Defensive programming prevents runtime failures
function assignIncidentToManager(incidentId, managerId) {
// Query with explicit null checks
var incident = new GlideQuery('incident')
.where('number', incidentId)
.where('state', '1')
.get();
var manager = new GlideQuery('sys_user')
.where('sys_id', managerId)
.where('active', true)
.get();
// Always validate before using
if (!incident || !manager) {
gs.error('Assignment failed: incident=' + !!incident + ', manager=' + !!manager);
return false;
}
incident.assigned_to = manager.sys_id;
return incident.update();
}Never call methods or access properties on the result of get() without first checking if it's null. Always validate your query results before using them.
Performance Rules
- Always use
limit()when callingforEach()on tables with over 1,000 records. Without it, your script will time out after 30 seconds and sys_admins will get "Long Running Script" alerts in the System Log. - Use
select()to specify only the fields you need when querying tables with large text fields likekb_knowledge.textorincident.description. Fetching unnecessary CLOB fields causes browser memory spikes over 500MB and crashes mobile clients. - Never nest GlideQuery calls inside
forEach()loops without explicit limits. Each nested query creates a new database connection; 50+ concurrent connections will trigger database pool exhaustion and lock out other users. - Use
whereNotNull()instead ofwhere('field', '!=', '')on indexed fields. The specialized method leverages database null indexes, reducing query execution from 15+ seconds to under 200ms on tables with millions of records. - Order results by indexed fields only when using
orderBy()with large datasets. Sorting by non-indexed fields forces full table scans and causes 504 Gateway Timeout errors on lists with over 10,000 records. - Avoid
where()conditions usingCONTAINSorSTARTSWITHoperators on string fields longer than 255 characters. These operations disable index usage and cause query response times to exceed 45 seconds, triggering automatic query cancellation. - Chain multiple
where()conditions in order of selectivity (most restrictive first). Put conditions that eliminate 90%+ of records before those that eliminate 10%; this reduces intermediate result sets and prevents temporary table overflow errors. - Use
get()instead offorEach()when you only need the first matching record.forEach()allocates memory for all matching records even if you break after the first iteration, causing unnecessary heap pressure.
Side Effects & Platform Behavior
- GlideQuery queries trigger ACL evaluation on every field access, writing access attempts to the
sys_security_logtable when security debugging is enabled, potentially creating thousands of audit records per query. - Business Rules fire normally when you call
update()orinsert()on GlideQuery results, butcurrentandpreviousobjects in the Business Rule will be GlideRecord instances, not GlideQuery objects. - Dictionary overrides and calculated fields are automatically resolved during query execution, but computed fields marked as "Calculate on server" may not populate until after the first
update()operation. - Client-side GlideQuery automatically respects domain separation, but server-side usage bypasses domain restrictions unless you explicitly call
setDomainQuery()on the query object. - Query execution logs appear in the
mysql_slow.logwhen queries take longer than 2 seconds, with GlideQuery-generated SQL marked with comment tags containing the script name and line number. - Reference field values are lazy-loaded, meaning the first access to
incident.caller_id.emailtriggers a separate database query that doesn't appear in your original query's execution plan. - Notifications configured with "Send when" conditions will evaluate against the updated record state, not the original query results, potentially sending unexpected emails when field values change during processing.
- Using GlideQuery in Workflows or Flow Designer requires converting results to GlideRecord objects using
new GlideRecord(tablename); gr.get(query_result.sys_id)because workflow engines expect GlideRecord context. - Table rotation policies apply to GlideQuery results, so queries against rotated tables like
sys_logormetric_instancemay return incomplete result sets if older partitions have been archived. - Memory allocation for query results counts against the JavaScript execution context limit (32MB default), and large result sets cause "Maximum call stack size exceeded" errors that terminate the entire script.
Debugging When It Breaks
The most common failure symptoms include null pointer exceptions when calling methods on query results, "Cannot read property" errors in browser console when accessing fields that don't exist, and silent failures where your script appears to run but produces no results. Users typically see generic "An error has occurred" messages or blank form sections, while developers see their Business Rules or Script Includes stopping execution without obvious cause.
When GlideQuery breaks, check the browser's Developer Tools Console first for client-side errors, then navigate to System Logs > All to find server-side JavaScript exceptions. Look specifically in the Script Debugger (System Definition > Script Debugger) if you've enabled debugging for your specific script. The Application Log (System Logs > System Log > Application Logs) contains database query execution details when "Log database queries" is enabled in system properties.
Common error patterns include "java.lang.NullPointerException" when get() returns null, "ReferenceError: Cannot read property 'getValue' of null" for missing reference fields, and "Maximum call stack size exceeded" when result sets are too large. Database timeouts appear as "Query execution was interrupted, maximum time exceeded" in the mysql slow query log. Performance issues manifest as "Long running script detected" warnings with execution times over 30 seconds.
Quick diagnostic checklist:
- Verify the table name exists and is spelled correctly
- Check if your where conditions are too restrictive (test with fewer filters)
- Confirm field names match the database column names, not display labels
- Add null checks before calling methods on query results
- Test ACL permissions by impersonating the user experiencing issues
Quick Reference
- Call
toArray()to convert query results to a standard JavaScript array for use withmap(),filter(), andreduce()methods - Use
optional()wrapper aroundget()results to chain operations safely without null checks:optional(query.get()).map(r => r.number) - GlideQuery objects are immutable - each chained method returns a new query instance, so you can safely reuse base queries
- Reference fields accessed with dot notation (
caller_id.email) automatically perform joins, but each access triggers a separate database query - Chain
disableWorkflow()beforeupdate()to prevent Business Rules and Workflows from firing on bulk operations - Use
whereNull()andwhereNotNull()instead of comparing against empty strings - they're faster and more semantically correct - Call
stream()for functional programming operations like.stream().filter().map().collect()on large datasets without loading all records into memory - Query conditions are case-sensitive by default - use
whereLike()with wildcards for case-insensitive string matching - The
count()method is optimized for large tables and returns results faster than usingtoArray().lengthon result sets - GlideQuery results maintain field display values automatically - access
record.state.$displayfor choice field labels without additional queries