CSA Hub Part 6 Business Rules
⚙️ Part 6 · Topic 2 of 4

Business Rules

⏱ ~75 min read · Part 6 — Self-Service & Automation · 20% Exam Weight
Heavily Tested: Business Rules are the most heavily tested automation topic on the CSA exam. Expect multiple questions on trigger timing (Before/After/Async/Display), the g_scratchpad pattern, setAbortAction(), and the differences between current and previous objects.

Business Rule Overview

Business Rules are server-side automation scripts that execute automatically when records are inserted, updated, deleted, or queried in ServiceNow. Unlike UI Policies (which run in the browser), Business Rules run on the server — they cannot be bypassed through the UI and apply to all access paths including REST APIs and integrations.

Key Facts

Stored In

The sys_business_rule table. Navigate via System Definition > Business Rules or type sys_business_rule.list in the filter navigator.

Runs Where

Entirely on the server. Has no access to the client-side DOM. Runs during the database transaction lifecycle.

Key API Objects

current — the record being acted on.
previous — field values before the change (available in Update BRs).
gs — GlideSystem utility methods.

Applies To

A single table. A Business Rule is scoped to one table but can query or modify any other table via GlideRecord.

Server-Side = Always Enforced: Because Business Rules execute on the server, they fire regardless of how data enters ServiceNow — whether through a form submission, REST API call, import set, or another script. This makes them the right choice for enforcing business logic and data integrity.

Business Rule vs. Client Script vs. UI Policy

Feature Business Rule Client Script UI Policy
Runs on Server Client (browser) Client (browser)
Can be bypassed No Yes (API, scripts) Yes (API, scripts)
Primary use Data validation, automation, integrity Dynamic form behavior Field visibility / mandatory
Database access Yes — full GlideRecord Limited (GlideAjax) No
Access current Yes — full field access + methods Yes — via g_form API Via conditions only

When Options (Trigger Timing) — CRITICAL EXAM TOPIC

The When field on a Business Rule controls at what point in the transaction lifecycle the script executes. This is the most heavily tested Business Rule concept on the CSA exam. Understand every option and its implications.

The Four When Options

1️⃣

Before

Runs before the database write occurs. Has full ability to modify current field values — changes are automatically saved as part of the transaction (no need to call current.update()).

Can abort: Yes — current.setAbortAction(true) prevents the write entirely.
sys_id available: No (record not yet inserted).

2️⃣

After

Runs after the database write completes. The record has been saved, so sys_id is available. To save further changes to the current record, you must call current.update() explicitly.

Can abort: No (record already written).
sys_id available: Yes.

3️⃣

Async

Runs in a separate background thread after the main transaction completes. Non-blocking — the user gets their response immediately. Ideal for slow or non-critical operations (email, REST callout, report generation).

Can abort: No — main transaction already finished.
sys_id available: Yes.

4️⃣

Display

Runs when a record is displayed (queried for viewing). Does not perform a database write. Primary purpose: populate the g_scratchpad object with server-side data for client scripts to consume.

Can abort: No.
DB write: None — read-only context.

Full Comparison Table

Aspect Before After Async Display
When it runs Before DB write After DB write After transaction, separate thread On record query/display
Modify current? Yes — auto-saved Yes — needs current.update() Yes — needs current.update() No DB write
Abort transaction? Yes No No No
sys_id on Insert? No Yes Yes Yes
Blocks user? Yes — synchronous Yes — synchronous No — background thread Yes — before form renders
Best for Validation, field defaulting, abort logic Trigger actions after save, create related records Slow ops: email, REST callouts, reports Populate g_scratchpad for client scripts
Example use case Prevent save if Priority = Critical and no Assignment Group Create a child task when a parent Incident is inserted Send notification email after incident update Pass server-computed value to client script
Exam Must-Know — When vs. When:
  • Before BR: modifying current saves automatically — do NOT call current.update() (causes infinite loop!)
  • After BR: must call current.update() to persist changes; has sys_id
  • Async BR: cannot affect the outcome of the original transaction; purely background
  • Display BR: only use is to populate g_scratchpad; never modifies the database

