Master server-side scripting from fundamentals to advanced techniques. Learn types, best practices, GlideRecord, debugging, and real-world implementation patterns.
If you're new to ServiceNow development, understanding Business Rules is fundamental. According to the official ServiceNow documentation, Business Rules automate and enforce business logic or policies whenever data changes in a table.
Business Rules are stored in the sys_script table and can be accessed via System Definition > Business Rules in the navigator. They're the backbone of server-side automation in ServiceNow, running regardless of how data is modified - whether through the UI, APIs, imports, or scripts.
ServiceNow provides four distinct types of Business Rules, each designed for specific use cases based on when they execute relative to database operations. Understanding these types is crucial for effective ServiceNow development.
| Type | When It Runs | Common Use Cases | Key Considerations |
|---|---|---|---|
| Before | Before database operation | Data validation, field auto-population, abort action | No current.update() needed - changes auto-save |
| After | After database operation (synchronous) | Updating related records, triggering events | Blocks user until complete; use for immediate needs |
| Async | After database (asynchronous, background) | Emails, external APIs, SLA calculations | Non-blocking; runs via scheduler |
| Display | When record is loaded for viewing | Pass data to client via g_scratchpad | Only runs on form load, not on insert/update |
Before Business Rules execute before the record is saved to the database. They're ideal for data validation, field manipulation, and controlling whether an operation should proceed.
current and previous objects to manipulate record datacurrent.setAbortAction(true) to prevent insert/update/deleteprevious object contains the record's state before modification(function executeRule(current, previous /*null when async*/) {
// Auto-populate assignment group based on category
if (current.category == 'network') {
current.assignment_group.setDisplayValue('Network Team');
} else if (current.category == 'hardware') {
current.assignment_group.setDisplayValue('Hardware Support');
}
// Set priority based on urgency and impact
if (current.urgency == 1 && current.impact == 1) {
current.priority = 1; // Critical
}
// No current.update() needed - changes save automatically
})(current, previous);
Use setAbortAction() to prevent invalid operations. As noted in the ServiceNow Community forum, this is essential for server-side validation:
(function executeRule(current, previous /*null when async*/) {
// Validate that end date is after start date
if ((!current.u_start_date.nil()) && (!current.u_end_date.nil())) {
var start = current.u_start_date.getGlideObject().getNumericValue();
var end = current.u_end_date.getGlideObject().getNumericValue();
if (start > end) {
gs.addErrorMessage('Start date must be before end date');
current.u_start_date.setError('Invalid date range');
current.setAbortAction(true);
}
}
})(current, previous);
setAbortAction() method can only be executed from the same scope as the record being modified. Cross-scope Business Rules cannot use this method directly - use the "Abort action" checkbox instead.(function executeRule(current, previous /*null when async*/) {
// Prevent closing incident without resolution notes
if (current.state == 6) { // Resolved
if (current.close_notes.nil()) {
gs.addErrorMessage('Resolution notes are required before closing');
current.setAbortAction(true);
}
}
// Prevent reopening after 30 days
if (current.state.changes() && previous.state == 7) { // Was Closed
var closedDate = previous.closed_at.getGlideObject();
var now = new GlideDateTime();
var daysDiff = gs.dateDiff(closedDate.getValue(), now.getValue(), true);
if (Math.abs(daysDiff) > 30) {
gs.addErrorMessage('Cannot reopen incidents closed more than 30 days ago');
current.setAbortAction(true);
}
}
})(current, previous);
After Business Rules execute after the record is saved to the database. They run synchronously, meaning the user must wait for them to complete before the transaction finishes.
current.update()(function executeRule(current, previous /*null when async*/) {
// When a high priority incident is created, auto-create a problem task
if (current.priority == 1 && current.operation() == 'insert') {
var problem = new GlideRecord('problem');
problem.initialize();
problem.short_description = 'Investigate: ' + current.short_description;
problem.description = 'Auto-created from critical incident: ' + current.number;
problem.u_related_incident = current.sys_id;
problem.assignment_group = current.assignment_group;
problem.insert();
// Log the created problem
gs.info('Created Problem ' + problem.number + ' for Incident ' + current.number);
}
})(current, previous);
(function executeRule(current, previous /*null when async*/) {
// Update parent change request when all tasks are complete
if (current.state == 3) { // Closed Complete
var parent = new GlideRecord('change_request');
if (parent.get(current.parent)) {
// Check if all child tasks are complete
var openTasks = new GlideAggregate('change_task');
openTasks.addQuery('parent', current.parent);
openTasks.addQuery('state', '!=', 3);
openTasks.addQuery('sys_id', '!=', current.sys_id);
openTasks.addAggregate('COUNT');
openTasks.query();
if (openTasks.next() && openTasks.getAggregate('COUNT') == 0) {
parent.state = 3; // Move to Review
parent.update();
}
}
}
})(current, previous);
current.update() in After Business Rules when possible. As the Quality Clouds best practices notes, this can cause recursive execution. If you must update the current record, use setWorkflow(false) to prevent recursion.Async Business Rules run asynchronously in the background after the database operation completes. According to the ServiceNow performance guidelines, they're queued by the scheduler and run as soon as resources are available.
| Use Case | After BR | Async BR |
|---|---|---|
| Send email notifications | ||
| Call external REST APIs | ||
| SLA calculations | ||
| Heavy data processing | ||
| User needs immediate feedback | ||
| Must complete before user proceeds |
(function executeRule(current, previous /*null when async*/) {
// Send critical incident to external monitoring system
if (current.priority == 1) {
try {
var request = new sn_ws.RESTMessageV2('External_Monitor', 'POST_Alert');
request.setStringParameter('incident_number', current.number);
request.setStringParameter('description', current.short_description);
request.setStringParameter('priority', current.priority.getDisplayValue());
var response = request.execute();
var httpStatus = response.getStatusCode();
if (httpStatus == 200) {
gs.info('Successfully sent alert for ' + current.number);
} else {
gs.error('Failed to send alert: ' + response.getBody());
}
} catch (ex) {
gs.error('Exception sending alert: ' + ex.message);
}
}
})(current, previous);
(function executeRule(current, previous /*null when async*/) {
// Process attachments after they're fully uploaded
// Using Async ensures attachments are available
var attachments = new GlideRecord('sys_attachment');
attachments.addQuery('table_name', 'incident');
attachments.addQuery('table_sys_id', current.sys_id);
attachments.query();
var attachmentList = [];
while (attachments.next()) {
attachmentList.push(attachments.file_name.toString());
}
if (attachmentList.length > 0) {
// Queue event for email notification with attachment info
gs.eventQueue('incident.attachments_added', current,
attachmentList.join(', '),
attachmentList.length.toString());
}
})(current, previous);
Display Business Rules execute when a record is loaded for viewing on a form - after data is read from the database but before it's displayed to the user. Their primary purpose is passing server-side data to client scripts via the g_scratchpad object.
The g_scratchpad object is a bridge between server and client. It's a one-directional process (server to client) that eliminates the need for expensive AJAX calls.
Display Business Rule:
(function executeRule(current, previous /*null when async*/) {
// Check if current user is member of VIP Support group
var groupSysId = 'd625dccec0a8016700a222a0f7900d06'; // VIP Support group
g_scratchpad.isVIPSupport = gs.getUser().isMemberOf(groupSysId);
// Also pass user's department
g_scratchpad.userDepartment = gs.getUser().getDepartmentID();
// Pass calculated field not on the form
g_scratchpad.daysSinceCreated = gs.dateDiff(current.sys_created_on,
gs.nowDateTime(), true);
})(current, previous);
Client Script (onLoad):
function onLoad() {
// Show/hide VIP-only fields based on group membership
if (g_scratchpad.isVIPSupport) {
g_form.setDisplay('u_vip_notes', true);
g_form.setDisplay('u_escalation_path', true);
} else {
g_form.setDisplay('u_vip_notes', false);
g_form.setDisplay('u_escalation_path', false);
}
// Show age warning for old records
if (parseInt(g_scratchpad.daysSinceCreated) > 30) {
g_form.showFieldMsg('state',
'This record is over 30 days old', 'warning');
}
}
Display Business Rule (on problem_task table):
(function executeRule(current, previous /*null when async*/) {
// Pass parent problem details to form
var gr = new GlideRecord('problem');
if (gr.get(current.parent)) {
g_scratchpad.parentProblem = current.parent.toString();
g_scratchpad.parentShortDesc = gr.short_description.toString();
g_scratchpad.parentAssignedTo = gr.assigned_to.toString();
g_scratchpad.parentAssignmentGroup = gr.assignment_group.toString();
}
})(current, previous);
Client Script (onLoad):
function onLoad() {
// Auto-populate fields from parent on new record
if (g_form.isNewRecord() && g_scratchpad.parentProblem) {
g_form.setValue('short_description',
'Task for: ' + g_scratchpad.parentShortDesc);
g_form.setValue('assigned_to', g_scratchpad.parentAssignedTo);
g_form.setValue('assignment_group', g_scratchpad.parentAssignmentGroup);
}
}
Before Query Business Rules are a special type that execute before every query on a table. According to the ServiceNow performance best practices, they can add filters to control which records users can access, making them powerful but potentially performance-impacting.
(function executeRule(current, previous /*null when async*/) {
// Only show records belonging to user's company
var userCompany = gs.getUser().getCompanyID();
if (userCompany) {
current.addQuery('company', userCompany);
}
})(current, previous);
Understanding execution order is crucial for debugging and avoiding conflicts. According to the ServiceNow Community, business rules apply consistently regardless of whether records are accessed through forms, lists, or web services. However, Display business rules only run when viewing forms.
Each Business Rule type responds to specific database operations. Use the checkboxes in the "When to run" tab to configure which operations trigger your rule:
| BR Type | Insert | Update | Delete | Query | Display | When It Fires |
|---|---|---|---|---|---|---|
| Before | ✓ | ✓ | ✓ | ✗ | ✗ | Before record is written to database |
| After | ✓ | ✓ | ✓ | ✗ | ✗ | After record is committed (synchronous) |
| Async | ✓ | ✓ | ✓ | ✗ | ✗ | After record is committed (background job) |
| Display | ✗ | ✗ | ✗ | ✗ | ✓ | When form is loaded for viewing only |
| Query | ✗ | ✗ | ✗ | ✓ | ✗ | Before any GlideRecord query executes |
When a form is submitted, here's the detailed execution order:
| Order | Component | Location | Purpose |
|---|---|---|---|
| 1 | onSubmit Client Scripts | Browser | Client-side validation before submit |
| 2 | Before BR (Order < 1000) | Server | Server validation, field manipulation |
| 3 | Engines (Workflow, etc.) | Server | At order 1000 |
| 4 | Before BR (Order ≥ 1000) | Server | After engine processing |
| 5 | Database Operation | Database | Insert / Update / Delete |
| 6 | After Business Rules | Server | Synchronous post-save logic |
| 7 | Async Business Rules | Scheduler | Background tasks (non-blocking) |
The Order field determines execution sequence among Business Rules of the same type. Lower values execute first. The default is 100, and the magic number 1000 is when ServiceNow engines (workflows, approvals) run.
For Async Business Rules, there are two relevant fields:
A common point of confusion for ServiceNow beginners is understanding the relationship between Business Rules and GlideRecord. They are fundamentally different concepts that work together.
| Aspect | Business Rule | GlideRecord |
|---|---|---|
| What is it? | An automation trigger/container | A JavaScript API/class |
| Purpose | Define WHEN code runs (on insert, update, delete, query) | Define HOW to interact with database (CRUD operations) |
| Stored in | sys_script table | Not stored - it's a runtime API |
| Configuration | Table, When (Before/After/Async), Conditions, Order | N/A - used within scripts |
| Relationship | Contains scripts that USE GlideRecord | Used BY Business Rules (and other scripts) |
Think of it this way: A Business Rule is the "container" that defines when your automation runs, and GlideRecord is the "tool" you use inside that container to work with data.
// This is a BUSINESS RULE (the automation container)
// It runs AFTER an incident is updated
(function executeRule(current, previous) {
// Inside the BR, we use GLIDERECORD (the database API)
// to create a related task
var task = new GlideRecord('task'); // GlideRecord API
task.initialize();
task.short_description = 'Follow-up for ' + current.number;
task.parent = current.sys_id;
task.insert(); // GlideRecord method
})(current, previous);
In Business Rules, current is a special GlideRecord object that represents the record being processed. You don't need to query for it - ServiceNow provides it automatically:
(function executeRule(current, previous) {
// 'current' IS a GlideRecord - no need to create one!
// It already contains the record being inserted/updated
current.priority = 1; // Modify the current record
current.assignment_group.setDisplayValue('Network Team');
// For Before BRs: changes save automatically
// For After BRs: would need current.update() (but avoid this!)
})(current, previous);
// QUERY: Find records
var gr = new GlideRecord('incident');
gr.addQuery('priority', 1);
gr.query();
while (gr.next()) { /* process */ }
// INSERT: Create new record
var gr = new GlideRecord('task');
gr.initialize();
gr.short_description = 'New task';
gr.insert();
// UPDATE: Modify existing records
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.query();
while (gr.next()) {
gr.state = 2;
gr.update(); // Required for GlideRecord updates
}
// COUNT: Use GlideAggregate (more efficient!)
var ga = new GlideAggregate('incident');
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
var count = ga.getAggregate('COUNT');
}
current.update() - changes to current are saved automatically. Use update() only when modifying OTHER records via GlideRecord.Following best practices is essential for maintaining a performant and maintainable ServiceNow instance. These guidelines come from Quality Clouds and the ServiceNow Community.
Before writing custom scripts, consider using ServiceNow's built-in Actions tab for simple field value assignments. Declarative actions are easier to maintain, less error-prone, and don't require scripting knowledge.
// BAD - Don't do this in Before BR
current.state = 2;
current.update(); // WRONG! Will cause recursion
// GOOD - Just set the value
current.state = 2;
// No update() needed - saves automatically
If you must use current.update() in an After BR, prevent recursion with setWorkflow(false):
(function executeRule(current, previous) {
// Only if absolutely necessary in After BR
current.u_processed = true;
current.setWorkflow(false); // Prevents recursive BRs
current.update();
current.setWorkflow(true); // Re-enable for future operations
})(current, previous);
| Practice | Why It Matters |
|---|---|
| Use GlideAggregate for counts | Database-optimized, doesn't load all records |
| Avoid multiple OR conditions | Poor database performance |
| Don't use CONTAINS in high-volume BRs | Full table scans are expensive |
| Use Script Includes over Global BRs | Only loads when called |
| Cache repeated lookups | Reduces database queries |
| Use setLimit() when appropriate | Don't retrieve more records than needed |
Effective debugging is essential for ServiceNow developers. According to ServiceNow's debugging guide, Business Rules exceeding 100ms are automatically logged as slow. This section covers the logging functions, debugging tools, and troubleshooting techniques every developer should know.
ServiceNow provides several logging functions through the gs (GlideSystem) object. Understanding when to use each is crucial for effective debugging:
| Function | Log Level | Writes To | Scoped App? | Use Case |
|---|---|---|---|---|
gs.debug() |
DEBUG | Session log, sys_log | Yes | Detailed debugging info (requires session debug enabled) |
gs.info() |
INFO | Session log, sys_log | Yes | General information messages |
gs.warn() |
WARN | Session log, sys_log | Yes | Warning conditions that aren't errors |
gs.error() |
ERROR | Session log, sys_log | Yes | Error conditions and exceptions |
gs.log() |
INFO | sys_log, node log file | No | Global scope only (legacy) |
gs.print() |
— | Node log file only | No | Text file logging only |
gs.log() will throw an error. Always use gs.info() or gs.debug() instead. This is a common mistake that causes Business Rules to fail silently.Log messages can be viewed in several locations depending on the logging function used:
gs.info(), gs.warn(), gs.error(), and gs.debug() output in the sys_log tablegs.log() and gs.print() outputAccording to ServiceNow KB2593531, log entries contain structured fields for effective debugging:
Session debugging is essential for seeing gs.debug() output and understanding Business Rule execution. According to the ServiceNow Developer Blog:
X-WantSessionDebugMessages: true to include gs.debug() output in the response.The Script Tracer provides detailed execution tracking for server-side scripts:
For performance analysis, use the Transaction Logs to identify slow Business Rules:
Use this pattern for comprehensive debugging in your Business Rules:
(function executeRule(current, previous) {
var BR_NAME = 'MyBusinessRule';
// Entry point logging
gs.info('[' + BR_NAME + '] Starting - Table: ' + current.getTableName() +
', Record: ' + current.getUniqueValue());
try {
var startTime = new Date().getTime();
// Debug level for detailed tracing (requires session debug)
gs.debug('[' + BR_NAME + '] Field values - state: ' + current.state +
', priority: ' + current.priority);
// Your business logic here
// ...
var endTime = new Date().getTime();
gs.info('[' + BR_NAME + '] Completed in ' + (endTime - startTime) + 'ms');
} catch (ex) {
// Always log errors with stack trace
gs.error('[' + BR_NAME + '] Error: ' + ex.message);
gs.error('[' + BR_NAME + '] Stack: ' + ex.stack);
}
})(current, previous);
If your Business Rule isn't executing or logging, check these common issues:
gs.log() with gs.info() in scoped applicationsgs.debug() outputChoosing the right scripting mechanism is crucial. According to the ServiceNow Community discussion, each has specific use cases.
| Feature | Business Rule | Client Script | UI Policy |
|---|---|---|---|
| Execution | Server-side | Browser (client-side) | Browser (client-side) |
| Triggers on | Insert, Update, Delete, Query | onLoad, onChange, onSubmit | Form load, Field change |
| APIs Available | GlideRecord, gs, etc. | g_form, g_user, g_scratchpad | UI Policy Actions |
| Works with | Any data access (UI, API, script) | Form interactions only | Form interactions only |
| Scripting Required | Yes | Yes | Optional |
| Scenario | Recommended Approach |
|---|---|
| Make field mandatory/read-only/hidden | UI Policy - No scripting needed |
| Complex client-side logic | Client Script |
| onSubmit validation before save | Client Script (immediate feedback) |
| Server-side data validation | Before Business Rule |
| Auto-populate fields on save | Before Business Rule |
| Create related records after save | After Business Rule |
| Send notifications/emails | Async Business Rule |
| Logic must run regardless of access method | Business Rule |
| Pass server data to form | Display BR + g_scratchpad |
Based on the ServiceNow Community's interview guide and real interview experiences, here are 30 frequently asked Business Rule questions organized by difficulty level:
sys_script table. Access via System Definition > Business Rules. (Q3)current object?previous object?current.field_name.changes() or compare current.field_name != previous.field_name. (Q10)current.setWorkflow(false) before update() to prevent recursion. (Q11)current.setAbortAction(true) in a Before BR. Optionally add a message with gs.addErrorMessage(). (Q14)current.caller_id.email) or create a new GlideRecord query to fetch related records. (Q17)gs.info(), gs.debug(), gs.warn(), gs.error(). Note: gs.log() does NOT work in scoped apps. (Q18)current.setWorkflow(false) before the update operation. (Q19)gs.nil() and checking for empty string?gs.nil() checks for null, undefined, and empty string. It's more comprehensive than == ''. (Q20)onSubmit Client Script → Before BR (Order < 1000) → Engines/Workflows (Order 1000) → Before BR (Order ≥ 1000) → Database Operation → After BR → Async BR (Q21)current object in After BR reflects the saved state. (Q27)current.setWorkflow(false) to prevent BRs from re-triggering, or add a condition checking current.operation() == 'update' with a specific field change. (Q29)Reference guide for technical terms and abbreviations used throughout this article.
Continue your ServiceNow learning journey with these related guides: