What It Is

GlideRecord is ServiceNow's server-side JavaScript API for database operations — the bridge between your business logic and the underlying MySQL database. It abstracts SQL operations into an object-oriented interface, letting you query, create, update, and delete records without writing raw SQL. Every piece of automation in ServiceNow that touches data — business rules, script includes, scheduled jobs, REST APIs — relies on GlideRecord to interact with tables. Without it, you're stuck with read-only GlideForm operations on the client side.

Architecturally, GlideRecord exists exclusively in server-side JavaScript contexts — business rules, script includes, scheduled scripts, transform maps, and workflow activities. It cannot run in client scripts, UI policies, or catalog client scripts because those execute in the browser where direct database access would be a massive security hole. When you call new GlideRecord('incident') on the server, you're creating an instance that can directly manipulate the incident table through ServiceNow's security layer. The platform handles ACL checks, business rule triggers, and audit logging behind the scenes.

Under the hood, ServiceNow translates your GlideRecord operations into parameterized SQL statements executed against the MySQL database. When you chain methods like gr.addQuery('state', '6').query(), the platform builds a SELECT statement with proper table joins (for reference fields), applies your current user's ACL restrictions, and executes it. The result set gets cached in the GlideRecord instance, which you iterate through with next(). For writes, ServiceNow queues the changes until insert() or update() triggers the actual database transaction and any associated business rules.

Without GlideRecord, you cannot automate ServiceNow. You cannot create incidents from integrations, update user records in bulk, or build approval workflows that modify request items. Client-side GlideForm can only read and modify the current form's fields — it cannot query other tables or create new records. The REST API and Import Sets ultimately use GlideRecord internally for their database operations. Even ServiceNow's own applications — ITSM, ITOM, SecOps — are built on thousands of business rules and script includes that manipulate data through GlideRecord calls.

Administrators use GlideRecord in simple business rules for field calculations and notifications. Developers build complex script includes with GlideRecord for reusable business logic, integration endpoints, and data processing jobs. Architects design GlideRecord patterns for performance at scale — batch processing, efficient queries, and caching strategies. The API scales from single-record lookups in a form business rule to processing thousands of records in scheduled jobs. Your comfort level with GlideRecord directly determines what you can build in ServiceNow.

GlideRecord relates closely to GlideAggregate for COUNT, SUM, and GROUP BY operations that don't require iterating individual records. GlideElement objects represent individual field values within a GlideRecord, providing methods like getDisplayValue() and getRefRecord(). The Table API provides metadata about field types and table structure that complements GlideRecord's data operations. Understanding all three APIs together gives you complete control over ServiceNow's data layer.

How It Works Under the Hood

When you instantiate a GlideRecord, ServiceNow creates a stateful object that maintains a connection to the database and tracks your query conditions, field modifications, and result set position. The object doesn't immediately hit the database — it builds your query through method chaining until you call query(), get(), or getDisplayValue(). This lazy evaluation pattern lets ServiceNow optimize the final SQL by combining conditions, eliminating redundant clauses, and choosing efficient indexes.

The platform maintains separate execution contexts for different script types, each with different GlideRecord capabilities. Business rules execute with full database access and can trigger additional business rules through their writes. Script includes run in a sandboxed context but can call other script includes and access most tables. Scheduled scripts run as the system user with elevated privileges but limited access to session data. Client-callable script includes via AJAX have restricted table access and cannot perform certain operations like user impersonation.

ServiceNow automatically handles transaction management, rollback scenarios, and business rule cascades that most developers never see. When a business rule's GlideRecord operation fails, the platform can rollback the entire transaction including the original record change that triggered the rule. The query cache persists across multiple next() iterations until you call query() again or the script ends, which is why reusing a GlideRecord instance for different queries requires careful attention to state.

