What It Is

The update() method is the workhorse of server-side data manipulation in ServiceNow — it commits field changes made to an existing GlideRecord object back to the database. Without it, all your setValue() calls remain in memory and never persist. This isn't just a convenience method; it's the bridge between your script's intentions and actual data changes that users will see in the platform. Unlike direct database operations, update() triggers the full ServiceNow business logic stack — Business Rules, ACLs, workflow activities, and audit trails all fire as if a user made the change through the UI.

Architecturally, update() exists exclusively on the server side — you'll find it in Business Rules, Script Includes, Scheduled Jobs, and Fix Scripts, but never in Client Scripts or UI Policies. This separation isn't arbitrary; it's a security and consistency boundary that prevents client-side code from bypassing server-side validation and business logic. When you call update() in a Business Rule, you're operating within the same database transaction that triggered the rule, which means you can be certain your changes will either all succeed together or all fail together. This transactional context is critical for data integrity but also means that exceptions in your update() logic can roll back the entire operation.

Under the hood, ServiceNow translates your update() call into a SQL UPDATE statement, but not before running it through a complex pipeline of validation, transformation, and business logic execution. The platform maintains a dirty field list for each GlideRecord instance — only fields you've actually changed get included in the final database query, which is crucial for performance and audit accuracy. This dirty field tracking also drives the Business Rule engine; only changed fields trigger field-specific Business Rules, and the previous object in Business Rules contains the pre-update values specifically because of this change tracking mechanism. The method returns the sys_id of the updated record as a string, but more importantly, it updates the GlideRecord object in place with any values that were modified by Business Rules during the update process.

Without update(), server-side automation becomes nearly useless — you can read data, perform calculations, and make decisions, but you cannot persist any changes back to the database. This creates a hard dependency for any meaningful server-side customization: automated assignment logic, calculated field updates, status transitions, approval processing, and integration synchronization all require update() calls to function. Even simple tasks like setting a timestamp or incrementing a counter become impossible without this method. The platform provides no alternative for programmatic record updates — there's no bulk update API that bypasses update(), no direct SQL access, and no other GlideRecord method that persists changes.

Every ServiceNow role uses update() differently, but they all use it. System administrators rely on it in Fix Scripts for data cleanup and migration tasks, often updating hundreds or thousands of records in a single script execution. Developers integrate it into Business Rules for real-time automation, Script Includes for reusable business logic, and Scheduled Jobs for batch processing tasks. Enterprise architects design around its transactional behavior when building complex automation chains that span multiple tables and business processes. The method is also central to ServiceNow's integration patterns — whether you're synchronizing user data from Active Directory, updating asset information from a CMDB tool, or processing webhook payloads from external systems, update() is how you persist those external changes into ServiceNow.

The update() method sits at the center of a constellation of related concepts that define server-side data manipulation in ServiceNow. It pairs most commonly with setValue() calls that stage the field changes, but it also interacts closely with setWorkflow(false) for bypassing workflow execution and setUseEngines(false) for skipping Business Rules entirely. Understanding when to use these control methods alongside update() often makes the difference between automation that works reliably and automation that creates infinite loops or performance problems. The method also contrasts sharply with insert() — while both persist data, update() requires an existing record and sys_id, triggers different Business Rule operations, and returns the existing sys_id rather than generating a new one.

How It Works Under the Hood

When you call update() on a GlideRecord, ServiceNow doesn't immediately hit the database. Instead, it initiates a complex validation and transformation pipeline that can modify your data, reject the update entirely, or trigger cascading updates to other records. The platform first checks your ACL permissions for the table and specific fields you're trying to update — if you lack write access to even one modified field, the entire update fails with a security exception. This happens before any Business Rules execute, which means your 'before' Business Rules won't run if you don't have proper permissions. After ACL validation, ServiceNow examines the GlideRecord's dirty field list and builds a change set containing only the fields you've actually modified since the record was loaded or last updated.

