What It Is

addQuery() is the primary method for adding WHERE conditions to GlideRecord database queries in ServiceNow. It translates JavaScript filter logic into SQL WHERE clauses, allowing you to retrieve specific records instead of pulling entire tables into memory. Without it, every GlideRecord would return all records from a table, making most business logic impossible to implement efficiently.

This method executes exclusively on the server side — in Business Rules, Script Includes, Scheduled Jobs, and Flow scripts. You'll never call addQuery() from a Client Script or UI Policy. The client side uses different mechanisms like g_list.addFilter() or encoded query strings passed to server endpoints.

Under the hood, ServiceNow's query engine collects all addQuery() conditions called on a GlideRecord instance and combines them with AND logic when you execute query(). The platform builds a single SQL statement with multiple WHERE conditions, executes it against the MySQL database, and returns a result set that you iterate through with next(). This is why the order of addQuery() calls doesn't matter — they're all conditions in the same WHERE clause.

Without addQuery(), you cannot implement data access patterns that scale beyond development instances. Loading 50,000 incident records to find the 12 that are priority 1 will timeout your script and degrade instance performance. You cannot build conditional workflows, generate accurate reports, or create integrations that don't transfer unnecessary data. It's the difference between a working ServiceNow implementation and one that grinds to a halt under real-world data volumes.

Developers use addQuery() in every server-side script that touches data. Administrators rely on it in scheduled jobs for data cleanup and reporting. Architects design entire data access patterns around its capabilities and limitations. Flow designers encounter it when building subflow logic that queries records. Anyone writing server-side code in ServiceNow uses this method daily.

It relates directly to addEncodedQuery(), which accepts condition lists instead of individual parameters, and addOrCondition(), which provides OR logic between conditions. While addQuery() builds conditions programmatically with clear field-operator-value parameters, addEncodedQuery() accepts the same encoded strings you see in list view URLs. You'll also find it working alongside orderBy() to sort filtered results.

How It Works Under the Hood

When you call addQuery() on a GlideRecord, ServiceNow doesn't immediately hit the database. Instead, the platform stores your field, operator, and value parameters in an internal query builder object attached to that GlideRecord instance. Multiple addQuery() calls accumulate as separate conditions in this builder.

The actual database query executes when you call query(). At that moment, ServiceNow's ORM layer converts all accumulated conditions into a single SQL SELECT statement with WHERE clauses joined by AND operators. The platform applies field-level security, processes dot-walking for reference fields, handles display value lookups, and executes the final query against the MySQL database cluster.

Most developers don't realize that addQuery() automatically handles display value translation for reference fields, choice fields, and date formatting. When you query assigned_to.name with a user's full name, the platform looks up the sys_id and queries against the reference field's actual value. This translation happens during query compilation, not during result iteration.

The Query Execution Lifecycle

  1. Script calls addQuery('field', 'operator', 'value') — parameters stored in internal condition array
  2. Additional addQuery() calls append more conditions to the same array
  3. Script calls query() — triggers query compilation process
  4. Platform resolves display values, applies ACLs, validates field names and operators
  5. ORM generates SQL SELECT with WHERE conditions joined by AND operators
  6. Database executes query, returns result set to GlideRecord instance for iteration
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

Client Script — Incident Form.js
function onChange(control, oldValue, newValue, isLoading) {
    // Client side cannot use GlideRecord addQuery() directly
    // Instead, call server-side Script Include via GlideAjax
    if (isLoading) return;
    
    var ga = new GlideAjax('IncidentQueryUtils');
    ga.addParam('sysparm_name', 'getRelatedIncidents');
    ga.addParam('sysparm_caller_number', g_form.getValue('caller_id'));
    ga.addParam('sysparm_priority', g_form.getValue('priority'));
    
    ga.getXMLAnswer(function(response) {
        var count = parseInt(response);
        if (count > 5) {
            g_form.showFieldMsg('caller_id', 'Caller has ' + count + ' open P1/P2 incidents', 'warning');
        }
    });
}
Script Include — IncidentQueryUtils.js
var IncidentQueryUtils = Class.create();
IncidentQueryUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    getRelatedIncidents: function() {
        var callerId = this.getParameter('sysparm_caller_id');
        var priority = this.getParameter('sysparm_priority');
        
        var gr = new GlideRecord('incident');
        gr.addQuery('caller_id', callerId);  // Core addQuery usage
        gr.addQuery('state', 'NOT IN', '6,7,8');  // Exclude resolved/closed/cancelled
        gr.addQuery('priority', '<=', priority);  // Same or higher priority
        gr.addQuery('opened_at', '>=', 'javascript:gs.beginningOfLast30Days()');  // Recent only
        
        gr.query();  // Execute all accumulated conditions with AND logic
        return gr.getRowCount().toString();  // Return count to client
    },
    
    type: 'IncidentQueryUtils'
});