The Query Execution Lifecycle

  1. GlideRecord instantiation creates an empty query builder object with table metadata loaded from the dictionary
  2. Query conditions via addQuery() and addEncodedQuery() get stored as query parameters, not executed
  3. First call to query() triggers SQL generation with ACL filtering and security constraints applied
  4. Database execution returns a result set that gets cached in the GlideRecord instance memory
  5. Each next() call advances the cursor through cached results and populates field values from the current row
  6. Field modifications via setValue() get queued locally until update() or insert() commits them to database
  7. Write operations trigger business rules, update version history, and fire notifications before returning control to your script
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, isTemplate) {
    // Client-side cannot use GlideRecord - must call server via AJAX
    if (isLoading || newValue == '') {
        return;
    }
    
    // Call server-side script include to get assignment group info
    var ga = new GlideAjax('IncidentUtils');
    ga.addParam('sysparm_name', 'getAssignmentGroupInfo');
    ga.addParam('sysparm_category', newValue);
    
    ga.getXML(function(response) {
        var answer = response.responseXML.documentElement.getAttribute('answer');
        if (answer) {
            var data = JSON.parse(answer);
            // Update assignment group field with server response
            g_form.setValue('assignment_group', data.groupId);
            g_form.setValue('u_expected_resolution', data.expectedHours);
        }
    });
}
Script Include — IncidentUtils.js
var IncidentUtils = Class.create();
IncidentUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    getAssignmentGroupInfo: function() {
        var category = this.getParameter('sysparm_category');
        var result = {};
        
        // Query assignment groups based on category - this requires GlideRecord
        var gr = new GlideRecord('sys_user_group');
        gr.addQuery('u_supported_categories', 'CONTAINS', category);
        gr.addQuery('active', true);
        gr.orderBy('u_workload'); // Get least busy group
        gr.setLimit(1);
        
        if (gr.next()) {
            result.groupId = gr.getUniqueValue();
            // Get SLA expectations from group's configuration
            result.expectedHours = gr.getValue('u_resolution_hours') || '24';
        }
        
        return JSON.stringify(result);
    },
    
    type: 'IncidentUtils'
});

Real-World Scenarios

Escalating Stale Incidents in Scheduled Jobs

Operations teams need incidents automatically escalated when they sit untouched beyond SLA thresholds. A daily scheduled job queries for stale incidents and updates their priority and assignment.

Scheduled Script Job — Escalate Stale Incidents.js
// Find incidents untouched for 48+ hours with priority 3 or lower
var gr = new GlideRecord('incident');
gr.addQuery('state', 'IN', '1,2,6'); // New, In Progress, Resolved
gr.addQuery('priority', '>=', '3'); // Priority 3, 4, 5
gr.addQuery('sys_updated_on', '<', 'javascript:gs.daysAgoStart(2)');
gr.addQuery('assigned_to', '!=', '');

gr.query();
var escalatedCount = 0;

while (gr.next()) {
    // Bump priority up one level (3->2, 4->3, 5->4)
    var currentPriority = parseInt(gr.getValue('priority'));
    if (currentPriority > 1) {
        gr.setValue('priority', currentPriority - 1);
        gr.setValue('work_notes', 'Auto-escalated due to 48hr inactivity');
        
        // Reassign to group manager for priority 2 incidents
        if (currentPriority - 1 == 2) {
            var mgr = this.getGroupManager(gr.assignment_group.getRefRecord());
            if (mgr) gr.setValue('assigned_to', mgr);
        }
        
        gr.update();
        escalatedCount++;
    }
}

gs.info('Escalated {0} stale incidents', escalatedCount);

Watch for query performance with date ranges — always use indexed fields like sys_updated_on rather than calculated fields. The getRefRecord() call creates a new GlideRecord instance which adds overhead in loops. Consider batching these operations with setWorkflow(false) if business rules aren't needed.

When incidents get resolved, the business wants automatic creation of follow-up tasks for documentation and customer communication. This business rule fires on incident resolution to create structured follow-up work.

Business Rule — Incident Resolution Tasks.js
// After Update business rule on incident table
(function executeRule(current, previous) {
    // Only run when state changes to Resolved (6) or Closed (7)
    if (current.state != '6' && current.state != '7') return;
    if (previous.state == '6' || previous.state == '7') return;
    
    var tasks = [
        {short_description: 'Update knowledge base with resolution', hours: 2},
        {short_description: 'Send resolution summary to requestor', hours: 1}
    ];
    
    for (var i = 0; i < tasks.length; i++) {
        var task = new GlideRecord('sc_task');
        task.initialize(); // Sets defaults from dictionary
        task.setValue('short_description', tasks[i].short_description);
        task.setValue('description', 'Follow-up task for incident ' + current.number);
        task.setValue('parent', current.getUniqueValue());
        task.setValue('assignment_group', current.getValue('assignment_group'));
        task.setValue('work_duration', tasks[i].hours * 3600); // Convert to seconds
        task.setValue('due_date', this.calculateDueDate(tasks[i].hours));
        
        var taskId = task.insert();
        gs.info('Created follow-up task {0} for incident {1}', taskId, current.number);
    }
})(current, previous);

Always call initialize() on new records to populate default values from the dictionary. Business rules that create records can trigger infinite loops if they modify the same table they're triggered on. The previous parameter comparison prevents the rule from running multiple times on the same state change.