The Business Rule engine executes next, running all 'before' Business Rules that match the update operation and any changed fields. These Business Rules have full access to modify the GlideRecord object, and their changes automatically become part of the same database transaction — no additional update() calls required. Business Rules can also halt the update entirely by calling setAbortAction(true), which stops processing before the database write occurs. After the database UPDATE statement executes successfully, 'after' Business Rules fire with access to the final field values, including any changes made by 'before' Business Rules or database triggers. This two-phase execution model ensures that validation and transformation logic runs before data persistence, while notification and integration logic runs after the data is safely committed.

The most critical aspect of update() execution that developers often miss is the automatic re-querying that happens after the database write completes. ServiceNow refreshes the GlideRecord object with the current database values, which means any changes made by database triggers, calculated fields, or 'before' Business Rules on other GlideRecord instances become visible in your original object. This refresh behavior is why you can call getValue() immediately after update() and see values that other Business Rules set, even if you didn't set them yourself. However, this refresh only includes fields that were part of the original query — if you need access to fields that weren't initially selected, you'll need to perform a fresh query after the update completes.

The Update Request Lifecycle

  1. ACL validation runs first — ServiceNow checks table and field-level write permissions for the current user context. Failure here throws a security exception and stops all processing.
  2. Dirty field analysis identifies exactly which fields have been modified since the record was loaded, building the minimal change set for database efficiency and audit accuracy.
  3. Before Business Rules execute in priority order, with full ability to modify field values, abort the operation, or trigger additional database operations within the same transaction.
  4. Database UPDATE statement executes with the final field values, including any modifications made by Business Rules. Database triggers and constraints also fire at this stage.
  5. GlideRecord refresh occurs automatically — the object is updated with current database values, making Business Rule changes visible to subsequent code.
  6. After Business Rules execute with access to the final committed values, typically handling notifications, integrations, and other post-update automation.
  7. Workflow activities process if workflows are enabled for the table, potentially triggering additional automation based on the record's new state.
  8. Audit trail entries are created for each changed field, and the method returns the sys_id string of the updated record to the calling code.
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 Auto-Assignment.js
// Business Rule: Before update on incident table
// Purpose: Auto-assign incidents based on category and location

(function executeRule(current, previous) {
    // Only process if assignment_group changed or is empty
    if (current.assignment_group.changes() || current.assignment_group.nil()) {
        
        // Get the appropriate assignment group based on business logic
        var assignmentGroup = getAssignmentGroup(current.category, current.location);
        
        if (assignmentGroup) {
            // Set the assignment group - this will be included in the same update
            current.assignment_group = assignmentGroup;
            
            // Clear assigned_to since group changed
            current.assigned_to = '';
            
            // Add work note explaining the auto-assignment
            current.work_notes = 'Auto-assigned to ' + current.assignment_group.getDisplayValue() + 
                               ' based on category: ' + current.category.getDisplayValue();
        }
    }
    
    // No explicit update() call needed - we're in a before Business Rule
    // Changes to current object will be included in the triggering update
})(current, previous);
Script Include — IncidentAssignmentUtils.js
// Script Include: Server-side utility for incident management
// Called from Business Rules, Scheduled Jobs, or other server-side scripts

var IncidentAssignmentUtils = Class.create();
IncidentAssignmentUtils.prototype = {
    
    // Update multiple incidents with new assignment logic
    // This method explicitly calls update() for each record
    massReassignIncidents: function(categoryFilter, newAssignmentGroup) {
        var gr = new GlideRecord('incident');
        gr.addQuery('category', categoryFilter);
        gr.addQuery('state', '!=', '6'); // Not resolved
        gr.query();
        
        var updateCount = 0;
        while (gr.next()) {
            // Make changes to the current record
            gr.setValue('assignment_group', newAssignmentGroup);
            gr.setValue('assigned_to', '');
            
            // Explicit update() call required in Script Include context
            // This triggers the full Business Rule chain for each record
            var sysId = gr.update();
            
            if (sysId) {
                updateCount++;
                gs.info('Updated incident: ' + gr.number + ' (' + sysId + ')');
            }
        }
        
        return updateCount;
    },
    
    type: 'IncidentAssignmentUtils'
};

