What It Is

The getRefRecord() method on GlideElement objects returns a GlideRecord for the referenced record in a reference field without executing a separate database query. When you access a reference field directly, ServiceNow performs a join operation internally, but getRefRecord() gives you a proper GlideRecord object with all its methods and properties available. This isn't syntactic sugar—it's a performance optimization that leverages ServiceNow's existing join data rather than hitting the database again. The method exists at the GlideElement layer, meaning it's available on any reference field from both server-side and client-side contexts where GlideRecord access is permitted.

Architecturally, getRefRecord() executes on the server side in all contexts—even when called from client scripts, it triggers a server-side lookup. In Business Rules, Script Includes, and other server-side scripts, it's a direct method call that accesses the already-cached reference data. In Client Scripts or UI Policies, calling getRefRecord() initiates an AJAX request to retrieve the referenced record. This distinction matters because client-side usage introduces asynchronous behavior and network latency that doesn't exist in server-side contexts. The method returns null if the reference field is empty or points to a non-existent record.

Under the hood, ServiceNow maintains reference field data through sophisticated caching and join mechanisms that most developers never see. When you query a GlideRecord, the platform automatically performs left outer joins for reference fields that might be accessed, storing this data in memory. The getRefRecord() method taps into this pre-loaded data, wrapping it in a GlideRecord object without the overhead of constructing a new query. This is why accessing incident.caller_id.getRefRecord().department performs better than creating a new GlideRecord query on sys_user with the caller ID as a filter. The platform has already done the heavy lifting during the initial query execution.

Without getRefRecord(), you're forced into one of two suboptimal patterns: direct dot-walking through reference fields (which works but gives you string values, not GlideRecord methods), or manually constructing new GlideRecord queries for every reference you need to access programmatically. The dot-walking approach like incident.caller_id.name returns display values but doesn't give you access to getValue(), getDisplayValue(), or other GlideRecord methods you need for complex field manipulation. Manual GlideRecord construction works but introduces unnecessary database load and code complexity. You cannot efficiently access multiple fields from the same referenced record, perform field-level operations, or chain to further references without this method.

Developers use getRefRecord() most frequently in Business Rules and Script Includes where performance matters and multiple referenced field values are needed. Admins encounter it less directly but benefit from its use in custom applications and integrations. Architects rely on it heavily when designing efficient data access patterns in large implementations where database optimization is critical. The method appears most commonly in audit trail generation, automated approval logic, and integration scripts where referenced user, group, or configuration item data must be processed programmatically. It's essential in any scenario where you're building dynamic content, performing bulk operations on records with references, or implementing custom workflow logic that depends on related record data.

The method sits at the intersection of several core ServiceNow concepts that developers must understand. It's intimately related to reference field architecture—you can't use getRefRecord() on non-reference fields without errors. It complements dot-walking syntax by providing object-oriented access to the same underlying data that dot-walking exposes as strings. Understanding getRefRecord() requires solid knowledge of GlideRecord lifecycle management because the returned GlideRecord object follows the same rules for modification, updates, and field access as any other GlideRecord instance. The performance characteristics make it a critical tool in the broader context of ServiceNow's query optimization strategy, working alongside proper filtering, field limiting, and other database efficiency techniques.

How It Works Under the Hood

When ServiceNow executes a GlideRecord query, the platform doesn't just retrieve the requested record—it performs strategic left outer joins on reference fields to preload related data it anticipates you might access. This happens regardless of whether you plan to use getRefRecord() or not, as part of ServiceNow's broader performance optimization strategy. The platform maintains this joined data in memory as part of the GlideRecord object's internal state, tagged by field name and accessible through the GlideElement interface. This preloading explains why initial GlideRecord queries can seem slower than expected—ServiceNow is doing extra work upfront to optimize subsequent field access patterns.

The getRefRecord() method acts as a factory pattern, constructing a new GlideRecord object from this cached reference data without hitting the database. The returned GlideRecord is fully functional with all standard methods available, but it represents a snapshot of the referenced record's state at query time, not real-time data. This distinction becomes critical in long-running scripts where the referenced record might change between your original query and when you call getRefRecord()—you're working with potentially stale data. If the reference field value is null or points to a non-existent record, the method returns null rather than an empty GlideRecord, which requires explicit null checking in production code.