Bulk User Updates with Error Handling

HR integration requires updating hundreds of user records when organizational changes occur. This script include processes bulk updates with proper error handling and rollback capabilities for failed operations.

Script Include — UserBulkUpdate.js
updateUserDepartments: function(userUpdates) {
    var successCount = 0;
    var errors = [];
    
    // Disable business rules for performance - we'll handle notifications manually
    var gr = new GlideRecord('sys_user');
    gr.setWorkflow(false);
    
    for (var i = 0; i < userUpdates.length; i++) {
        var update = userUpdates[i];
        gr.initialize();
        
        if (gr.get('employee_number', update.employeeId)) {
            try {
                gr.setValue('department', update.newDepartmentId);
                gr.setValue('manager', update.newManagerId);
                gr.setValue('u_effective_date', update.effectiveDate);
                
                if (gr.update()) {
                    successCount++;
                } else {
                    errors.push('Update failed for employee ' + update.employeeId);
                }
            } catch (ex) {
                errors.push('Error updating ' + update.employeeId + ': ' + ex.message);
                gs.error('User update failed: {0}', ex);
            }
        } else {
            errors.push('Employee not found: ' + update.employeeId);
        }
    }
    
    return {success: successCount, errors: errors};
},

Bulk operations benefit from setWorkflow(false) to bypass business rules and improve performance. Always wrap update() operations in try-catch blocks for production scripts. Reusing the same GlideRecord instance with initialize() between iterations is more memory-efficient than creating new instances but requires careful state management.

The Classic Mistake

⚠️

Never call query() inside a while(gr.next()) loop — you'll reset the iterator and create an infinite loop.

Anti-pattern — Do Not Use This.js
// Trying to get incidents AND their related problems
var gr = new GlideRecord('incident');
gr.addQuery('state', '!=', 7);
gr.query();

while (gr.next()) {
    gs.info('Processing incident: ' + gr.number);
    
    // DISASTER: This resets the iterator!
    var problemGr = new GlideRecord('problem');
    problemGr.addQuery('related_incidents', 'CONTAINS', gr.sys_id);
    problemGr.query();
    
    // This will run forever because gr.next() starts over
    while (problemGr.next()) {
        gs.info('Related problem: ' + problemGr.number);
    }
}

This creates an infinite loop because calling query() inside the loop resets the outer GlideRecord's iterator position back to the beginning. ServiceNow's transaction timeout will eventually kill the script after 30 seconds, throwing a "Transaction cancelled: maximum execution time exceeded" error in the System Log. You'll see the same incident number logged repeatedly before the timeout hits. The platform treats each query() call as a fresh database query that interferes with any active iterators.

The Fix.js
// Proper way: separate the queries completely
var incidents = [];
var gr = new GlideRecord('incident');
gr.addQuery('state', '!=', 7);
gr.query();

// First, collect all incident sys_ids
while (gr.next()) {
    incidents.push({
        sys_id: gr.sys_id.toString(),
        number: gr.number.toString()
    });
}

// Then process each incident separately
for (var i = 0; i < incidents.length; i++) {
    gs.info('Processing incident: ' + incidents[i].number);
    
    var problemGr = new GlideRecord('problem');
    problemGr.addQuery('related_incidents', 'CONTAINS', incidents[i].sys_id);
    problemGr.query();
    
    while (problemGr.next()) {
        gs.info('Related problem: ' + problemGr.number);
    }
}
💡

One GlideRecord query() per logical operation — never nest query() calls or mix different record iterations.

Performance Rules

  1. Never iterate over more than 1,000 records without setLimit() — beyond this threshold, Business Rules and other triggers will push execution time over 30 seconds, causing transaction timeouts and angry sys admin tickets.
  2. Always add indexed fields to addQuery() first — queries without indexes on tables over 100k records trigger database table scans that lock the table and degrade platform performance for all users.
  3. Use getRowCount() instead of counting in loops — counting 50+ records manually consumes 10x more memory and processing time than letting the database COUNT() function handle it.
  4. Call setWorkflow(false) for bulk operations — workflow processing on batches over 100 records will spawn thousands of workflow contexts, exhausting the workflow engine and causing record lock conflicts.
  5. Never use CONTAINS or LIKE operators on text fields longer than 1000 characters — these force full-text scans that can take 45+ seconds per query on journal fields, comments, or work notes.
  6. Avoid OR queries with more than 3 conditions — the database query planner cannot optimize complex OR statements, leading to full table scans that degrade response times from milliseconds to 15+ seconds.
  7. Use chooseWindow() for pagination instead of setLimit() with offsets — manual offset calculation forces the database to process and discard all preceding records, making page 50 of a report 50x slower than page 1.
  8. Always use autoSysFields(false) for read-only operations — automatically updating sys_mod_count and sys_updated_on during queries that only read data creates unnecessary database write locks and audit entries.