Real-World Scenarios

Service Catalog Auto-Assignment Logic

A catalog item needs to automatically assign requests to agents based on the requester's location and the agent's current workload. The business rule finds available agents in the same location who have fewer than 10 active assignments.

Business Rule — Auto Assignment.js
(function executeRule(current, previous /*null when async*/) {
    var requesterLocation = current.opened_by.location.toString();
    
    // Find agents in same location who are active
    var agentGr = new GlideRecord('sys_user');
    agentGr.addQuery('location', requesterLocation);
    agentGr.addQuery('active', true);
    agentGr.addQuery('user_roles', 'CONTAINS', 'catalog_agent');  // Has role
    agentGr.query();
    
    var availableAgent = null;
    var lowestWorkload = 999;
    
    while (agentGr.next()) {
        // Count active assignments for this agent
        var assignmentGr = new GlideRecord('sc_req_item');
        assignmentGr.addQuery('assigned_to', agentGr.sys_id);
        assignmentGr.addQuery('state', 'NOT IN', '3,4,7');  // Not closed states
        assignmentGr.query();
        
        var workload = assignmentGr.getRowCount();
        if (workload < 10 && workload < lowestWorkload) {
            lowestWorkload = workload;
            availableAgent = agentGr.sys_id.toString();
        }
    }
    
    if (availableAgent) {
        current.assigned_to = availableAgent;
        current.state = 2;  // Work in Progress
    }
    
})(current, previous);

Watch for performance issues when querying inside loops — this pattern can generate dozens of database calls. Consider pre-loading assignment counts in a single query with GlideAggregate instead. Also ensure the user_roles field query has proper indexing or you'll see timeout errors in production.

Integration Data Sync with Change Tracking

A scheduled integration needs to identify CMDB records that have changed since the last sync and send updates to an external asset management system. The script queries for CIs modified in the last hour with specific change types.

Scheduled Job — CMDB Sync.js
var SyncAssets = Class.create();
SyncAssets.prototype = {
    
    syncChangedCIs: function() {
        var lastSync = gs.getProperty('cmdb.last_sync_time', gs.minutesAgo(60));
        
        // Find CIs that changed since last sync
        var ciGr = new GlideRecord('cmdb_ci_server');
        ciGr.addQuery('sys_updated_on', '>', lastSync);
        ciGr.addQuery('install_status', '!=', '7');  // Not retired
        ciGr.addQuery('operational_status', '!=', '6');  // Not disposed
        ciGr.addQuery('u_sync_enabled', true);  // Custom field for sync control
        ciGr.query();
        
        var updatedRecords = [];
        
        while (ciGr.next()) {
            // Check what fields actually changed
            var auditGr = new GlideRecord('sys_audit');
            auditGr.addQuery('tablename', 'cmdb_ci_server');
            auditGr.addQuery('documentkey', ciGr.sys_id);
            auditGr.addQuery('sys_created_on', '>', lastSync);
            auditGr.addQuery('fieldname', 'IN', 'name,ip_address,cpu_count,ram');  // Only sync these fields
            auditGr.orderByDesc('sys_created_on');
            auditGr.query();
            
            if (auditGr.hasNext()) {
                updatedRecords.push(this._buildSyncPayload(ciGr));
            }
        }
        
        return this._sendToExternalSystem(updatedRecords);
    },
    
    type: 'SyncAssets'
};

Audit table queries can be extremely expensive on large instances. Always include documentkey and date range filters to avoid full table scans. Consider maintaining your own change tracking table instead of relying on sys_audit for high-frequency sync operations.

Dynamic Approval Routing with Escalation

A workflow needs to route purchase requests to different approval chains based on amount, category, and requester's department. High-value requests require additional approvals, and the system must check for previous approvals by the same manager to avoid duplicates.