The Request Lifecycle

  1. Initial GlideRecord query executes with automatic left outer joins on reference fields based on ServiceNow's internal heuristics and field access patterns
  2. Platform caches joined reference data in memory as part of the GlideElement object structure, indexed by field name and sys_id
  3. Developer calls getRefRecord() on a reference field's GlideElement object during script execution
  4. Method checks internal cache for reference data; if present, constructs a new GlideRecord object populated with the cached field values
  5. If cache miss occurs (rare but possible with complex reference chains), method falls back to executing a direct database query for the referenced sys_id
  6. Resulting GlideRecord object is returned to calling code with full method access and field manipulation capabilities, but represents a point-in-time snapshot
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

Business Rule — Incident Assignment.js
// Business Rule: Before Update on incident table
// Purpose: Auto-assign based on caller's department and location

(function executeRule(current, previous) {
    // Verify we have a caller and the assignment_group is empty
    if (!current.caller_id.nil() && current.assignment_group.nil()) {
        
        // Get the caller's full record without additional database query
        var caller = current.caller_id.getRefRecord();
        
        if (caller && !caller.department.nil()) {
            // Access multiple caller fields efficiently from cached data
            var deptId = caller.department.getValue();
            var locationId = caller.location.getValue();
            var vipStatus = caller.vip.getValue();
            
            // Use Script Include to determine assignment group
            var assignmentUtil = new IncidentAssignmentUtil();
            var groupId = assignmentUtil.getGroupForDepartment(deptId, locationId, vipStatus);
            
            if (groupId) {
                current.assignment_group = groupId;
                current.work_notes = 'Auto-assigned based on caller department: ' + caller.department.getDisplayValue();
            }
        }
    }
})(current, previous);
Script Include — IncidentAssignmentUtil.js
// Script Include: Server-side utility for assignment logic
// Purpose: Centralized business logic for incident assignment

var IncidentAssignmentUtil = Class.create();
IncidentAssignmentUtil.prototype = {
    initialize: function() {
        // Initialize any needed properties or cache
    },
    
    getGroupForDepartment: function(deptId, locationId, isVip) {
        // Build query with multiple filters for optimal group selection
        var groupQuery = new GlideRecord('sys_user_group');
        groupQuery.addActiveQuery();
        groupQuery.addQuery('type', 'incident_assignment');
        
        // Priority logic: VIP gets specialized groups
        if (isVip == 'true') {
            groupQuery.addQuery('name', 'CONTAINS', 'VIP');
        }
        
        // Location-based assignment takes precedence over department
        groupQuery.addQuery('location', locationId);
        groupQuery.addQuery('department', deptId);
        groupQuery.orderBy('order'); // Assume custom ordering field
        groupQuery.setLimit(1);
        
        if (groupQuery.next()) {
            return groupQuery.getUniqueValue();
        }
        
        // Fallback to department-only matching
        return this._getDepartmentGroup(deptId);
    },
    
    _getDepartmentGroup: function(deptId) {
        // Private method for fallback assignment logic
        var fallbackQuery = new GlideRecord('sys_user_group');
        fallbackQuery.addActiveQuery();
        fallbackQuery.addQuery('department', deptId);
        fallbackQuery.setLimit(1);
        
        return fallbackQuery.next() ? fallbackQuery.getUniqueValue() : null;
    },
    
    type: 'IncidentAssignmentUtil'
};

Real-World Scenarios

Approval Workflow with Manager Hierarchy

A service catalog request needs approval routing based on the requester's management chain and the requested item's cost. The system must traverse multiple reference fields efficiently to determine the appropriate approval path without creating excessive database load.

Business Rule — Catalog Request Approval.js
// Business Rule: After Insert on sc_req_item
// Purpose: Route approvals based on cost and management hierarchy

(function executeRule(current, previous) {
    var requester = current.request.requested_for.getRefRecord();
    if (!requester) return;
    
    // Get item cost and determine approval threshold
    var itemCost = parseFloat(current.price) || 0;
    var requiresManagerApproval = itemCost > 500;
    var requiresDirectorApproval = itemCost > 2000;
    
    if (requiresManagerApproval) {
        var manager = requester.manager.getRefRecord();
        if (manager) {
            // Create approval record with manager details
            this.createApproval(current, manager, 'Manager approval required for: ' + current.short_description);
            
            // Check if director approval also needed
            if (requiresDirectorApproval && !manager.manager.nil()) {
                var director = manager.manager.getRefRecord();
                if (director) {
                    this.createApproval(current, director, 'Director approval required - high value item');
                }
            }
        }
    }
    
    current.approval = requiresManagerApproval ? 'requested' : 'approved';
});

Watch for null reference chains—if the requester has no manager, getRefRecord() returns null and calling methods on it will error. Always check for null returns, especially in management hierarchies where the chain might break. The performance benefit only applies when you need multiple fields from the referenced record—if you only need one value, dot-walking is simpler.