Operation Triggers

The Operation setting on a Business Rule determines which database event(s) cause the rule to fire. You can select one or more operations — multiple selections are OR'd together.

Insert

Fires when a new record is created and inserted into the table.

Example: Auto-assign a new incident to a default group on creation.

Update

Fires when an existing record is modified and saved.

Example: Validate that resolution notes exist before closing an incident.

Delete

Fires when a record is removed from the table.

Example: Prevent deletion of incidents still in "In Progress" state.

Query

Fires when a record is read/displayed. Used exclusively with the Display When setting.

Example: Populate g_scratchpad before form is rendered.

Combining Operations

It is common and valid to select Insert + Update together on a single Business Rule when you want the same logic to run on both create and modify. This avoids duplicating rules.

Insert + Update pattern: For field defaulting or validation that applies whether a record is new or being edited, select both Insert and Update. The current.isNewRecord() method returns true on Insert and false on Update — useful if you need slightly different logic for each case within the same rule.
// Example: Insert + Update BR — handle both cases
if (current.isNewRecord()) {
    // New record — set defaults
    current.priority = 3;
    current.state = 1;
} else {
    // Existing record being updated — validate
    if (current.state == 6 && current.close_notes == '') {
        current.setAbortAction(true);
        gs.addErrorMessage('Close notes are required when resolving an incident.');
    }
}

Business Rule Script — current & previous Objects

Inside a Business Rule script, two special GlideRecord objects are automatically available: current and previous. Knowing exactly what each provides — and when — is critical for the exam.

The current Object

The current object represents the record being processed. You can read and write its field values.

// Read a field value (returns GlideElement — use .toString() for string)
var state = current.state.toString();
var caller = current.caller_id.getDisplayValue();

// Write a field value (Before BR — auto-saved)
current.priority = 2;
current.assignment_group.setDisplayValue('Service Desk');

// Check if the current record is a new insert
if (current.isNewRecord()) {
    current.opened_by = gs.getUserID();
}

// Check if ANY field on the record changed (Update trigger)
if (current.changes()) {
    gs.log('Something changed on record: ' + current.sys_id);
}

// Abort the transaction (Before BR only)
current.setAbortAction(true);
gs.addErrorMessage('Your error message displayed to the user.');

The previous Object

The previous object holds the field values from before the update. It is only populated during Update operations — it is empty (null) on Insert.

// Read previous value of a field
var oldState = previous.state.toString();
var newState = current.state.toString();

gs.log('State changed from ' + oldState + ' to ' + newState);

// Check if a specific field changed
if (current.state.changes()) {
    // State field changed — run specific logic
    gs.log('Incident state was changed.');
}

// Check if a field changed TO a specific value
if (current.state.changesTo(6)) {
    // State just became "Resolved"
    current.resolved_at = new GlideDateTime();
}

// Check if a field changed FROM a specific value
if (current.state.changesFrom(1)) {
    // State was "New" and is now something else
    gs.log('Incident is no longer in New state.');
}

Field Change Methods — Quick Reference

Method Returns true when... Works on
current.changes() ANY field on the record changed Update operations
current.field.changes() That specific field changed value Update operations
current.field.changesTo('x') That field's new value equals 'x' Update operations
current.field.changesFrom('x') That field's old value was 'x' Update operations
current.isNewRecord() The record is being inserted (no sys_id yet) Insert operations
Exam Trap — changes() vs. field.changes():
  • current.changes() — true if any field changed (record-level)
  • current.state.changes() — true only if the state field specifically changed
  • These are different methods on different objects — know the distinction

GlideRecord in Business Rules

Business Rules have full access to the GlideRecord API, allowing them to query, create, update, and delete records in any table. This is what makes Business Rules powerful for complex automation.

Querying Other Records

