What It Is

getValue() is the GlideRecord method that retrieves a field's raw database value as a string, solving the fundamental problem of data integrity when working with ServiceNow's dual-value field system. While ServiceNow stores internal values in the database (sys_ids for references, actual boolean values, raw dates), the platform also maintains display representations for human consumption. This method cuts through ServiceNow's display layer to access the actual stored data, which is critical for reliable scripting, integrations, and business logic that cannot afford to be corrupted by localization, user preferences, or display formatting.

Architecturally, getValue() operates exclusively on the server side within the Rhino JavaScript engine, where GlideRecord objects live and database connections exist. It cannot and does not execute in browsers—any client-side script that appears to use getValue() is actually triggering a server round-trip through GlideAjax, Script Includes, or similar mechanisms. This server-only execution gives it direct access to the database layer and ServiceNow's internal field processing, but also means every call carries the overhead of server-side execution context.

Under the hood, ServiceNow implements getValue() as a direct interface to the GlideElement class hierarchy, where each field type has specific logic for converting its internal representation to a string. Reference fields return their 32-character sys_id, date/time fields return ISO-formatted strings in the system timezone, choice fields return their internal choice values (not labels), and encrypted fields may return obfuscated values depending on security context. The method bypasses all display business rules, UI policies, and user-specific formatting preferences that would normally influence what you see on forms.

Without getValue(), you cannot reliably compare reference fields, build accurate integrations, or ensure data consistency across different user sessions and locales. Direct field access (gr.assigned_to) returns GlideElement objects that automatically convert to display values when used in string contexts, leading to fragile code that breaks when users have different language preferences or when display business rules change. Any script that queries reference fields, builds REST API responses, or performs field comparisons needs the internal value to function correctly across all environments and user contexts.

Developers use getValue() constantly in business rules, script includes, and background scripts for data manipulation and integration work. Architects rely on it when designing API contracts and data synchronization patterns that must remain stable regardless of instance configuration changes. System administrators need it less frequently, but encounter it when debugging workflows, building reports with accurate reference field data, or troubleshooting integration issues where display values have corrupted business logic.

getValue() forms a critical triad with getDisplayValue() and setValue() in ServiceNow's field manipulation arsenal. While getDisplayValue() retrieves human-readable representations and setValue() accepts either internal or display values for field updates, getValue() provides the foundation for data-driven logic that must remain consistent. It also relates closely to GlideElement's implicit string conversion behavior, though accessing that through direct field references creates the display value problems that explicit getValue() calls avoid.

How It Works Under the Hood

When you call getValue() on a GlideRecord field, ServiceNow's Rhino JavaScript engine delegates the call to the appropriate GlideElement subclass based on the field's data type. The platform maintains a registry of field type handlers—GlideElementReference for reference fields, GlideElementBoolean for boolean fields, GlideElementGlideDateTime for datetime fields—each with specialized logic for converting internal database values to their string representations. This type-specific processing ensures that complex field types like encrypted fields, currencies, and journals return meaningful string values rather than raw binary data or internal identifiers.

The method execution bypasses ServiceNow's display layer entirely, including any display business rules, UI policies, client scripts, or formatting rules that would normally modify field presentation. This direct database-to-string conversion happens within the same transaction context as your script, meaning the returned value reflects the current state of the record without requiring additional database queries. For reference fields specifically, ServiceNow retrieves the sys_id from the foreign key column without performing a join to fetch display values from the referenced table, making getValue() operations significantly faster than their getDisplayValue() counterparts.

Critically, the string conversion process respects the system's timezone and locale settings at the server level, not individual user preferences. Date and datetime fields return values in the instance's default timezone with ISO 8601 formatting, while numeric fields use period decimal separators regardless of user locale. This server-side standardization makes getValue() output predictable and suitable for programmatic consumption, but means the returned values may not match what users see in forms if their personal settings differ from system defaults.