Asset Discovery Integration

An integration script processes configuration items from discovery, needing to access assigned user details, location information, and business service mappings. The script runs nightly on thousands of CIs, making query efficiency critical for meeting maintenance windows.

Scheduled Job — CI Data Export.js
// Scheduled Script Execution: Nightly CI data export
// Purpose: Generate asset report with related record details

var ciQuery = new GlideRecord('cmdb_ci_computer');
ciQuery.addActiveQuery();
ciQuery.addNotNullQuery('assigned_to');
ciQuery.query();

var exportData = [];

while (ciQuery.next()) {
    // Get assigned user details without separate query
    var assignedUser = ciQuery.assigned_to.getRefRecord();
    var location = ciQuery.location.getRefRecord();
    
    if (assignedUser && location) {
        var ciData = {
            asset_tag: ciQuery.asset_tag.toString(),
            serial_number: ciQuery.serial_number.toString(),
            // Access multiple user fields efficiently
            user_name: assignedUser.user_name.toString(),
            user_email: assignedUser.email.toString(),
            user_department: assignedUser.department.getDisplayValue(),
            user_cost_center: assignedUser.cost_center.toString(),
            // Location details from cached reference
            location_name: location.name.toString(),
            building: location.parent.getDisplayValue(),
            city: location.city.toString()
        };
        
        exportData.push(ciData);
    }
}

In bulk operations like this, the performance difference between getRefRecord() and separate queries compounds dramatically. Always validate that referenced records exist before accessing their fields—discovery data often has incomplete reference field populations. Consider implementing batch processing with setLimit() to avoid memory issues on large datasets.

Dynamic Email Notification Content

A notification template needs personalized content including the incident caller's manager information, affected CI details, and assignment group contact information. The email must be generated efficiently since notifications can trigger in high volumes during outages.

Notification Script — Incident Alert.js
// Mail Script in Notification: Incident Assignment Alert
// Purpose: Generate personalized notification with related record data

// Access current incident (available in notification context)
var caller = current.caller_id.getRefRecord();
var assignedGroup = current.assignment_group.getRefRecord();
var affectedCI = current.cmdb_ci.getRefRecord();

// Build personalized email content
var emailBody = 'Dear ' + (caller ? caller.first_name + ' ' + caller.last_name : 'Valued Customer') + ',\n\n';

if (caller && !caller.manager.nil()) {
    var manager = caller.manager.getRefRecord();
    emailBody += 'Your manager ' + manager.name + ' has been notified of this incident.\n';
}

if (affectedCI) {
    emailBody += 'Affected System: ' + affectedCI.name + '\n';
    emailBody += 'Business Criticality: ' + affectedCI.business_criticality + '\n';
    
    // Check for CI owner information
    if (!affectedCI.owned_by.nil()) {
        var ciOwner = affectedCI.owned_by.getRefRecord();
        emailBody += 'System Owner: ' + ciOwner.name + ' (' + ciOwner.email + ')\n';
    }
}

if (assignedGroup) {
    emailBody += '\nAssigned to: ' + assignedGroup.name + '\n';
    emailBody += 'Contact: ' + assignedGroup.email + '\n';
}

// Return the constructed email body
template.print(emailBody);

Notification scripts execute frequently and under tight time constraints, making getRefRecord() essential for performance. Always provide fallback text for null references since notifications must be deliverable even with incomplete data. Be cautious with nested reference chains in notifications—each level increases the chance of null values that could break email generation. Test notification scripts with records that have missing reference field values to ensure graceful degradation.

The Classic Mistake

⚠️

Calling getRefRecord() on a field that's already null or empty without checking first.

Anti-pattern — Do Not Use This.js
// Business Rule on incident table
(function executeRule(current, previous) {
    // Dangerous: assignment_group might be empty
    var groupRecord = current.assignment_group.getRefRecord();
    
    // This will throw a NullPointerException
    var groupName = groupRecord.name.toString();
    var manager = groupRecord.manager.getDisplayValue();
    
    // Trying to use the values
    gs.log('Group: ' + groupName);
    gs.log('Manager: ' + manager);
    
    // This entire business rule fails silently
    current.work_notes = 'Assigned to ' + groupName;
})(current, previous);

When the reference field is empty, getRefRecord() returns null, not an empty GlideRecord. ServiceNow throws a java.lang.NullPointerException in the server logs when you try to access properties on null. The business rule stops executing completely, and any subsequent logic never runs. Users see no error message—the rule just silently fails, making this incredibly hard to debug in production.