// Basic query — iterate through all matching records
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);            // state = "New"
gr.addQuery('priority', 1);         // AND priority = "Critical"
gr.query();

while (gr.next()) {
    gs.log('Found incident: ' + gr.number);
}

// Get a single record by sys_id
var gr2 = new GlideRecord('sys_user');
gr2.get(current.caller_id);         // get() queries by sys_id
var callerName = gr2.name.toString();

// Get a single record by field value
var gr3 = new GlideRecord('sys_user_group');
gr3.get('name', 'Service Desk');    // get(field, value)
if (gr3.isValidRecord()) {
    current.assignment_group = gr3.sys_id;
}

Creating New Records

// Create a related task when an incident is resolved
var task = new GlideRecord('task');
task.initialize();                  // initialize() prepares a blank record
task.short_description = 'Follow-up for: ' + current.short_description;
task.assignment_group = current.assignment_group;
task.parent = current.sys_id;       // link to parent incident
var newSysId = task.insert();       // insert() saves and returns the new sys_id
gs.log('Created task: ' + newSysId);

Updating Records

// Update a specific record
var gr = new GlideRecord('incident');
if (gr.get('INC0001234')) {         // get by number field implicitly uses display value
    gr.setValue('state', 6);        // setValue(field, value)
    gr.setValue('close_notes', 'Auto-closed by business rule');
    gr.setWorkflow(false);          // prevent triggering workflows on this update
    gr.update();                    // must call update() to save
}

// Bulk update — use updateMultiple() on a query (no loop)
var grBulk = new GlideRecord('incident');
grBulk.addQuery('state', 1);
grBulk.addQuery('assigned_to', '');
grBulk.setValue('assignment_group.name', 'Service Desk');
grBulk.updateMultiple();            // updates all matching records at once

Deleting Records

// Delete a specific record
var gr = new GlideRecord('task');
gr.addQuery('parent', current.sys_id);
gr.addQuery('state', 1);  // only delete "New" child tasks
gr.query();
while (gr.next()) {
    gr.deleteRecord();
}
Performance Warning: Avoid running complex GlideRecord queries inside Before Business Rules. Every Before BR blocks the current user's transaction. Heavy queries cause noticeable form save delays. Move slow or non-critical operations to Async BRs wherever possible.

Conditions

A Business Rule's Condition is an additional filter that must pass before the script executes. This narrows when the BR fires beyond just the table and operation.

Two Condition Types

Filter Condition

Built using the condition builder UI (the standard filter row interface). Generates a GlideRecord-style condition evaluated against the current record.

Example: State is "Resolved" AND Priority is "1 - Critical"

Advanced Condition (Script)

A JavaScript expression in the Advanced field that must return true or false. Runs server-side with access to current, previous, and gs.

Example: current.priority == 1 && gs.hasRole('itil')

Both Must Pass: If both a Filter Condition and an Advanced Condition are set, both must evaluate to true for the Business Rule script to execute. They are AND'd together, not OR'd.
// Advanced Condition examples (these go in the Condition field, not the Script)

// Only fire if the caller_id field actually changed
current.caller_id.changes()

// Only fire if state is changing TO Resolved (6)
current.state.changesTo(6)

// Only fire for Critical priority incidents assigned to a specific group
current.priority == 1 && current.assignment_group.name == 'NOC Team'

// Only fire if the current user has the manager role
gs.hasRole('manager')

Order

When multiple Business Rules are configured on the same table with the same operation and When settings, they execute in ascending order — lower order number runs first.

Order Rules

  • Default order value: 100
  • Lower order = runs first (e.g., order 50 runs before order 100)
  • Order only matters for BRs with the same table, same operation, and same When timing
  • A Before BR with order 100 always runs before any After BR (When timing takes precedence over Order)
  • If two BRs have the same order, execution sequence is undefined — avoid duplicate order numbers
