Business Rules Development

SkillDev tools

Develop secure, efficient business rules with correct timing, conditions, Glide API patterns, error handling, and tests.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Business Rules Development skill

What this skill tells your AI

The instructions your AI receives, as published by happy-technologies-llc/happy-platform-skills in skills/development/business-rules/SKILL.md and read by ahel’s review.

Overview

Business rules are server-side scripts that execute when records are displayed, inserted, updated, deleted, or queried. They are the backbone of ServiceNow automation.

  • What problem does it solve? Automates record-level logic, enforces data integrity, and triggers workflows based on record changes
  • Who should use this skill? ServiceNow developers building custom automation logic
  • Expected outcomes: Well-structured, performant business rules that follow ServiceNow best practices

Prerequisites

  • Roles: admin or scoped app developer role
  • Knowledge: JavaScript fundamentals, GlideRecord API basics
  • Access: sys_script table, target table for business rule
  • Related skills: admin/script-execution, admin/update-set-management

When to Use Business Rules

Timing Matrix

WhenTriggerUse Casecurrent/previous
beforeInsert/Update/DeleteValidate data, set field values, abort operationsBoth available
afterInsert/Update/DeleteCreate related records, send notifications, external integrationsBoth available
asyncInsert/Update/DeleteLong-running operations, external API callsOnly current
displayQuery/DisplayCalculate runtime values, populate scratchpadOnly current

Decision Guide

Use BEFORE when:

  • Setting default values based on other fields
  • Validating data before save
  • Modifying field values before commit
  • Aborting invalid operations with current.setAbortAction(true)

Use AFTER when:

  • Creating child/related records
  • Sending notifications (after record is committed)
  • Updating other tables
  • Triggering workflows

Use ASYNC when:

  • Making external REST/SOAP calls
  • Processing large data sets
  • Operations that can fail without blocking the user
  • Long-running calculations

Use DISPLAY when:

  • Calculating values for form display only
  • Populating g_scratchpad for client scripts
  • Runtime-only field values (not stored)

Procedure

Phase 1: Create a Business Rule

Step 1.1: Query Table Schema

First, understand the target table structure.

Using MCP:

Tool: SN-Get-Table-Schema
Parameters:
  table_name: incident
Step 1.2: Create Basic Business Rule

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script
  data:
    name: Set Priority Based on Impact and Urgency
    collection: incident
    active: true
    when: before
    order: 100
    filter_condition: impactCHANGES^ORurgencyCHANGES
    script: |
      (function executeRule(current, previous /*null when async*/) {

        // Calculate priority from impact and urgency matrix
        var impact = parseInt(current.impact);
        var urgency = parseInt(current.urgency);

        // Priority matrix: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning
        var matrix = {
          '1-1': 1, '1-2': 2, '1-3': 3,
          '2-1': 2, '2-2': 3, '2-3': 4,
          '3-1': 3, '3-2': 4, '3-3': 5
        };

        var key = impact + '-' + urgency;
        var newPriority = matrix[key] || 4;

        if (current.priority != newPriority) {
          current.priority = newPriority;
          gs.info('Priority calculated: ' + newPriority + ' for ' + current.number);
        }

      })(current, previous);
Step 1.3: Create After Business Rule

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script
  data:
    name: Create Related Task on P1 Incident
    collection: incident
    active: true
    when: after
    order: 200
    filter_condition: priority=1^stateVALCHANGES1
    script: |
      (function executeRule(current, previous /*null when async*/) {

        // Only on insert or when becoming P1
        if (current.operation() == 'insert' ||
            (previous && previous.priority != 1)) {

          var task = new GlideRecord('sc_task');
          task.initialize();
          task.short_description = 'P1 Response: ' + current.short_description;
          task.description = 'Critical incident requires immediate response.\n\nIncident: ' + current.number;
          task.assignment_group = current.assignment_group;
          task.assigned_to = current.assigned_to;
          task.priority = 1;
          task.parent = current.sys_id;
          var taskId = task.insert();

          gs.info('Created P1 response task: ' + taskId + ' for ' + current.number);
        }

      })(current, previous);