The Fix.js
// Business Rule on incident table
(function executeRule(current, previous) {
    // Always check if the reference field has a value first
    if (!current.assignment_group.nil()) {
        var groupRecord = current.assignment_group.getRefRecord();
        
        // Additional safety check for the returned record
        if (groupRecord && groupRecord.isValidRecord()) {
            var groupName = groupRecord.name.toString();
            var manager = groupRecord.manager.getDisplayValue();
            
            gs.log('Group: ' + groupName);
            gs.log('Manager: ' + manager);
            current.work_notes = 'Assigned to ' + groupName;
        }
    }
})(current, previous);
💡

Always check nil() on the reference field before calling getRefRecord() — null reference fields return null records, not empty ones.

Performance Rules

  1. Never call getRefRecord() inside loops processing more than 50 records. Each call executes a database query. Processing 500+ records will trigger the 30-second transaction timeout and kill your script.
  2. Cache the returned GlideRecord in a variable if you need multiple field values. Calling getRefRecord() multiple times on the same field executes multiple identical queries, wasting database connections and memory.
  3. Use getDisplayValue() instead of getRefRecord() when you only need the display field value. getDisplayValue() uses cached data and doesn't hit the database.
  4. Avoid getRefRecord() in Client Scripts and UI Policies. It triggers synchronous server calls that freeze the browser for 2-5 seconds per call. Users will think the form is broken.
  5. Don't modify the returned GlideRecord and call update() from within Business Rules. This creates recursive Business Rule execution that will crash your instance with stack overflow errors.
  6. Never use getRefRecord() in Script Includes called by scheduled jobs processing over 1000 records. The memory consumption grows linearly and will consume all available JVM heap space, requiring a node restart.
  7. Check ACL permissions before using getRefRecord() in client-side scripts. If the user lacks read access to the referenced table, the method returns null even when the reference exists, breaking your logic silently.
  8. Use setLimit() when querying the returned GlideRecord further. Without limits, joins to large tables like sys_audit or sys_email can return 50,000+ records and exhaust server memory.

Side Effects & Platform Behavior

  • ACL table rules fire on the referenced table when getRefRecord() executes, potentially blocking access or triggering security scripts
  • Database access entries get logged to syslog_transaction table with the query details, creating audit trails admins can track
  • Domain separation rules apply to the returned record—you might get null even if the reference exists but is in a different domain
  • Field-level read ACLs on the referenced table get evaluated, potentially masking sensitive field values with ***
  • Session timeout counters reset with each database query, potentially extending user sessions beyond configured limits
  • Database connection pooling gets impacted—each call consumes a connection from the pool until the script completes
  • Query performance gets recorded in sys_db_cache and stats.do performance monitoring
  • Client-side usage creates AJAX calls visible in browser Network tab, exposing table structure and field names
  • Memory cleanup doesn't happen until script execution completes—large datasets stay in memory throughout the entire transaction
  • Workflow activities and Business Rule executions on the referenced table won't fire—this is read-only access that bypasses normal record lifecycle events

Debugging When It Breaks

The most common failure is NullPointerException when trying to access properties on a null return value. Users see forms that don't save properly or scripts that appear to do nothing. In client-side code, you'll see "TypeError: Cannot read property 'xyz' of null" in the browser console. Server-side failures show up in System Log > All with stack traces pointing to your script line numbers.

Performance issues manifest as transaction timeouts after 30 seconds, showing "Transaction cancelled due to timeout" in the logs. Browser-based scripts freeze the UI with spinning loading icons that never complete. Check the Script Debugger in System Diagnostics > Session Debug > Debug Business Rule to see exactly which database queries are running and how long they take.

ACL-related failures return null records without obvious error messages. Look for "ACL Denied" entries in System Log > All, or check the Security Debug plugin output. Quick diagnostic checklist:

  • Verify the reference field contains a valid sys_id using getValue()
  • Check if the target record actually exists with a direct GlideRecord query
  • Test with an admin account to rule out ACL issues
  • Enable Database Debug logging to see the actual SQL queries being generated

Quick Reference

  • Always check nil() on reference fields before calling getRefRecord()
  • Returns null for empty references, not an empty GlideRecord object
  • Each call executes a database query—cache results in variables for multiple field access
  • Use getDisplayValue() for display-only values—it's cached and much faster
  • Client-side usage creates synchronous server calls that freeze the browser
  • ACL permissions apply—users without table access get null returns
  • Domain separation affects results—cross-domain references may return null
  • Never call update() on returned records from Business Rules—causes infinite recursion
  • Memory usage scales linearly with usage—avoid in high-volume processing without limits
  • Returned GlideRecord supports full querying with addQuery() and query() for related record traversal