Best Practice: Use order increments of 10 or 25 (e.g., 10, 25, 50, 100, 200) to leave room for future BRs to be inserted between existing ones without renumbering everything.
Order Business Rule Name When Runs
50 Set Default Priority Before / Insert 1st
100 Validate Assignment Group Before / Insert+Update 2nd
200 Abort If Missing Data Before / Insert+Update 3rd
100 Create Child Task After / Insert Runs after all Before BRs

Display Business Rules & g_scratchpad

The Display Business Rule is a specialized type used exclusively to pass server-side data to client scripts. It runs at query time — just before a record's form is rendered — and populates the g_scratchpad object.

Why g_scratchpad Exists

Client scripts run in the browser and cannot directly call server-side GlideRecord queries. The g_scratchpad provides a bridge: a server-side Display BR populates it with data, which the client script then reads — all in one page load without a round-trip AJAX call.

Display BR + g_scratchpad Pattern:
  1. User navigates to a record form
  2. ServiceNow fires the Display Business Rule (When = Display, Operation = Query)
  3. The BR script populates g_scratchpad.myProperty = someValue
  4. The form renders; client scripts run and read g_scratchpad.myProperty

Display BR Script (Server Side)

// Display Business Rule — runs server-side when form is displayed
// When: Display | Operation: Query | Table: incident

// Pass caller's manager to the client
var caller = new GlideRecord('sys_user');
if (caller.get(current.caller_id)) {
    g_scratchpad.callerManager = caller.manager.getDisplayValue();
    g_scratchpad.callerDept    = caller.department.getDisplayValue();
    g_scratchpad.isVip         = (caller.u_vip_user == true);
}

// Pass a server-side computed value
var taskCount = new GlideAggregate('task');
taskCount.addQuery('parent', current.sys_id);
taskCount.addAggregate('COUNT');
taskCount.query();
taskCount.next();
g_scratchpad.openTaskCount = taskCount.getAggregate('COUNT');

Consuming g_scratchpad in a Client Script (Client Side)

// Client Script — onLoad
function onLoad() {
    // Read data populated by the Display Business Rule
    var manager   = g_scratchpad.callerManager;
    var dept      = g_scratchpad.callerDept;
    var isVip     = g_scratchpad.isVip;
    var taskCount = g_scratchpad.openTaskCount;

    if (isVip) {
        g_form.addInfoMessage('VIP Caller — Priority review required.');
        g_form.setValue('priority', 1);
    }

    if (taskCount > 5) {
        g_form.addInfoMessage('Warning: ' + taskCount + ' open tasks linked to this incident.');
    }
}

Display BR vs. GlideAjax

Aspect Display BR + g_scratchpad GlideAjax
When data is fetched At page load — before form renders On demand — triggered by a client event
Async? No — synchronous with form load Yes — async callback
Best for Data needed immediately when form opens Data needed reactively (e.g., on field change)
Extra round trip? No — bundled into initial page request Yes — extra HTTP request per call

Best Practices & Pitfalls

Business Rules are powerful but can cause serious problems when misused. These are the most important pitfalls to know for the exam and real-world administration.

Infinite Loop Prevention

Classic Infinite Loop Scenario: A Before BR on the Incident table (Update trigger) modifies a field and implicitly saves via current.update() (which you should NOT do in a Before BR). This triggers the Update event again, firing the same BR — infinite recursion.
// WRONG — calling current.update() in a Before BR causes infinite loop
// Before BR fires → modifies current → calls current.update()
// → triggers another Update event → Before BR fires again → ...
current.priority = 2;
current.update();    // DO NOT DO THIS in a Before BR

// CORRECT — in a Before BR, just modify current; changes are auto-saved
current.priority = 2;
// No current.update() needed — it happens automatically

// For After/Async BRs that call current.update() and risk looping:
// Use setWorkflow(false) to suppress re-firing workflows:
current.setWorkflow(false);
current.update();

// Or use autoSysFields(false) to prevent last_updated timestamp changes:
current.autoSysFields(false);
current.setWorkflow(false);
current.update();

setWorkflow() — Preventing Cascades