Real-World Scenarios

Automated SLA Clock Management

A large enterprise needs to pause SLA timers when incidents are pending vendor response, then resume them when the vendor provides updates. The business rule must handle state transitions while maintaining accurate SLA calculations for reporting compliance.

Business Rule — SLA Clock Control.js
// Business Rule: Before update on incident table
// Condition: state changes OR vendor_status changes

(function executeRule(current, previous) {
    var needsSLAUpdate = false;
    
    // Check if we're moving to or from vendor pending state
    if (current.state == '10' && previous.state != '10') {
        // Moving to vendor pending - pause all active SLA tasks
        pauseActiveSLAs(current.sys_id, 'Vendor response required');
        needsSLAUpdate = true;
    } 
    else if (previous.state == '10' && current.state != '10') {
        // Moving from vendor pending - resume SLA tasks
        resumeActiveSLAs(current.sys_id, 'Vendor response received');
        needsSLAUpdate = true;
    }
    
    if (needsSLAUpdate) {
        // Log the SLA action for audit purposes
        current.work_notes = 'SLA timers ' + 
            (current.state == '10' ? 'paused' : 'resumed') + 
            ' due to vendor status change';
    }
})(current, previous);

Watch for infinite loops when Business Rules modify SLA-related fields — use condition scripts to prevent unnecessary rule execution. Also be aware that SLA engine calculations happen asynchronously, so immediate queries for SLA status may not reflect the changes you just made.

Multi-Table Asset Synchronization

When hardware assets are updated in the CMDB, related configuration items and user assignment records need to stay synchronized. This scheduled job processes nightly updates from an external asset management system and ensures data consistency across multiple ServiceNow tables.

Scheduled Job — Asset Sync.js
// Scheduled Job: Nightly asset synchronization
// Processes updates from external asset management system

// Query assets that have pending updates from integration
var assetGR = new GlideRecord('alm_asset');
assetGR.addQuery('u_sync_status', 'pending_update');
assetGR.query();

var processedCount = 0;
while (assetGR.next()) {
    // Update asset fields from staging table data
    var stagingData = getStagingData(assetGR.asset_tag);
    
    if (stagingData) {
        assetGR.setValue('assigned_to', stagingData.user_id);
        assetGR.setValue('location', stagingData.location_id);
        assetGR.setValue('u_cost_center', stagingData.cost_center);
        assetGR.setValue('u_sync_status', 'synchronized');
        assetGR.setValue('u_last_sync', new GlideDateTime());
        
        // Critical: use setWorkflow(false) to prevent workflow loops
        assetGR.setWorkflow(false);
        
        // Update() call persists all changes and triggers Business Rules
        var updatedId = assetGR.update();
        if (updatedId) processedCount++;
    }
}

gs.info('Asset sync completed: ' + processedCount + ' records updated');

Always use setWorkflow(false) in bulk operations to prevent workflow activities from creating performance bottlenecks. Consider using setUseEngines(false) if you need to bypass Business Rules entirely, but document this decision carefully since it can break dependent automation.

Service Request Approval Chain Processing

A complex service request requires multiple levels of approval based on cost and department. When approvals are completed, the system must update request status, notify stakeholders, and trigger fulfillment processes in the correct sequence.

Script Include — ApprovalProcessor.js
// Script Include: Handles complex approval workflow logic
// Called from Business Rules on sysapproval_approver table

