What It Is

Business Rules are server-side JavaScript scripts that execute automatically when records are inserted, updated, deleted, or queried in ServiceNow. They run on the ServiceNow application server, not in the user's browser, making them the primary mechanism for enforcing business logic that must be guaranteed regardless of how data enters the system. Unlike Client Scripts that can be bypassed by web service calls or imports, Business Rules execute for every database transaction, whether it originates from the UI, REST API, SOAP web services, or scheduled jobs. This makes them the platform's enforcement layer for critical business logic that cannot be circumvented.

Architecturally, Business Rules sit between your application layer and the database, intercepting every transaction to apply custom logic. When a record operation occurs, ServiceNow's engine evaluates all applicable Business Rules based on their table, conditions, and timing configuration before or after the database operation completes. The script executes with full server-side privileges and access to the GlideRecord API, system properties, and other server-side resources unavailable to client-side scripts. This positioning makes them uniquely powerful but also uniquely dangerous to performance if poorly implemented.

Under the hood, ServiceNow processes Business Rules through a sophisticated execution engine that evaluates rule conditions, determines execution order based on the order field, and manages the current and previous GlideRecord objects that scripts use to access field values. The engine maintains a transaction context that ensures all before rules complete before the database write occurs, then executes after rules once the record is committed. This transactional integrity is what allows before rules to prevent database writes by calling setAbortAction(true) and enables after rules to perform operations that depend on the record existing in the database with its final sys_id.

Without Business Rules, you cannot reliably enforce data validation, automatically populate fields based on complex logic, or trigger workflows that must execute regardless of data source. Client Scripts can validate data from form submissions, but they're powerless against API calls, data imports, or scheduled jobs that bypass the UI entirely. Workflow conditions can trigger processes, but they lack the granular field-level access and immediate execution timing that Business Rules provide. Data Policies handle basic field requirements, but they can't implement complex calculations, external system integration, or conditional logic that spans multiple tables.

ServiceNow administrators typically use Business Rules for basic field population and data validation that doesn't require complex scripting. Developers implement sophisticated business logic, integrations with external systems, and performance-critical operations that require careful optimization. System architects design Business Rule strategies that balance functionality with performance, often establishing governance around execution order and database query patterns to prevent cascading performance issues. The most experienced architects know that Business Rules are where good ServiceNow implementations succeed and poor ones fail spectacularly.

Business Rules work closely with several adjacent platform concepts but serve distinct purposes. While Client Scripts handle immediate user feedback and form validation, Business Rules enforce the same logic server-side where it cannot be bypassed. Script Includes provide reusable functions that Business Rules can call, allowing complex logic to be centralized and unit tested separately from the rule triggers. Workflows orchestrate multi-step processes over time, but Business Rules handle the immediate data transformations and validations that workflows often depend on to function correctly.

How It Works Under the Hood

When a database operation begins, ServiceNow's Business Rule engine queries the sys_script table to find all active rules that match the target table and operation type. The engine evaluates each rule's condition script (if present) in order of the order field, creating execution queues for before and after rules separately. For before rules, the engine provides both current and previous GlideRecord objects where changes made to current will be saved to the database, while previous contains the original values for comparison.

The execution context includes access to system APIs that are unavailable client-side, including gs.log() for server-side logging, gs.getProperty() for system properties, and the full GlideRecord API for database operations. What many developers don't realize is that Business Rules execute within the same transaction as the triggering database operation, meaning database queries within before rules see uncommitted changes from other before rules, while queries in after rules see the final committed state. This transaction boundary is critical for understanding why certain operations work in after rules but fail in before rules.

ServiceNow also maintains a Business Rule cache that stores compiled rule definitions to avoid repeatedly parsing rule conditions and scripts. This cache invalidates when rules are modified, but it means that the first execution after a rule change may have different performance characteristics than subsequent executions. The platform tracks rule execution statistics and provides debugging information through the Business Rule - Debug system property, though enabling debug mode significantly impacts performance and should never be used in production for extended periods.