Flow Script — Approval Routing.js
var approvers = [];
var requestAmount = parseFloat(inputs.request_amount);
var category = inputs.category;
var requesterId = inputs.requester_id;

// Get requester's manager and department
var userGr = new GlideRecord('sys_user');
userGr.addQuery('sys_id', requesterId);
userGr.query();
if (userGr.next()) {
    var managerId = userGr.manager.toString();
    var department = userGr.department.toString();
    
    // Always require manager approval first
    if (managerId) {
        approvers.push({user: managerId, level: 1});
    }
    
    // High-value requests need department head approval
    if (requestAmount > 5000) {
        var deptGr = new GlideRecord('cmn_department');
        deptGr.addQuery('sys_id', department);
        deptGr.addQuery('dept_head', '!=', '');
        deptGr.query();
        if (deptGr.next()) {
            var deptHead = deptGr.dept_head.toString();
            if (deptHead != managerId) {  // Avoid duplicate approvals
                approvers.push({user: deptHead, level: 2});
            }
        }
    }
    
    // IT category requests need additional IT approval
    if (category == 'hardware' || category == 'software') {
        var itGr = new GlideRecord('sys_user');
        itGr.addQuery('department.name', 'Information Technology');
        itGr.addQuery('title', 'CONTAINS', 'Manager');
        itGr.addQuery('active', true);
        itGr.query();
        if (itGr.next()) {
            approvers.push({user: itGr.sys_id.toString(), level: 3});
        }
    }
}

outputs.approval_chain = JSON.stringify(approvers);

Department and organizational queries can return unexpected results when users have multiple roles or departments change. Always validate that your approval chain has at least one approver before proceeding. Consider caching department head lookups as they rarely change but get queried frequently.

The Classic Mistake

⚠️

Calling addQuery() inside a GlideRecord loop to build dynamic conditions, expecting all queries to apply to the next query() call.

Anti-pattern — Do Not Use This.js
// Trying to build complex AND conditions dynamically
var gr = new GlideRecord('incident');
var conditions = ['priority=1', 'state=2', 'assignment_group=hardware'];

// Developer thinks this builds one query with all conditions
for (var i = 0; i < conditions.length; i++) {
    var parts = conditions[i].split('=');
    gr.addQuery(parts[0], parts[1]);
    
    // WRONG: calling query() inside the loop
    gr.query();
    
    while (gr.next()) {
        gs.log('Found: ' + gr.number);
    }
}

This executes three separate database queries instead of one combined query. Each query() call resets the GlideRecord's result set, so only the last condition (assignment_group=hardware) actually gets applied. ServiceNow doesn't accumulate addQuery() calls across multiple query() executions — each query execution starts fresh with whatever conditions exist at that moment. You'll see this in the generated SQL in System Log > All where three separate SELECT statements appear instead of one with a proper WHERE clause.

The Fix.js
// Build ALL conditions first, THEN query once
var gr = new GlideRecord('incident');
var conditions = ['priority=1', 'state=2', 'assignment_group=hardware'];

// Add all conditions before calling query()
for (var i = 0; i < conditions.length; i++) {
    var parts = conditions[i].split('=');
    gr.addQuery(parts[0], parts[1]);
}

// Single query() call with all conditions ANDed together
gr.query();
while (gr.next()) {
    gs.log('Found: ' + gr.number);
}
💡

Never call query() until you've added ALL conditions. Once query() executes, your condition-building phase is over.

Performance Rules

  1. Index your addQuery() fields. Queries on non-indexed fields over 10,000 records trigger table scans that cause 30+ second response times and administrator timeout alerts.
  2. Put the most selective condition first. Use addQuery('sys_created_on', '>', '2024-01-01') before addQuery('active', true) to let the database eliminate millions of records early.
  3. Avoid CONTAINS and STARTSWITH operators on string fields over 100,000 records. These force full text scans that crash mobile browsers and generate slow query warnings.
  4. Limit reference field traversals to 2 levels maximum. addQuery('caller_id.manager.department', 'IT') creates expensive JOINs that timeout on large datasets and bypass query optimizations.
  5. Use setLimit() when you know you only need a subset. Without limits, GlideRecord loads ALL matching records into memory, causing OutOfMemory exceptions on queries returning over 50,000 records.
  6. Chain multiple addQuery() calls rather than using addEncodedQuery() for complex conditions. This lets ServiceNow optimize individual conditions and produces clearer execution plans.
  7. Test queries against production data volumes in System Definition > Tables before deployment. Development instances with 1,000 incident records won't reveal the performance problems that appear with 10 million records.
  8. Monitor System Logs > Slow Queries after deployment. Queries taking over 5 seconds appear here with full execution plans, letting you identify which addQuery() conditions need optimization.