processApprovalComplete: function(approverRecord) {
    // Get the related service request
    var requestGR = new GlideRecord('sc_request');
    if (!requestGR.get(approverRecord.document_id)) {
        gs.error('Cannot find request: ' + approverRecord.document_id);
        return false;
    }
    
    // Check if all required approvals are complete
    var pendingApprovals = this.getPendingApprovalCount(requestGR.sys_id);
    
    if (pendingApprovals == 0) {
        // All approvals complete - update request status
        requestGR.setValue('approval', 'approved');
        requestGR.setValue('request_state', '3'); // Approved state
        requestGR.setValue('u_approval_completed', new GlideDateTime());
        
        // Add journal entry for audit trail
        requestGR.setValue('comments', 'All required approvals received - proceeding to fulfillment');
        
        // Update with full Business Rule processing for downstream automation
        var updateResult = requestGR.update();
        
        if (updateResult) {
            // Trigger fulfillment process after successful status update
            this.triggerFulfillment(requestGR.sys_id);
            return true;
        }
    }
    
    return false;
}

Approval processing is particularly prone to race conditions when multiple approvers respond simultaneously. Use database-level locking or check approval counts immediately before the update to prevent inconsistent state transitions.

The Classic Mistake

⚠️

Calling update() inside a Business Rule that triggers on the same table creates an infinite loop.

Anti-pattern — Do Not Use This.js
// Business Rule on incident table, Before/After update
(function executeRule(current, previous) {
    
    // Auto-assign high priority incidents
    if (current.priority == '1' && current.assigned_to.nil()) {
        var gr = new GlideRecord('incident');
        gr.get(current.sys_id);
        gr.assigned_to = 'admin';
        gr.update(); // INFINITE LOOP!
        
        gs.info('Auto-assigned incident ' + current.number);
    }
    
})(current, previous);

This creates an infinite recursion because the update() call triggers the same Business Rule again. ServiceNow's recursion detection will eventually kick in after 16 iterations, throwing a "Maximum execution depth exceeded" error in the Application Logs. The incident gets saved in a corrupted state, often with duplicate audit entries and incomplete field updates. You'll see "Recursive Business Rule detected" warnings flooding the System Log, and users will get generic "An error has occurred" messages.

The Fix.js
// Business Rule on incident table, Before update
(function executeRule(current, previous) {
    
    // Auto-assign high priority incidents
    if (current.priority == '1' && current.assigned_to.nil()) {
        // Modify current record directly - no update() needed
        current.assigned_to = 'admin';
        current.work_notes = 'Auto-assigned due to P1 priority';
        
        gs.info('Auto-assigned incident ' + current.number);
    }
    
})(current, previous);
💡

In Business Rules, modify the current object directly. Only use update() when you need to modify a different record than the one triggering the rule.

Performance Rules

  1. Never call update() inside loops that process more than 50 records. Each call triggers all Business Rules, ACLs, and notifications for that table. Above 50 updates, you'll hit transaction timeouts and lock the target table for other users.
  2. Use updateMultiple() instead of looping update() calls when updating the same field across multiple records. updateMultiple() bypasses Business Rules and executes as a single database transaction, reducing execution time by 80% or more.
  3. Avoid calling update() on tables with complex Business Rule chains (incident, change_request, sc_req_item). Each update can trigger 15-20 Business Rules in sequence. In Client Scripts, this causes browser freezing and 30+ second page load times.
  4. Set setWorkflow(false) before update() when making administrative updates that shouldn't trigger approval workflows. Workflow execution adds 2-5 seconds per record and can exhaust the workflow engine's thread pool during bulk operations.
  5. Use autoSysFields(false) when you need to preserve original sys_updated_on timestamps during data migrations. Without this, every update() overwrites audit trails and breaks SLA calculations that depend on original creation dates.
  6. Never call update() inside onChange Client Scripts without user confirmation. Each keystroke can trigger a server roundtrip, creating dozens of update transactions and making forms unusable. Users will complain about "laggy" forms and data loss when they type quickly.
  7. Check isValidRecord() before calling update() when the GlideRecord might be empty. Invalid records cause silent failures where the script continues executing but no database changes occur, leading to data inconsistency bugs that take weeks to discover.
  8. Wrap update() calls in try-catch blocks when updating records that might be locked by other transactions. Concurrent update conflicts throw exceptions that crash Scheduled Jobs and Background Scripts, requiring manual intervention to resume processing.