Step 1.4: Create Async Business Rule

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script
  data:
    name: Notify External System on Incident Create
    collection: incident
    active: true
    when: async
    order: 500
    action_insert: true
    script: |
      (function executeRule(current, previous /*null when async*/) {

        try {
          var request = new sn_ws.RESTMessageV2('External Notification', 'POST');
          request.setStringParameterNoEscape('incident_number', current.number.toString());
          request.setStringParameterNoEscape('short_description', current.short_description.toString());
          request.setStringParameterNoEscape('priority', current.priority.toString());

          var response = request.execute();
          var httpStatus = response.getStatusCode();

          if (httpStatus == 200 || httpStatus == 201) {
            gs.info('External notification sent for: ' + current.number);
          } else {
            gs.error('External notification failed: ' + httpStatus + ' - ' + response.getBody());
          }
        } catch (e) {
          gs.error('External notification error: ' + e.message);
        }

      })(current, previous);

Phase 2: Using current and previous Objects

Step 2.1: Understanding current vs previous

The current object represents the record being processed. The previous object contains field values before the current transaction.

Key Differences:

Aspectcurrentprevious
AvailabilityAll business rulesbefore/after only (null in async)
ModifiableYes (before rules)No (read-only)
Insert operationsHas valuesnull
ValuesNew/modifiedOriginal before change
Step 2.2: Detecting Field Changes

Using .changes() Method (Recommended):

Tool: SN-Create-Record
Parameters:
  table_name: sys_script
  data:
    name: Log State Changes
    collection: incident
    active: true
    when: after
    order: 100
    script: |
      (function executeRule(current, previous /*null when async*/) {

        // EFFICIENT: Exit early if field hasn't changed
        if (!current.state.changes()) {
          return;  // No work to do
        }

        // Get old and new values
        var oldState = previous ? previous.state.getDisplayValue() : '(new)';
        var newState = current.state.getDisplayValue();

        gs.info('Incident ' + current.number + ' state changed: ' + oldState + ' -> ' + newState);

        // Add work note
        current.work_notes = 'State changed from ' + oldState + ' to ' + newState;

      })(current, previous);

Manual Change Detection (When .changes() Not Suitable):

// For complex comparisons or calculated changes
if (previous && current.priority != previous.priority) {
  // Priority changed
}

// For reference fields, compare sys_id
if (previous && current.assigned_to.toString() != previous.assigned_to.toString()) {
  // Assignment changed
}

// For multiple fields
var fieldsToCheck = ['state', 'priority', 'assigned_to'];
var changedFields = [];
for (var i = 0; i < fieldsToCheck.length; i++) {
  var field = fieldsToCheck[i];
  if (current[field].changes()) {
    changedFields.push(field);
  }
}
if (changedFields.length > 0) {
  gs.info('Changed fields: ' + changedFields.join(', '));
}
Step 2.3: Checking Operation Type
// Determine what triggered the business rule
var operation = current.operation();

switch (operation) {
  case 'insert':
    // Record is being created
    gs.info('New record: ' + current.getTableName());
    break;
  case 'update':
    // Record is being updated
    gs.info('Update to: ' + current.getUniqueValue());
    break;
  case 'delete':
    // Record is being deleted
    gs.info('Deleting: ' + current.getUniqueValue());
    break;
}

Phase 3: Condition Field vs Script Conditions

Step 3.1: Condition Field (Preferred for Simple Conditions)

The condition field uses encoded queries and is evaluated BEFORE the script runs. This is more efficient because:

  • Evaluated at the database level
  • Script never executes if condition fails
  • No JavaScript overhead

Best Practices for Condition Field:

# Only run on active P1 incidents
active=true^priority=1

# Only when state changes to Resolved
stateVALCHANGES6

# Only when assigned_to changes
assigned_toCHANGES

# Multiple conditions (AND)
active=true^priority=1^stateVALCHANGES6

# Only on insert (no previous value for state)
stateISEMPTYfalse^ORstateISEMPTY

Common Condition Operators:

OperatorMeaningExample
CHANGESField value changedstateCHANGES
VALCHANGESChanged TO specific valuestateVALCHANGES6
CHANGESFROMChanged FROM specific valuestateCHANGESFROM1
=Equalspriority=1
!=Not equalsstate!=7
ISEMPTYField is emptyassigned_toISEMPTY
ISNOTEMPTYField has valueassigned_toISNOTEMPTY
Step 3.2: Script Conditions (For Complex Logic)

