Business Rules
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.
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
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).
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.
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.
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 |
- Before BR: modifying
currentsaves automatically — do NOT callcurrent.update()(causes infinite loop!) - After BR: must call
current.update()to persist changes; hassys_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.
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 |
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();
}
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')
// 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
| 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.
- User navigates to a record form
- ServiceNow fires the Display Business Rule (When = Display, Operation = Query)
- The BR script populates
g_scratchpad.myProperty = someValue - 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
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.
- Runs before the database write
- Changes to
currentare automatically saved — do NOT callcurrent.update() - Can abort the transaction:
current.setAbortAction(true) - No
sys_idavailable on Insert (record not yet written) - Blocks the user's transaction — keep it fast
- Runs after the database write completes
- Must call
current.update()to persist further changes tocurrent sys_idis available (record has been written)- Cannot abort the transaction — record is already saved
- 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
- Runs when a record is displayed (Query operation)
- Primary purpose: populate
g_scratchpadfor client scripts - No database write — does not modify records
- Client scripts read from
g_scratchpad.propertyName
current— the record as it is NOW (new values)previous— the record as it WAS before the update (old values); null on Insertcurrent.changes()— true if ANY field on the record changedcurrent.field.changes()— true if THAT specific field changedcurrent.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'
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 |