The Request Lifecycle

  1. Database operation begins (insert, update, delete, or display/query) triggering the Business Rule engine evaluation
  2. Engine queries sys_script table for active rules matching the target table, operation type, and applies table inheritance
  3. Rules are sorted by order field and filtered by condition scripts, creating separate execution queues for before/after rules
  4. Before rules execute sequentially with access to current and previous GlideRecord objects, changes to current affect the database write
  5. If no before rule calls setAbortAction(true), the database operation commits with any modifications from before rules
  6. After rules execute with current containing the committed record state and previous containing pre-transaction values
  7. Transaction completes and control returns to the calling code (UI, API, import job, etc.)
Free Newsletter

Enjoying this? Get one deep-dive per week.

Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.

No spam · Unsubscribe anytime

The Core Pattern

Client Script — Incident Form.js
function onSubmit() {
    // Client-side validation before form submission
    // This catches obvious errors immediately for better UX
    var priority = g_form.getValue('priority');
    var category = g_form.getValue('category');
    
    if (priority == '1' && !category) {
        g_form.addErrorMessage('Critical incidents require a category');
        return false; // Prevents form submission
    }
    
    // Even if this passes, server-side Business Rule will re-validate
    // Client scripts can be bypassed by API calls, imports, etc.
    return true;
}
Business Rule — Incident Validation (Before).js
(function executeRule(current, previous /*null when async*/) {
    // Server-side validation that CANNOT be bypassed
    // Runs for UI submissions, API calls, imports - everything
    
    // Access field values from the current record
    var priority = current.getValue('priority');
    var category = current.getValue('category');
    
    if (priority == '1' && gs.nil(category)) {
        // Set error message and prevent database write
        gs.addErrorMessage('Critical incidents require a category');
        current.setAbortAction(true);
        return; // Stop processing this rule
    }
    
    // Automatically populate fields based on business logic
    if (priority == '1' && gs.nil(current.escalation)) {
        current.escalation = '0'; // Auto-escalate critical incidents
    }
    
})(current, previous);

Real-World Scenarios

Automatic SLA Assignment Based on Customer Tier

Premium customers need faster response times than standard customers, but the SLA must be set before the incident is saved to ensure proper task creation. A before Business Rule can look up the customer's contract tier and automatically assign the appropriate SLA definition.

Business Rule — Auto SLA Assignment (Before).js
(function executeRule(current, previous) {
    // Only process new incidents or when customer changes
    if (current.isNewRecord() || current.caller_id.changes()) {
        
        var customerGR = new GlideRecord('sys_user');
        if (customerGR.get(current.caller_id)) {
            // Check the customer's account for tier information
            var accountGR = new GlideRecord('customer_account');
            accountGR.addQuery('customer', customerGR.sys_id);
            accountGR.query();
            
            if (accountGR.next()) {
                // Assign SLA based on customer tier - before record saves
                var slaName = accountGR.getValue('tier') == 'premium' ? 'Premium Incident SLA' : 'Standard Incident SLA';
                current.setValue('sla_due', slaName);
                
                gs.log('Auto-assigned SLA: ' + slaName + ' for customer: ' + customerGR.getDisplayValue());
            }
        }
    }
})(current, previous);

Watch for infinite loops if the SLA assignment triggers other Business Rules that modify the same incident. Consider using autoSysFields(false) when updating related records to prevent unnecessary rule re-execution. The changes() method prevents the rule from running unnecessarily when other fields update.

When a Change Request is approved, all related Change Tasks should automatically move to the Work in Progress state to notify assignees that implementation can begin. An after Business Rule ensures this happens after the Change Request status is committed to the database.

Business Rule — Cascade Change Approval (After).js
(function executeRule(current, previous) {
    // Only when status changes to approved
    if (current.state.changes() && current.getValue('state') == '3') {
        
        var taskGR = new GlideRecord('change_task');
        taskGR.addQuery('change_request', current.sys_id);
        taskGR.addQuery('state', '1'); // Only pending tasks
        taskGR.query();
        
        var updatedCount = 0;
        while (taskGR.next()) {
            taskGR.setValue('state', '2'); // Work in Progress
            taskGR.setValue('work_notes', 'Change Request approved - implementation authorized');
            
            // Prevent this update from triggering Business Rules unnecessarily
            taskGR.autoSysFields(false);
            taskGR.setWorkflow(false);
            taskGR.update();
            updatedCount++;
        }
        
        gs.log('Updated ' + updatedCount + ' change tasks for CR: ' + current.number);
    }
})(current, previous);