Side Effects & Platform Behavior

  • ACL evaluation occurs for every field referenced in addQuery() conditions, even if the user never sees those fields. Query conditions on restricted fields fail silently, returning empty result sets.
  • Domain separation applies automatically to addQuery() results. Records outside the current user's domain won't appear even if they match all other conditions, unless you call setDomainQuery(false) first.
  • Query execution writes to sys_db_cache and sys_statistics tables for performance monitoring. Frequent identical queries consume cache memory and skew performance analytics.
  • Business Rules with query timing don't fire for addQuery() operations. Only before query Business Rules execute, and they can modify your conditions before database execution.
  • Client-side addQuery() calls serialize to SOAP requests visible in browser Network tabs. Each condition becomes a separate XML element, exposing query logic to client-side inspection.
  • Database views and extending tables automatically filter addQuery() results. Querying task returns incidents and change requests; querying incident adds implicit sys_class_name='incident' conditions.
  • Script execution context affects query scope. addQuery() in Scoped Applications can't access global table fields without explicit cross-scope privileges, causing unexpected empty results.
  • Transaction boundaries matter for data consistency. addQuery() inside Business Rules may not see updates made earlier in the same transaction, returning stale data until the transaction commits.
  • Workflow and Flow Designer activities that use addQuery() write activity execution logs to wf_executing and sys_flow_context tables, including the full query conditions for audit purposes.
  • Debug mode adds addQuery() execution traces to sys_log_transaction with full SQL statements and execution times. This data persists for 7 days and can expose sensitive query conditions.

Debugging When It Breaks

When addQuery() fails, you'll typically see empty result sets when you expect data, or timeout errors in production. Users report missing records or incomplete lists, while developers see hasNext() returning false immediately. The most deceptive symptom is getting results in development but none in production, usually indicating ACL restrictions or domain separation issues that only manifest with real user contexts and data volumes.

Check System Log > All first for database-level errors like "Invalid column name" or "Table doesn't exist". Browser console shows client-side failures with messages like "Security constraints restrict this operation" or "SOAP fault". For server-side scripts, enable debug logging in System Diagnostics > Debug and look for transaction logs that show the actual SQL generated from your conditions.

Common error patterns include "Field 'xyz' doesn't exist" when you misspell field names, "Access denied" when ACLs block field access, and "Query timeout" when conditions aren't selective enough. Look for log entries containing your table name followed by execution times over 10 seconds, which indicate performance problems that will fail in production.

Quick diagnostic checklist:

  • Verify field names in System Definition > Dictionary — case-sensitive and no spaces
  • Test the same conditions manually in list filters to isolate query logic from code issues
  • Check user roles and ACLs if getting empty results with admin credentials work fine
  • Add gs.log(gr.getRowCount()) after query() to confirm how many records match
  • Use gr.getEncodedQuery() before query() to see the final query string being executed

Quick Reference

  • Chain multiple addQuery() calls before query() — they automatically AND together
  • Use addNullQuery() and addNotNullQuery() for null checks, not addQuery('field', '')
  • Reference field dot-walking works: addQuery('caller_id.department', 'IT') queries the caller's department field
  • Date queries need proper format: addQuery('sys_created_on', '>', '2024-01-01 00:00:00') or use gs.dateGenerate()
  • Boolean fields accept true/false or 1/0: addQuery('active', true) is cleaner than '1'
  • Choice field queries are case-sensitive: addQuery('state', '2') not 'In Progress' or 'in progress'
  • Use IN operator for multiple values: addQuery('priority', 'IN', '1,2,3') instead of multiple OR conditions
  • String operators include CONTAINS, STARTSWITH, ENDSWITH, DOES NOT CONTAIN — all case-insensitive
  • Client-side addQuery() requires g_form.getReference() or GlideAjax for server roundtrips
  • Always check hasNext() or getRowCount() > 0 before assuming records exist in the result set