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:
currentcontains the record's current database values as they will be displayed on the formpreviousis 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 types — Before, After, Async, Display explained
- Debugging ServiceNow scripts — diagnosing Business Rule issues
- Flow Designer vs Business Rules — when to move async logic to flows
- Script Includes — extracting Business Rule logic into reusable libraries