Use script conditions only when the condition field cannot express the logic.

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script
  data:
    name: Complex Condition Example
    collection: incident
    active: true
    when: before
    order: 100
    condition: |
      // Script condition - returns true/false
      // Available: current, previous

      // Check if escalating (priority going from higher number to lower)
      if (current.priority.changes()) {
        var oldPri = previous ? parseInt(previous.priority) : 5;
        var newPri = parseInt(current.priority);
        return newPri < oldPri;  // True if escalating
      }
      return false;
    script: |
      (function executeRule(current, previous /*null when async*/) {

        // This only runs if condition returned true
        current.work_notes = 'Incident escalated from P' + previous.priority + ' to P' + current.priority;
        gs.info('Escalation detected: ' + current.number);

      })(current, previous);

When to Use Script Conditions:

  • Complex date calculations
  • Cross-table validations
  • Dynamic conditions based on user roles
  • Calculations involving multiple fields
  • Conditions requiring GlideRecord queries

Phase 4: Common Glide API Methods

Step 4.1: GlideRecord Essentials
// Query records
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.addQuery('priority', 1);
gr.orderByDesc('sys_created_on');
gr.setLimit(10);
gr.query();

while (gr.next()) {
  gs.info('Found: ' + gr.number + ' - ' + gr.short_description);
}

// Get single record by sys_id
var incident = new GlideRecord('incident');
if (incident.get('sys_id_value')) {
  gs.info('Found: ' + incident.number);
}

// Get by field value
var user = new GlideRecord('sys_user');
if (user.get('user_name', 'admin')) {
  gs.info('Admin sys_id: ' + user.sys_id);
}

// Insert new record
var newTask = new GlideRecord('task');
newTask.initialize();
newTask.short_description = 'New task from business rule';
newTask.assignment_group = current.assignment_group;
var sysId = newTask.insert();

// Update record
var toUpdate = new GlideRecord('incident');
if (toUpdate.get('sys_id_value')) {
  toUpdate.work_notes = 'Updated via business rule';
  toUpdate.update();
}

// Delete record (use with caution)
var toDelete = new GlideRecord('task');
if (toDelete.get('sys_id_value')) {
  toDelete.deleteRecord();
}
Step 4.2: GlideSystem (gs) Methods
// Logging
gs.info('Information message');
gs.warn('Warning message');
gs.error('Error message');
gs.debug('Debug message');  // Requires debug enabled

// User context
var userId = gs.getUserID();
var userName = gs.getUserName();
var userDisplayName = gs.getUserDisplayName();

// Check roles
if (gs.hasRole('admin')) {
  // Admin-only logic
}
if (gs.hasRole('itil') || gs.hasRole('catalog_admin')) {
  // ITIL or catalog admin logic
}

// Date/Time
var now = gs.now();                    // Current date/time string
var nowDT = gs.nowDateTime();          // GlideDateTime
var today = gs.beginningOfToday();     // Start of today
var daysAgo = gs.daysAgo(7);           // 7 days ago

// Properties
var propValue = gs.getProperty('my.property.name', 'default');
gs.setProperty('my.property.name', 'new_value');

// Generate GUID
var guid = gs.generateGUID();

// Include script include
gs.include('MyScriptInclude');
var util = new MyScriptInclude();

// Nil check (empty string, null, undefined)
if (gs.nil(current.assigned_to)) {
  // Field is empty
}

// Event queue (trigger events)
gs.eventQueue('incident.created', current, current.number, current.priority);
Step 4.3: GlideDateTime Operations
// Current time
var now = new GlideDateTime();

// Create from string
var dt = new GlideDateTime('2026-02-06 10:30:00');

// Add/subtract time
var future = new GlideDateTime();
future.addDays(5);
future.addHours(2);
future.addMinutes(30);

// Compare dates
var dt1 = new GlideDateTime();
var dt2 = new GlideDateTime(current.sys_created_on);
if (dt1.after(dt2)) {
  gs.info('dt1 is after dt2');
}

// Calculate duration
var duration = GlideDateTime.subtract(dt2, dt1);  // Returns GlideDuration
var seconds = duration.getNumericValue() / 1000;
var displayValue = duration.getDisplayValue();  // "2 Days 3 Hours"

// Business time calculations
var schedule = new GlideSchedule('sys_id_of_schedule');
var dur = schedule.duration(dt1, dt2);

// Format output
var formatted = now.getDisplayValue();  // User's timezone
var internal = now.getValue();          // Internal format (UTC)
var date = now.getLocalDate().getValue();  // Date only
Step 4.4: GlideAggregate for Counts and Sums
// Count records
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
  var count = ga.getAggregate('COUNT');
  gs.info('Active incidents: ' + count);
}