Side Effects & Platform Behavior

  • Every insert(), update(), and deleteRecord() triggers all active Business Rules (before/after/async), ACL evaluations, and notification processing for that table — a single update can spawn 10+ background jobs.
  • Audit fields (sys_created_by, sys_updated_on, sys_mod_count) are automatically populated on every write operation — these write to the base table and create entries in sys_audit if auditing is enabled for that table.
  • Dictionary overrides and field-level ACLs execute during getValue() and setValue() calls — restricted fields may return empty values even when data exists, visible in System Log > Security as "ACL denied" entries.
  • Reference field access automatically queries the target table — calling gr.assignment_group.name executes a hidden query to sys_user_group table, creating additional database load and potential ACL checks.
  • Table inheritance means querying task also searches incident, problem, change_request and all other child tables — performance degrades exponentially with the number of inherited tables involved.
  • Domain separation applies automatically to all GlideRecord operations — records in other domains become invisible without switching domain context, potentially breaking scripts that expect to see all data.
  • Journal fields (work_notes, comments) write to separate journal tables (sys_journal_field) and trigger email notifications to watchers — a single comment update can send dozens of emails.
  • Using GlideRecord in Client Scripts creates synchronous AJAX calls that block the browser UI — each server round-trip freezes the form for 200-500ms, making the interface feel sluggish to end users.
  • Transaction rollbacks affect ALL GlideRecord operations in the same transaction — if a Business Rule throws an error after your update() call, your changes get rolled back even though your code succeeded.
  • Workflow activities automatically pause at GlideRecord operations that modify the current record — this can create deadlocks where workflows wait indefinitely for record locks to clear.

Debugging When It Breaks

The most common failure is the silent failure — your GlideRecord code runs without errors but finds zero records or updates nothing. Users report "the automation isn't working" while developers see no error messages. The culprit is usually ACLs blocking field access, domain restrictions filtering results, or query conditions that look correct but don't match the actual data format. Check System Log > All for "ACL denied" messages and Script Debugger for the actual SQL being generated.

Transaction timeouts manifest as "Transaction cancelled: maximum execution time exceeded" errors in System Log > Errors, usually accompanied by user complaints about forms that "submit but don't save." The browser shows a generic error page or hangs indefinitely. These happen when iterating over large result sets, triggering cascading Business Rules, or running complex queries without proper indexing. Enable SQL debugging in System Diagnostics > Session Debug > SQL to see which queries are taking too long.

Quick diagnostic checklist when GlideRecord misbehaves:

  • Add gs.info('Query: ' + gr.getEncodedQuery()) before query() to verify the generated query string
  • Check getRowCount() immediately after query() — if it's 0, your query conditions are wrong
  • Verify field names with gr.isValidField('field_name') — typos in field names fail silently
  • Test with setWorkflow(false) to isolate Business Rule interference
  • Check your domain context — switch to global domain if records seem to be missing
  • Look for "Invalid table" or "Security constraints" messages in System Log > Security

Quick Reference

  • Always call toString() on GlideElement values before storing in arrays or comparing — GlideElement objects behave unpredictably outside the loop context
  • Reference field dot-walking (gr.caller_id.email) only works 4 levels deep — beyond that, you get empty strings without error messages
  • Use addNullQuery() instead of addQuery('field', '') — empty string and null are different in ServiceNow's database layer
  • The sys_id field is always indexed — use it for joins and lookups instead of display fields like number or name
  • Choice fields store internal values, not display values — query for 'New' not '1' on the state field, use getDisplayValue() to get the label
  • Date/time queries need the exact database format — use gs.dateGenerate() to convert human dates to the required yyyy-MM-dd HH:mm:ss format
  • Boolean fields accept 'true'/'false' strings, not JavaScript booleans — use setValue('active', 'true') not setValue('active', true)
  • Use setAbortAction(true) in Business Rules to prevent save/update operations — much cleaner than throwing exceptions
  • Journal fields append content with setJournalEntry() but overwrite with setValue() — choose the right method or lose existing comments
  • Client-side GlideRecord is synchronous and blocking — every call freezes the UI, use GlideAjax for production client scripts instead