The Request Lifecycle

  1. JavaScript engine receives the getValue('field_name') method call on a GlideRecord instance
  2. GlideRecord validates the field name exists in the table schema and retrieves the field's data dictionary definition
  3. Platform instantiates the appropriate GlideElement subclass based on field type (reference, choice, datetime, etc.)
  4. GlideElement retrieves the raw database value from the current record's field buffer in memory
  5. Type-specific conversion logic processes the internal value (sys_id extraction for references, timezone conversion for dates)
  6. Method returns the processed value as a JavaScript string to the calling script context
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 scripts cannot call getValue() directly
    // Must use server round-trip for internal values
    if (isLoading || newValue === '') return;
    
    var ga = new GlideAjax('IncidentUtils');
    ga.addParam('sysparm_name', 'getAssignmentGroupManager');
    ga.addParam('sysparm_group_id', newValue); // This is the sys_id from client
    
    ga.getXMLAnswer(function(response) {
        var manager_id = response; // Server returns internal sys_id value
        
        // Set the manager field using internal value for data integrity
        if (manager_id) {
            g_form.setValue('u_group_manager', manager_id);
        }
    });
}
Script Include — IncidentUtils.js
var IncidentUtils = Class.create();
IncidentUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    getAssignmentGroupManager: function() {
        var groupId = this.getParameter('sysparm_group_id');
        if (!groupId) return '';
        
        var groupGR = new GlideRecord('sys_user_group');
        if (!groupGR.get(groupId)) return '';
        
        // getValue() returns the sys_id, not display name
        // Critical for setting reference fields correctly
        var managerId = groupGR.getValue('manager');
        
        // Always return internal values from server-side Ajax calls
        // Client expects sys_id, not "John Smith"
        return managerId || '';
    },
    
    type: 'IncidentUtils'
});

Real-World Scenarios

Assignment Group Escalation Logic

A business rule needs to escalate incidents to the next level assignment group when SLA thresholds are breached. The rule must compare current assignment group against a predefined escalation hierarchy without being affected by group name changes or user display preferences.

Business Rule — SLA Escalation.js
// Get current assignment group sys_id for reliable comparison
var currentGroupId = current.getValue('assignment_group');
if (!currentGroupId) return; // Cannot escalate without current group

// Build escalation mapping using sys_ids, never display names
var escalationMap = {
    'a1b2c3d4e5f6789012345678901234567': '9876543210fedcba098765432109876543', // L1 to L2
    '9876543210fedcba098765432109876543': 'fedcba0987654321fedcba0987654321ab', // L2 to L3
};

// Look up next level group ID from our reliable mapping
var nextGroupId = escalationMap[currentGroupId];
if (nextGroupId) {
    // Set using sys_id to ensure assignment succeeds regardless of group renames
    current.setValue('assignment_group', nextGroupId);
    current.setValue('assigned_to', ''); // Clear individual assignment
    
    gs.addInfoMessage('Incident escalated to next level support group');
}
Script Include — EscalationUtils.js
var EscalationUtils = Class.create();
EscalationUtils.prototype = {
    
    getEscalationChain: function(currentGroupId) {
        var chain = [];
        var groupGR = new GlideRecord('sys_user_group');
        
        // Build complete escalation path using sys_ids
        groupGR.addQuery('sys_id', currentGroupId);
        groupGR.query();
        
        while (groupGR.next()) {
            chain.push({
                id: groupGR.getValue('sys_id'),    // Internal value for updates
                name: groupGR.getDisplayValue('name'), // Display for logging
                parent: groupGR.getValue('parent')     // sys_id for next lookup
            });
            
            var parentId = groupGR.getValue('parent');
            if (parentId) groupGR.get(parentId);
            else break;
        }
        return chain;
    }
};

Using getValue() for reference field comparisons ensures the logic works even when assignment group names change or when the business rule runs in different user contexts. Direct field access would fail if groups get renamed, breaking critical escalation workflows. Watch for edge cases where assignment groups are deleted—the sys_id comparison will fail gracefully while display value logic might throw exceptions.

REST API Data Export

A scripted REST API endpoint exports request item data to an external system that requires stable reference field identifiers. The external system stores ServiceNow sys_ids and cannot handle display value changes when user names or catalog item names are updated.