After rules are essential here because the Change Request must be committed before updating related records that reference it. Using setWorkflow(false) prevents workflow rules from triggering during the batch update, improving performance. Always log the number of affected records for troubleshooting cascade operations.

External System Integration with Error Handling

When high-priority incidents are created, they need to trigger alerts in an external monitoring system, but the ServiceNow transaction shouldn't fail if the external system is unavailable. An after Business Rule with proper error handling ensures reliable integration.

Business Rule — External Alert Integration (After).js
(function executeRule(current, previous) {
    // Trigger external alert for new high-priority incidents
    if (current.isNewRecord() && current.getValue('priority') <= '2') {
        
        try {
            var restMessage = new sn_ws.RESTMessageV2('External Monitoring API', 'POST');
            restMessage.setStringParameter('incident_number', current.getValue('number'));
            restMessage.setStringParameter('priority', current.getDisplayValue('priority'));
            restMessage.setStringParameter('short_description', current.getValue('short_description'));
            
            var response = restMessage.execute();
            
            if (response.getStatusCode() == 200) {
                gs.log('External alert created for incident: ' + current.number);
                current.setValue('u_external_alert_sent', 'true');
                current.update(); // Safe because this is after rule
            } else {
                gs.error('External API error for ' + current.number + ': ' + response.getBody());
            }
            
        } catch (ex) {
            // Never let external integration failures break ServiceNow transactions
            gs.error('External alert failed for ' + current.number + ': ' + ex.getMessage());
        }
    }
})(current, previous);
⚠️

Never call current.update() within a before Business Rule - it creates infinite recursion. After rules can safely update the current record because they won't retrigger themselves.

The Classic Mistake

⚠️

Making database queries inside business rules without checking if you're already inside a database operation.

Anti-pattern — Do Not Use This.js
(function executeRule(current, previous) {
    // This creates infinite recursion
    var gr = new GlideRecord('incident');
    gr.addQuery('assigned_to', current.assigned_to);
    gr.query();
    
    var count = 0;
    while (gr.next()) {
        count++;
        // Update other incidents - triggers more business rules
        gr.work_notes = 'Updated by assignment rule';
        gr.update(); // DANGER: This triggers business rules again
    }
    
    current.u_related_incidents = count;
    
})(current, previous);

This code creates infinite recursion because the gr.update() call inside the loop triggers business rules on those records, which may trigger this same rule again. ServiceNow's script engine will eventually kill the transaction with a "Maximum execution time exceeded" error in the System Log, but not before consuming massive server resources. The user sees a generic "An error occurred while processing your request" message, and the original record update fails completely. Internally, ServiceNow is spawning dozens or hundreds of business rule executions that stack up until the script runner hits its safety limits.

The Fix.js
(function executeRule(current, previous) {
    // Use autoSysFields(false) and setWorkflow(false) to prevent recursion
    var gr = new GlideRecord('incident');
    gr.addQuery('assigned_to', current.assigned_to);
    gr.addQuery('sys_id', '!=', current.sys_id); // Don't include current record
    gr.query();
    
    var count = 0;
    while (gr.next()) {
        count++;
        gr.work_notes = 'Updated by assignment rule';
        gr.autoSysFields(false); // Prevent sys_updated_on changes
        gr.setWorkflow(false);   // Skip business rules and workflows
        gr.update();
    }
    
    current.u_related_incidents = count;
    
})(current, previous);
💡

Always use setWorkflow(false) when updating other records from within a business rule to prevent infinite recursion.