Side Effects & Platform Behavior

  • Triggers all Business Rules with operation 'update' on the target table, including Before, After, and Async rules that may modify other records or send notifications
  • Executes ACL rules for each field being updated, potentially blocking the operation or modifying field values based on user roles and conditions
  • Creates audit history entries in sys_audit table for each changed field when table auditing is enabled, consuming significant database storage on high-volume tables
  • Updates sys_updated_on, sys_updated_by, and sys_mod_count system fields automatically unless autoSysFields(false) is set
  • Triggers workflow transitions and approval processes when setWorkflow(true) is active, potentially changing record state beyond your intended field updates
  • Sends email notifications configured in Notification rules that match the update conditions, even for administrative script updates
  • Creates entries in sys_journal_field table when updating journal fields like work_notes or comments, visible in the record's Activity stream
  • Fails silently when called on records the current user lacks write access to, returning false but not throwing an exception or logging an error
  • Invalidates cached data in related lists and reference field displays, forcing expensive re-queries when users next view related records
  • Breaks when called inside Display Business Rules or other read-only contexts, throwing "Cannot update record in display mode" errors

Debugging When It Breaks

The most common failure is the silent failure where update() returns false and no changes are saved. Users report that their scripts "aren't working" but no errors appear. This happens when ACL restrictions block the update, when the record is locked by another transaction, or when a Business Rule prevents the save. The developer sees no exception in their script, making this particularly frustrating to diagnose. Client-side, you'll see the form remain unchanged after the script runs, while server-side scripts simply continue executing with the unchanged record.

When recursion occurs, users see "An error has occurred" messages in the browser, and the target record may be left in an inconsistent state with partial field updates. Check System Log > All for "Maximum execution depth exceeded" or "Recursive Business Rule detected" messages. The Application Log under System Logs > Application Logs will show the complete stack trace with the specific Business Rule causing the loop. Script Debugger (when enabled) will show exactly which update() call triggered the cascade.

Performance issues manifest as browser timeouts on forms, Background Script timeouts, or user complaints about "slow" applications. Look for transaction timeouts in the Application Logs and check the System Diagnostics > Stats for unusual database wait times. Quick diagnostic checklist:

  • Check if update() returns false - indicates ACL or Business Rule blocking the save
  • Verify the GlideRecord contains a valid record with isValidRecord()
  • Look for "Business Rule aborted" messages indicating a rule called setAbortAction(true)
  • Check System Definition > Business Rules for rules on your target table with Update operations
  • Test with setWorkflow(false) to isolate workflow-related failures
  • Enable Debug logging for the specific table to see detailed Business Rule execution

Quick Reference

  • Returns sys_id string on success, false on failure - always check the return value for error handling
  • Use setLimit(1) before queries when you only need to update one record - prevents accidentally updating multiple records with loose queries
  • Call setWorkflow(false) before update() for administrative updates to prevent unwanted approvals and state changes
  • Never modify sys_id field before calling update() - ServiceNow uses this to locate the record and changing it creates a new record instead
  • Pass a reason string like update('Automated cleanup') to create audit trail entries explaining the change
  • Business Rules with 'update' operation fire even for setDisplayValue() changes - not just direct field assignments
  • Use changes() method to check if any fields actually changed before calling update() - prevents unnecessary Business Rule execution
  • Client-side GlideRecord.update() requires a callback function and executes asynchronously - never assume immediate completion
  • Empty string values ('') clear field contents, while nil() method clears reference fields - use the right method for the field type
  • Scoped applications require explicit table access permissions in Application Cross-Scope Privileges to call update() on tables outside their scope