current vs previous in ServiceNow Business Rules: The Complete Reference

current and previous are the two most-used objects in every Business Rule, and one of the most reliably misunderstood areas of ServiceNow scripting. Most developers have a working mental model — but with gaps that lead to subtle bugs: logic that fires on inserts when it should only fire on updates, Async rules that use previous values that are no longer reliable, or code that checks a field comparison that never evaluates correctly because the field type requires a different comparison method. This guide covers everything: what each object contains in every rule type, the async previous problem, changes() vs getOldValue() vs direct comparison, operation(), and the patterns that avoid the common mistakes.

What is current?

In any Business Rule, current is a GlideRecord object representing the record being processed by the rule. Its contents depend on the rule type and the operation:

  • Before rules — current contains the new values being saved. The record has not yet been written to the database. You can modify current's field values and they will be saved as part of the transaction.
  • After rules — current contains the values as they were saved to the database. The transaction is complete.
  • Async rules — current contains the values at the time the rule was queued — which may differ from the record's current database state by the time the rule actually executes.
  • Display rules — current contains the record's current database values, as they will be rendered in the form.

What is previous?

previous is a GlideRecord object containing the field values of the record as they were before the current operation. On an update, previous holds the pre-change state. On an insert, previous is an empty GlideRecord with no meaningful values.

// current = what is being saved
// previous = what was in the database before this save

// Check if state changed in this update
if (current.state != previous.state) {
    gs.log('State changed from ' + previous.getValue('state') 
           + ' to ' + current.getValue('state'));
}

// Check if the record transitioned to a specific state
if (current.state == '6' && previous.state != '6') {
    // Record is being resolved for the first time in this save
    current.setValue('resolved_at', new GlideDateTime());
}

previous on insert operations

When a new record is being created (insert operation), previous is an empty GlideRecord. All fields return empty or null values. This is the most common source of Business Rule bugs when a rule is configured to fire on both insert and update.

// Bug — previous.state is empty on insert, comparison is always true or always false
if (current.state != previous.state) {
    // On insert: previous.state is empty string '', current.state is '1' (New)
    // This condition is ALWAYS true on insert — even if you don't want it to be
}

// Fix — always check operation() before using previous
if (current.operation() == 'update' && current.state != previous.state) {
    // Only runs on updates where state actually changed
}

// Alternative — scope the rule to Update only
// Check only 'Update' checkbox on the Business Rule record
// Then previous is always meaningful

The operation() method

current.operation() returns the DML operation that triggered the rule: 'insert', 'update', or 'delete'. Use it whenever your rule logic depends on which operation occurred.

var op = current.operation();

if (op == 'insert') {
    // New record being created
    // previous has no meaningful values
    setupNewRecord(current);
} else if (op == 'update') {
    // Existing record being modified
    // previous has pre-change values
    handleUpdate(current, previous);
} else if (op == 'delete') {
    // Record being deleted
    // current has the record values at time of deletion
    logDeletion(current);
}

For Display Business Rules, current.operation() returns 'query'.

Checking if a field changed — three methods

There are three ways to check if a specific field changed in an update. Each has different strengths.

Method 1 — Direct comparison

// Compare current and previous values directly
if (current.state != previous.state) {
    // State changed in this update
}

// Warning: this is string comparison, not semantic comparison
// Works for most simple fields (String, Integer, Choice)
// Be careful with reference fields — comparing reference fields
// compares the underlying sys_ids

Method 2 — changes() method

// GlideElement changes() — returns true if this field changed in this operation
if (current.state.changes()) {
    // State field was modified
}

// More semantically clear than direct comparison
// Works correctly for all field types including references and booleans
// Only works in Before and After rules — unreliable in Async rules

// Check what value it changed to AND from
if (current.assignment_group.changes()) {
    gs.log('Group changed from: ' + previous.assignment_group.getDisplayValue()
          + ' to: ' + current.assignment_group.getDisplayValue());
}

Method 3 — getOldValue()

// Get the previous value of a specific field
// Equivalent to previous.getValue('field_name') but more explicit
var oldState = current.state.getOldValue(); // Returns previous value as string
var oldGroup = current.assignment_group.getOldValue(); // Returns previous sys_id

// Useful when you need the old value but don't want to rely on previous object
if (current.getValue('state') == '6' && current.state.getOldValue() != '6') {
    // Transitioning to resolved
}

In practice: use changes() to check if a field changed, use direct comparison or getOldValue() to compare specific values. Changes() is the most readable and works correctly across all field types.

The async previous problem — the most important warning

This is the subtlest and most consequential gotcha with Business Rules. In an Async rule, previous is not guaranteed to contain the pre-change values.