// Prevent the update from triggering workflows (but still triggers other BRs)
gr.setWorkflow(false);
gr.update();

// Prevent triggering OTHER business rules on this update
// (use carefully — can hide unintended side effects)
gr.setWorkflow(false);
gr.autoSysFields(false);
gr.update();

Firing Events with gs.eventQueue()

To trigger asynchronous notifications or integrations from a Business Rule without blocking the transaction, use gs.eventQueue() to enqueue an event.

// Fire a custom event asynchronously (for notifications, integrations)
// gs.eventQueue(event_name, gr_object, param1, param2)
gs.eventQueue('incident.critical.created', current, current.number.toString(), gs.getUserName());

// The event is picked up by a Notification or Script Action configured for that event name
// This avoids blocking the main transaction for slow notification logic

Best Practice Checklist

Use Conditions

Always set a Filter Condition or Advanced Condition to narrow when the BR fires. Avoid "fire on every save" BRs — it wastes server resources.

No current.update() in Before BRs

Changes to current in a Before BR are auto-saved. Calling current.update() here creates an infinite loop.

Use Async for Slow Ops

REST callouts, complex queries, and report generation should be Async BRs. Before/After BRs block the user's transaction.

setWorkflow(false)

When calling gr.update() inside a BR to update a related record, use setWorkflow(false) to prevent triggering additional workflows unnecessarily.

Use gs.eventQueue()

Fire events via gs.eventQueue() for notifications and integrations rather than calling them synchronously from a Before BR.

Keep Scripts Focused

Each Business Rule should do one thing. Split complex logic into multiple targeted BRs with clear names and conditions.

Key Exam Facts & Quick Reference

This section consolidates the most frequently tested Business Rule facts for fast review before the exam.

Before BR:
  • Runs before the database write
  • Changes to current are automatically saved — do NOT call current.update()
  • Can abort the transaction: current.setAbortAction(true)
  • No sys_id available on Insert (record not yet written)
  • Blocks the user's transaction — keep it fast
After BR:
  • Runs after the database write completes
  • Must call current.update() to persist further changes to current
  • sys_id is available (record has been written)
  • Cannot abort the transaction — record is already saved
Async BR:
  • Runs in a separate background thread after the transaction completes
  • Non-blocking — user gets their response immediately
  • Cannot abort the main transaction (it's already done)
  • Use for: email notifications, REST callouts, slow queries, report generation
Display BR:
  • Runs when a record is displayed (Query operation)
  • Primary purpose: populate g_scratchpad for client scripts
  • No database write — does not modify records
  • Client scripts read from g_scratchpad.propertyName
current vs. previous:
  • current — the record as it is NOW (new values)
  • previous — the record as it WAS before the update (old values); null on Insert
  • current.changes() — true if ANY field on the record changed
  • current.field.changes() — true if THAT specific field changed
  • current.field.changesTo('x') — true if that field's new value is 'x'
  • current.field.changesFrom('x') — true if that field's old value was 'x'
setAbortAction() Rules:
  • current.setAbortAction(true) — prevents the database write from occurring
  • Only works in Before BRs — in After or Async BRs it has no effect
  • Use with gs.addErrorMessage('message') to show the user why the save was rejected

Business Rule Anatomy — All Fields at a Glance

Field Purpose Key Values / Notes
Name Descriptive name for the BR Should clearly describe what it does and when
Table The table this BR applies to One table per BR (no cross-table triggering)
Active Whether the BR is enabled Unchecking disables without deleting
When Timing relative to DB write Before / After / Async / Display
Order Execution sequence among same-table/same-when BRs Lower runs first; default 100
Insert / Update / Delete / Query Operation checkboxes that trigger the BR Multiple can be selected; Query is for Display only
Filter Conditions Additional condition using condition builder Both Filter + Advanced must pass
Advanced Condition JavaScript boolean expression Must return true or false
Script The automation logic that runs when all conditions pass Has access to current, previous, gs
← UI Policies & Actions Client Scripts →