Scripted REST API — Data Export.js
// Export request items with stable reference field values
var requestItems = [];
var ritm = new GlideRecord('sc_req_item');
ritm.addQuery('request', request.pathParams.request_id);
ritm.addQuery('active', true);
ritm.query();

while (ritm.next()) {
    var item = {
        sys_id: ritm.getValue('sys_id'),
        number: ritm.getValue('number'),
        // Use getValue() for reference fields to maintain external system links
        requested_for: ritm.getValue('request.requested_for'),
        cat_item: ritm.getValue('cat_item'),
        assignment_group: ritm.getValue('assignment_group'),
        assigned_to: ritm.getValue('assigned_to'),
        
        // Include display values separately for human readability
        requested_for_name: ritm.getDisplayValue('request.requested_for'),
        state: ritm.getValue('state') // Choice fields return internal values
    };
    requestItems.push(item);
}
Transform Map — External System Sync.js
// Transform incoming external data using sys_id references
var ExternalSyncUtils = Class.create();
ExternalSyncUtils.prototype = {
    
    syncRequestItem: function(externalData) {
        var ritm = new GlideRecord('sc_req_item');
        if (ritm.get(externalData.servicenow_id)) {
            
            // Update assignment using sys_id from external system
            if (externalData.assigned_user_id) {
                // Validate the sys_id exists before setting
                var userGR = new GlideRecord('sys_user');
                if (userGR.get(externalData.assigned_user_id)) {
                    ritm.setValue('assigned_to', externalData.assigned_user_id);
                }
            }
            
            // getValue() ensures we log the actual database value being updated
            gs.info('Updated RITM {0} assigned_to from {1} to {2}',
                   ritm.getValue('number'), 
                   ritm.getValue('assigned_to'),
                   externalData.assigned_user_id);
        }
    }
};

REST APIs must use getValue() for reference fields to maintain referential integrity with external systems. Display values create fragile integrations that break when referenced records are updated. Be cautious with dot-walking (request.requested_for) in getValue() calls—if intermediate references are empty, the call returns empty strings rather than throwing errors.

Multi-Instance Data Migration

A background script migrates configuration item data between development and production instances where user names and reference field display values differ between environments. The script must preserve logical relationships using sys_ids while handling missing references gracefully.

Background Script — CI Migration.js
// Export CIs with all reference relationships preserved
var exportData = [];
var ci = new GlideRecord('cmdb_ci_server');
ci.addQuery('install_status', 1); // Only installed CIs
ci.query();

while (ci.next()) {
    var record = {
        name: ci.getValue('name'),
        // getValue() captures exact reference field values for recreation
        owned_by: ci.getValue('owned_by'),
        managed_by: ci.getValue('managed_by'),
        support_group: ci.getValue('support_group'),
        location: ci.getValue('location'),
        
        // Include display values for manual verification if needed
        owned_by_display: ci.getDisplayValue('owned_by'),
        location_display: ci.getDisplayValue('location'),
        
        // Choice and string fields work reliably with getValue()
        operational_status: ci.getValue('operational_status'),
        environment: ci.getValue('environment')
    };
    
    exportData.push(record);
    gs.print('Exported CI: ' + record.name + ' (' + ci.getValue('sys_id') + ')');
}
Background Script — CI Import Validation.js
// Import script validates references before creating records
var CIMigrationUtils = Class.create();
CIMigrationUtils.prototype = {
    
    validateAndImport: function(importRecord) {
        var ci = new GlideRecord('cmdb_ci_server');
        ci.initialize();
        
        // Set simple fields using getValue() equivalents
        ci.setValue('name', importRecord.name);
        ci.setValue('operational_status', importRecord.operational_status);
        
        // Validate reference field sys_ids exist in target instance
        if (importRecord.owned_by) {
            var user = new GlideRecord('sys_user');
            if (user.get(importRecord.owned_by)) {
                ci.setValue('owned_by', importRecord.owned_by);
            } else {
                gs.warn('User not found: {0} ({1})', 
                       importRecord.owned_by_display, importRecord.owned_by);
            }
        }
        
        var newId = ci.insert();
        return newId;
    }
};