// Count by group
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.orderByAggregate('COUNT', false);  // Descending
ga.query();
while (ga.next()) {
  var priority = ga.priority.getDisplayValue();
  var count = ga.getAggregate('COUNT');
  gs.info(priority + ': ' + count + ' incidents');
}

// Sum values
var ga = new GlideAggregate('sc_task');
ga.addQuery('active', true);
ga.addAggregate('SUM', 'time_worked');
ga.query();
if (ga.next()) {
  var totalTime = ga.getAggregate('SUM', 'time_worked');
}

// Average
ga.addAggregate('AVG', 'priority');

// Min/Max
ga.addAggregate('MIN', 'sys_created_on');
ga.addAggregate('MAX', 'sys_created_on');

Phase 5: Error Handling and Logging

Step 5.1: Comprehensive Error Handling Pattern

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script
  data:
    name: Robust Error Handling Example
    collection: incident
    active: true
    when: after
    order: 100
    script: |
      (function executeRule(current, previous /*null when async*/) {

        var BR_NAME = 'Robust Error Handling Example';

        try {
          // Validate inputs
          if (gs.nil(current.sys_id)) {
            throw new Error('Current record has no sys_id');
          }

          // Main logic
          var relatedCI = new GlideRecord('cmdb_ci');
          if (!relatedCI.get(current.cmdb_ci)) {
            gs.warn('[' + BR_NAME + '] No CI found for incident: ' + current.number);
            return;  // Exit gracefully
          }

          // Perform operations
          relatedCI.operational_status = 2;  // Under maintenance
          relatedCI.update();

          gs.info('[' + BR_NAME + '] Updated CI: ' + relatedCI.name);

        } catch (e) {
          // Log error with context
          gs.error('[' + BR_NAME + '] Error: ' + e.message);
          gs.error('[' + BR_NAME + '] Incident: ' + current.number);
          gs.error('[' + BR_NAME + '] Stack: ' + e.stack);

          // Optionally create incident for business rule error
          // createErrorIncident(BR_NAME, e, current);
        }

      })(current, previous);
Step 5.2: Abort Actions with User Feedback
// Before business rule - prevent invalid save
(function executeRule(current, previous) {

  var errors = [];

  // Validation 1: Required field
  if (current.priority == 1 && gs.nil(current.assigned_to)) {
    errors.push('P1 incidents must have an assigned user');
  }

  // Validation 2: Business logic
  if (current.state == 6 && gs.nil(current.resolution_notes)) {
    errors.push('Resolution notes are required when resolving');
  }

  // Validation 3: Cross-field validation
  if (current.impact == 1 && current.urgency == 1 && current.priority != 1) {
    errors.push('Impact 1 + Urgency 1 must equal Priority 1');
  }

  // Abort if errors
  if (errors.length > 0) {
    var errorMsg = errors.join('\n');
    gs.addErrorMessage(errorMsg);
    current.setAbortAction(true);
    gs.info('[Validation BR] Aborted save: ' + errorMsg);
  }

})(current, previous);

Phase 6: Performance Optimization

Step 6.1: Use .changes() for Efficiency

WRONG - Always executes full script:

// Inefficient - script runs on every update
(function executeRule(current, previous) {
  if (previous && current.state != previous.state) {
    // State changed - do work
  }
})(current, previous);

RIGHT - Use condition field + .changes():

Filter Condition: stateCHANGES

Script:
(function executeRule(current, previous) {
  // Script only runs when state changes
  // Additional validation with .changes() for safety
  if (current.state.changes()) {
    // Do work
  }
})(current, previous);
Step 6.2: Minimize GlideRecord Queries

WRONG - Query inside loop:

var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
  // BAD: Query for each incident
  var user = new GlideRecord('sys_user');
  user.get(incidents.assigned_to);
  // ...
}

RIGHT - Use dot-walking or batch queries:

// Option 1: Dot-walking (single query)
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
  var userName = incidents.assigned_to.name;  // Dot-walk
  var userEmail = incidents.assigned_to.email;
}

// Option 2: Batch query with lookup map
var userIds = [];
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
  if (!gs.nil(incidents.assigned_to)) {
    userIds.push(incidents.assigned_to.toString());
  }
}

