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

  1. Query Plan Construction: Each chained method (where(), orderBy(), limit()) adds nodes to an internal query tree without touching the database.
  2. Security Context Application: ServiceNow applies ACLs, domain separation, and field-level security rules to determine what data the current user can access.
  3. Query Optimization: The platform analyzes the query tree, determines optimal join strategies, and selects appropriate database indexes based on table statistics.
  4. SQL Generation: The abstract query is translated into database-specific SQL with parameterized values to prevent injection attacks.
  5. Database Execution: The SQL executes against the MySQL/Oracle/SQL Server backend, returning raw result sets to the application tier.
  6. 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 Stream or Optional collections.
Free Newsletter

Enjoying this? Get one deep-dive per week.

Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.

No spam · Unsubscribe anytime

The Core Pattern

Script Include — IncidentQueryUtils.js
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'
};
Business Rule — Incident Assignment.js
// 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.

Script Include — CatalogFilterUtils.js
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.

Script Include — HRIntegrationValidator.js
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.

Script Include — ChangeApprovalEscalation.js
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.

Anti-pattern — Do Not Use This.js
// 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.

The Fix.js
// 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

  1. Always use limit() when calling forEach() 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.
  2. Use select() to specify only the fields you need when querying tables with large text fields like kb_knowledge.text or incident.description. Fetching unnecessary CLOB fields causes browser memory spikes over 500MB and crashes mobile clients.
  3. 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.
  4. Use whereNotNull() instead of where('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.
  5. 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.
  6. Avoid where() conditions using CONTAINS or STARTSWITH operators 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.
  7. 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.
  8. Use get() instead of forEach() 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_log table when security debugging is enabled, potentially creating thousands of audit records per query.
  • Business Rules fire normally when you call update() or insert() on GlideQuery results, but current and previous objects 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.log when 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.email triggers 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_log or metric_instance may 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 with map(), filter(), and reduce() methods
  • Use optional() wrapper around get() 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() before update() to prevent Business Rules and Workflows from firing on bulk operations
  • Use whereNull() and whereNotNull() 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 using toArray().length on result sets
  • GlideQuery results maintain field display values automatically - access record.state.$display for choice field labels without additional queries