Data migration scripts depend on getValue() to capture portable reference relationships that work across instances with different user populations and organizational structures. Display value-based migrations fail catastrophically when target instances have differently named users or groups. Always validate that reference sys_ids exist in the target instance before attempting setValue() operations—invalid sys_ids will be silently ignored, leaving reference fields empty.

The Classic Mistake

⚠️

Using getValue() on Reference fields to compare display names instead of sys_ids, causing lookup failures.

Anti-pattern — Do Not Use This.js
// Business Rule on incident table
(function executeRule(current, previous) {
    // Developer thinks this compares user names
    var currentAssignee = current.assigned_to.getValue();
    var previousAssignee = previous.assigned_to.getValue();
    
    // This breaks - comparing sys_ids to display names
    if (currentAssignee != 'John Smith' && previousAssignee == 'John Smith') {
        // Send notification when John Smith is unassigned
        gs.addInfoMessage('Assignment changed from John Smith');
        
        // This query will never find records
        var gr = new GlideRecord('sys_user');
        gr.addQuery('name', currentAssignee); // Querying name field with sys_id value
        gr.query();
        if (gr.next()) {
            gs.log('New assignee: ' + gr.name);
        }
    }
})(current, previous);

This fails because getValue() on Reference fields returns the sys_id (like '62826bf03710200044e0bfc8bcbe5df1'), never the display name. The browser console shows no errors, but the condition never evaluates true and the query returns zero results. ServiceNow internally stores only sys_ids in Reference fields - display names are resolved at render time through table joins. When you compare a sys_id against 'John Smith', JavaScript performs a string comparison that always fails silently.

The Fix.js
// Business Rule on incident table
(function executeRule(current, previous) {
    // Use getDisplayValue() for human-readable comparisons
    var currentAssignee = current.assigned_to.getDisplayValue();
    var previousAssignee = previous.assigned_to.getDisplayValue();
    
    if (currentAssignee != 'John Smith' && previousAssignee == 'John Smith') {
        gs.addInfoMessage('Assignment changed from John Smith');
        
        // Or use getValue() to get sys_id for queries
        var assigneeSysId = current.assigned_to.getValue();
        if (assigneeSysId) {
            var gr = new GlideRecord('sys_user');
            gr.get(assigneeSysId); // Direct sys_id lookup - much faster
            gs.log('New assignee: ' + gr.name);
        }
    }
})(current, previous);
💡

getValue() for sys_ids and database operations, getDisplayValue() for human comparisons and UI display.

Performance Rules

  1. Never call getValue() inside loops over more than 100 records - each call triggers field access overhead that compounds to 5-10 second page load times and browser timeouts.
  2. Use direct field access (gr.field_name) instead of getValue('field_name') when the field name is known at development time - eliminates string lookup overhead that adds 200-300ms per 1000 field reads.
  3. Cache getValue() results in variables when the same field is accessed more than 3 times in a single script - repeated calls on Reference fields trigger redundant database lookups causing 30+ second Business Rule execution times.
  4. Avoid getValue() on Journal fields in client scripts - these fields can contain 50KB+ of data that causes browser memory spikes and 'script unresponsive' dialogs when loaded on form initialization.
  5. Replace getValue() with getUniqueValue() when building REST API responses that include sys_ids - getValue() forces string conversion overhead that adds 2-3 seconds to responses containing 500+ records.
  6. Never call getValue() on encrypted fields in scheduled jobs - decryption operations are CPU-intensive and cause job queue backups when processing more than 50 records with encrypted data per execution.
  7. Implement null checks before getValue() calls on optional Reference fields - calling getValue() on empty References generates database queries for non-existent records, adding 100-200ms overhead per empty field in production.