var userMap = {};
if (userIds.length > 0) {
  var users = new GlideRecord('sys_user');
  users.addQuery('sys_id', 'IN', userIds.join(','));
  users.query();
  while (users.next()) {
    userMap[users.sys_id.toString()] = {
      name: users.name.toString(),
      email: users.email.toString()
    };
  }
}
Step 6.3: Order and Active Flag Best Practices
PracticeRecommendation
Order100-199 for validation, 200-399 for field setting, 400+ for external/async
ActiveSet to false during development, enable when tested
ConditionUse filter condition field, not script conditions
InheritanceSet "Inherits" carefully - usually leave unchecked
Step 6.4: Avoid Recursive Updates

DANGEROUS - Can cause infinite loops:

// After business rule on incident
(function executeRule(current, previous) {
  current.work_notes = 'Updated at ' + gs.now();
  current.update();  // DANGER: Triggers business rules again!
})(current, previous);

SAFE - Set workflow false or use before rules:

// Option 1: Disable workflow on update
var gr = new GlideRecord('incident');
if (gr.get(current.sys_id)) {
  gr.setWorkflow(false);  // Skip business rules
  gr.autoSysFields(false);  // Skip sys field updates
  gr.work_notes = 'Updated at ' + gs.now();
  gr.update();
}

// Option 2: Use before rule instead (preferred)
// Before business rule - modifies current directly
(function executeRule(current, previous) {
  current.work_notes = 'Updated at ' + gs.now();
  // No .update() needed - current is saved automatically
})(current, previous);

Phase 7: Testing Business Rules

Step 7.1: Query Existing Business Rules

Using MCP:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script
  query: collection=incident^active=true
  fields: name,when,order,filter_condition,active
  limit: 50
Step 7.2: Test with Background Script

Using MCP:

Tool: SN-Execute-Background-Script
Parameters:
  script: |
    // Test business rule by simulating record operation
    var testIncident = new GlideRecord('incident');
    testIncident.initialize();
    testIncident.short_description = 'Test incident for BR testing';
    testIncident.caller_id = gs.getUserID();
    testIncident.impact = 1;
    testIncident.urgency = 1;

    // Insert will trigger before/after insert rules
    var sysId = testIncident.insert();
    gs.info('Created test incident: ' + sysId);

    // Verify priority was calculated
    testIncident.get(sysId);
    gs.info('Priority after BR: ' + testIncident.priority.getDisplayValue());

    // Clean up (optional)
    // testIncident.deleteRecord();
  description: Test priority calculation business rule
Step 7.3: Check System Logs

Using MCP:

Tool: SN-Query-Table
Parameters:
  table_name: syslog
  query: messageLIKEbusiness rule^ORmessageLIKE[BR]^sys_created_on>javascript:gs.minutesAgo(10)
  fields: message,level,sys_created_on,source
  limit: 50
Step 7.4: Explicit Local Development Cycle

Pull the current script and freshness metadata:

Tool: SN-Get-Record
Parameters:
  table_name: sys_script
  sys_id: [business_rule_sys_id]
  fields: sys_id,script,sys_updated_on,sys_mod_count
  instance: dev

Save the script field locally and edit it in the IDE. Immediately before pushing, repeat SN-Get-Record and compare sys_updated_on, sys_mod_count, and the original script. If the remote record changed, merge it locally instead of overwriting it. Otherwise push only the reviewed script field:

Tool: SN-Update-Record
Parameters:
  table_name: sys_script
  sys_id: [business_rule_sys_id]
  data:
    script: [reviewed_local_script]
  instance: dev

Tool Usage Summary

OperationMCP ToolPurpose
Create BRSN-Create-Record (sys_script)Create new business rule
Update BRSN-Update-RecordModify existing business rule
Query BRsSN-Query-TableFind business rules on table
Test BRSN-Execute-Background-ScriptSimulate record operations
DebugSN-Query-Table (syslog)Check execution logs
Local DevSN-Get-Record + SN-Update-RecordExplicit pull, freshness check, edit, and push
Get SchemaSN-Get-Table-SchemaUnderstand table structure

Best Practices

Code Quality

  • Use IIFE Pattern: Wrap all scripts in (function executeRule(current, previous) {...})(current, previous);
  • Name Variables Clearly: Use descriptive names like existingTask not gr2
  • Comment Complex Logic: Explain why, not what
  • Avoid Magic Numbers: Use constants or comments for values like state=6

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
37
Forks
13
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
business-rules
Source
github.com/happy-technologies-llc/happy-platform-skills