Performance Rules

  1. Never call GlideRecord.query() without addQuery() - table scans over 10,000 records cause 30+ second timeouts and sys admin alerts.
  2. Use GlideAggregate instead of GlideRecord for counting - looping through 500+ records to count them will cause browser timeouts.
  3. Limit while(gr.next()) loops to 100 iterations max using gr.setLimit(100) - unbounded loops crash the transaction and require database connection pool recovery.
  4. Never use getDisplayValue() on reference fields inside loops - each call requires a separate database lookup causing exponential slowdown.
  5. Use current.changes() in 'before update' rules to avoid unnecessary processing - business rules fire on every field change, including automated ones.
  6. Avoid gs.eventQueue() calls inside loops - each event requires a separate database insert to sysevent table causing transaction timeouts over 50 events.
  7. Check table size before writing business rules on sys_audit, sys_journal_field, or syslog - these high-volume tables cause immediate performance degradation.
  8. Use condition builder instead of scripted conditions when possible - JavaScript evaluation is 10x slower than database query conditions.

Side Effects & Platform Behavior

  • All business rules fire workflows, notifications, and other business rules unless explicitly disabled with setWorkflow(false) - creating cascading automation chains.
  • Business rules write to sys_audit table on every field change and create entries in sys_update_xml when modified.
  • 'Display business rules' run on every form load and list view, making them visible in browser network tab as separate AJAX calls.
  • ACL evaluation happens after 'before' business rules but before 'after' business rules, potentially blocking operations your business rule depends on.
  • Business rule errors appear in System Log → All and terminate the entire transaction, rolling back all database changes including the triggering record.
  • Import sets, REST API calls, and web service integrations bypass business rules by default unless ignore_update_business_rules is set to false.
  • Business rules trigger on child table records when parent table rules match, creating unexpected behavior on extended tables like incident extending task.
  • Mass updates through list views, scheduled jobs, and data import trigger business rules individually for each record, not as a bulk operation.
  • 'Async' business rules write to syslog_transaction and sysevent tables and execute outside the original transaction context.
  • Global business rules appear in all application scopes and run regardless of the current application context, potentially causing cross-scope data pollution.

Debugging When It Breaks

When business rules fail, users typically see either a generic "An error occurred while processing your request" message or the form simply doesn't save with no feedback. Developers see JavaScript errors in the browser console for display business rules, but server-side business rule failures are completely invisible to the browser. The most frustrating symptom is the "silent failure" where the form appears to save successfully but the business rule logic never executed.

Always start debugging in System Logs → All, filtered by time range when the problem occurred. Look for entries with "Business Rule" in the source field or "RhinoException" in the message. Script syntax errors show as "JavaScriptException" with line numbers. Database query errors appear as "SQLException" with the actual database error message. For display business rules, check the browser console Network tab for failed AJAX calls to xmlhttp.do endpoints.

Enable business rule debugging by navigating to the specific business rule record and checking the Debug checkbox, then reproduce the issue. This creates detailed execution logs in System Logs → Script Debugger showing variable values and execution flow. Performance problems show up as "Script execution time exceeded" errors, while infinite recursion creates dozens of identical log entries with incrementing execution depths.

Quick diagnostic checklist:

  • Check business rule conditions and table filters - they might not match your test record
  • Verify the business rule is Active and not filtered by Update Set
  • Confirm your test user has write access to all fields the business rule modifies
  • Look for order conflicts with other business rules on the same table and timing

Quick Reference

  • 'Before' rules can modify current object directly, 'after' rules require current.update() to save changes
  • Business rules execute in order value ascending (100, 200, 300) within same timing (before/after)
  • Use current.operation() to detect insert vs update vs delete instead of checking current.isNewRecord()
  • 'Display' business rules run on form load and must return strings, not modify the current object
  • Global business rules affect all tables - use table-specific rules unless you truly need global scope
  • REST API and Integration Hub ignore business rules by default - enable with ignore_update_business_rules=false
  • Use gs.nil() not == null to check for empty ServiceNow field values
  • 'Async' business rules lose transaction context - current and previous objects are read-only snapshots
  • Business rule conditions use database syntax (field=value) not JavaScript - use script field for complex logic
  • Delete business rules fire before record removal - current object still contains all field values