Side Effects & Platform Behavior

  • Field access through getValue() triggers ACL evaluation on the target field, writing Access entries to sys_security_log when Security Debug is enabled.
  • Reading Reference fields via getValue() updates the sys_glide_object_log table with field access statistics used by Performance Analytics for database optimization reports.
  • Client-side getValue() calls on dirty fields force immediate AJAX synchronization with the server, visible in browser Network tab as POST requests to /api/now/ui/form endpoints.
  • Accessing encrypted fields through getValue() creates audit records in sys_security_log with operation type 'field_access' and includes the accessing user's session ID for compliance tracking.
  • Using getValue() in Before Business Rules on Reference fields can trigger recursive rule execution if the referenced record has Display Rules that modify the current record, causing stack overflow errors.
  • Field reads via getValue() increment the read_count metric in sys_db_cache_stats, affecting database cache optimization algorithms and potentially causing cache evictions for frequently-accessed fields.
  • Calling getValue() on Journal fields triggers Journal Entry Business Rules on the sys_journal_entry table, even for read operations, due to internal journal aggregation processes.
  • In scoped applications, getValue() calls on cross-scope Reference fields write scope boundary violations to sys_scope_privilege_log when the target table lacks proper cross-scope access configuration.
  • Server-side getValue() operations bypass client-side field validation rules but still trigger server-side Data Policies, creating inconsistent validation behavior between form submission and programmatic access.
  • Reading fields with active Transform Maps via getValue() during import operations can trigger transformation scripts prematurely, writing 'premature_transform' warnings to sys_import_log.

Debugging When It Breaks

The most common failure symptoms include silent null returns when you expect values, sys_ids appearing in UI fields where you expected display names, and 'Cannot read property of undefined' errors in client scripts. Users typically report seeing technical values (like sys_ids or internal codes) instead of meaningful text, or form fields that appear empty despite having data. In Business Rules, you'll see logic branches that never execute because Reference field comparisons fail silently.

Check the browser's JavaScript console (F12 → Console) for client-side issues - look for 'TypeError: Cannot read property' messages when accessing nested Reference fields. For server-side problems, navigate to System Log → All and filter by your script name or source table. The Script Debugger (System Definition → Script Debugger) shows real-time getValue() return values and helps identify when fields return unexpected data types. Enable SQL logging under System Diagnostics → SQL to see if Reference field access triggers excessive database queries.

Watch for specific error patterns: 'NullPointerException' in server logs indicates calling getValue() on uninitialized GlideRecord objects. 'Invalid field name' warnings appear when field names contain typos or the field doesn't exist on the table. Client-side 'Synchronous XMLHttpRequest' warnings indicate getValue() calls forcing synchronous server communication. Performance logs show 'Long-running script' entries when field access becomes inefficient in loops.

Quick diagnostic checklist:

  • Verify the GlideRecord has next() or get() was called successfully before getValue()
  • Check field spelling in Dictionary (System Definition → Tables & Columns) - case sensitivity matters
  • Test if the field contains data using gr.field_name.nil() before accessing values
  • Confirm field-level Read ACLs allow access for the current user's role
  • For Reference fields, verify the referenced record exists and isn't deleted
  • Use typeof gr.field_name.getValue() to check if return value matches expected data type

Quick Reference

  • Reference field getValue() always returns 32-character sys_id strings, never display names or objects
  • DateTime fields return ISO format strings ('2024-01-15 14:30:00') regardless of user's timezone or display preferences
  • Boolean fields return string 'true' or 'false', not JavaScript boolean primitives - use == 'true' for comparisons
  • Empty fields return empty string (''), never null or undefined - use gr.field.nil() for proper null checking
  • Choice fields return internal values ('1', '2', '3') not choice labels - use getDisplayValue() for user-friendly labels
  • Client-side getValue() works only on loaded form fields - returns null for fields not visible on current form view
  • Journal fields via getValue() return complete journal history as concatenated string, potentially 50KB+ of data
  • Currency fields return unformatted decimal strings ('1234.56') without currency symbols or thousand separators
  • Encrypted fields require 'read_encrypter' role for getValue() to return decrypted values - otherwise returns '***' placeholder
  • Use direct field access (gr.number) instead of getValue('number') for 10-15% better performance in loops