Here is why: Async rules do not run immediately. They are queued and run by the ServiceNow scheduler, typically within seconds but potentially minutes after the triggering save. In a busy instance with heavy automation activity, there may be a queue. By the time your Async rule runs, the record may have been updated again — changing the values that previous would reflect.

Specifically: the previous object in an Async rule contains the values from the record at queue time, but those values may have already been overwritten by subsequent updates before the rule executed. For logic that depends critically on the pre-change state, this can cause incorrect behaviour that is extremely difficult to reproduce and debug.

// UNSAFE in an Async rule — previous may not reflect the expected pre-change state
(function executeRule(current, previous) {
    // If the record was updated again before this async rule ran,
    // previous.state may not be the value from the triggering update
    if (current.state == '6' && previous.state != '6') {
        // This may fire incorrectly or not fire when it should
        sendResolutionSurvey(current);
    }
})(current, previous);

The solutions:

  • Capture what you need in a Before or After rule — store the pre-change value in a custom field on the record or in a separate tracking table, then read that stored value in the Async rule
  • Use current.operation() instead of previous comparisons — if you only need to know the operation type, that is reliable in Async rules
  • Use Flow Designer for async automation that depends on state changes — Flow Designer captures trigger context reliably and is not subject to the same timing issues
  • For state-change detection in async processing, query the record fresh and apply your own change-detection logic

Reference field comparisons

Reference fields store sys_ids internally. When comparing reference fields, you are comparing the referenced record's sys_id — not its display value.

// Comparing reference fields — compares underlying sys_ids
if (current.assigned_to != previous.assigned_to) {
    // Assignment changed — comparing sys_ids
    gs.log('Assignment changed from ' + previous.assigned_to.getDisplayValue()
          + ' to ' + current.assigned_to.getDisplayValue());
}

// Using getValue() for reference fields — also returns sys_id
if (current.getValue('assigned_to') != previous.getValue('assigned_to')) {
    // Same result
}

// getDisplayValue() for human-readable comparison
if (current.assigned_to.getDisplayValue() != previous.assigned_to.getDisplayValue()) {
    // Also works — but you probably want the sys_id comparison for scripts
}

Boolean field comparisons

Boolean fields store 'true' or 'false' as strings internally. Direct comparison works but be aware of the string representation:

// Boolean field comparison
if (current.active == 'true') { /* record is active */ }
if (current.active == 'false') { /* record is inactive */ }

// Or use changes()
if (current.active.changes()) {
    if (current.active == 'true') {
        // Record was just activated
    } else {
        // Record was just deactivated
    }
}

current and previous in Display rules

Display rules fire on form load (query operation). In a Display rule:

  • current contains the record's current database values as they will be displayed on the form
  • previous is not meaningful — the record has not been modified
  • Do not attempt to modify current in a Display rule — use it only for reading values and populating g_scratchpad

Practical patterns

Pattern — State transition detection (safe for all rule types)

// Works in Before and After rules
// For Async — see warning above
(function executeRule(current, previous) {
    // Check if this is an update (not insert) first
    if (current.operation() != 'update') return;

    // Use changes() for clean field change detection
    if (!current.state.changes()) return;

    var newState = current.getValue('state');
    var oldState = current.state.getOldValue();

    gs.log('State transition: ' + oldState + ' → ' + newState, 'StateTransitionBR');

    // Handle specific transitions
    if (newState == '6') {
        // Transitioning to Resolved
        handleResolution(current, oldState);
    } else if (newState == '2' && oldState == '1') {
        // New → In Progress
        handleAssignment(current);
    }
})(current, previous);

Pattern — Safe async rule with state check

// Async rule that queries fresh values rather than relying on previous
(function executeRule(current, previous) {
    // Query the record fresh to get reliable current state
    var gr = new GlideRecord('incident');
    if (!gr.get(current.sys_id)) {
        gs.warn('Record not found in async rule: ' + current.sys_id, 'AsyncBR');
        return;
    }

    // Apply logic based on current database state, not previous
    if (gr.getValue('state') == '6' && gr.getValue('u_survey_sent') != 'true') {
        sendSurvey(gr);
        gr.setValue('u_survey_sent', 'true');
        gr.setWorkflow(false);
        gr.update();
    }
})(current, previous);

Related guides:

Business Rule series: Business Rule types — Before, After, Async, Display · Flow Designer vs Business Rules — choosing the right tool · Debugging scripts — diagnosing rule execution issues · Script Includes — extracting reusable logic out of rules · GlideRecord performance — writing efficient queries inside rules · Stop using toString() — field reading in rules

50 developer patterns in one guide

The NowSpectrum Pro Tips and Tricks guide covers 50 battle-tested patterns including Business Rules, current/previous, GlideRecord, and scripting best practices.

Get the Pro Tips Guide →
← Back to all posts