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
- Script calls
addQuery('field', 'operator', 'value')— parameters stored in internal condition array - Additional
addQuery()calls append more conditions to the same array - Script calls
query()— triggers query compilation process - Platform resolves display values, applies ACLs, validates field names and operators
- ORM generates SQL SELECT with WHERE conditions joined by AND operators
- Database executes query, returns result set to GlideRecord instance for iteration
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) {
// 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');
}
});
}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.
(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.
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.
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.
// 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.
// 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
- 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. - Put the most selective condition first. Use
addQuery('sys_created_on', '>', '2024-01-01')beforeaddQuery('active', true)to let the database eliminate millions of records early. - Avoid
CONTAINSandSTARTSWITHoperators on string fields over 100,000 records. These force full text scans that crash mobile browsers and generate slow query warnings. - 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. - 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. - Chain multiple
addQuery()calls rather than usingaddEncodedQuery()for complex conditions. This lets ServiceNow optimize individual conditions and produces clearer execution plans. - Test queries against production data volumes in
System Definition > Tablesbefore deployment. Development instances with 1,000 incident records won't reveal the performance problems that appear with 10 million records. - Monitor
System Logs > Slow Queriesafter deployment. Queries taking over 5 seconds appear here with full execution plans, letting you identify whichaddQuery()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 callsetDomainQuery(false)first. - Query execution writes to
sys_db_cacheandsys_statisticstables for performance monitoring. Frequent identical queries consume cache memory and skew performance analytics. - Business Rules with
querytiming don't fire foraddQuery()operations. Onlybefore queryBusiness 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. Queryingtaskreturns incidents and change requests; queryingincidentadds implicitsys_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 towf_executingandsys_flow_contexttables, including the full query conditions for audit purposes. - Debug mode adds
addQuery()execution traces tosys_log_transactionwith 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())afterquery()to confirm how many records match - Use
gr.getEncodedQuery()beforequery()to see the final query string being executed
Quick Reference
- Chain multiple
addQuery()calls beforequery()— they automatically AND together - Use
addNullQuery()andaddNotNullQuery()for null checks, notaddQuery('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 usegs.dateGenerate() - Boolean fields accept
true/falseor1/0:addQuery('active', true)is cleaner than'1' - Choice field queries are case-sensitive:
addQuery('state', '2')not'In Progress'or'in progress' - Use
INoperator 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()requiresg_form.getReference()orGlideAjaxfor server roundtrips - Always check
hasNext()orgetRowCount() > 0before assuming records exist in the result set