What It Is

Background Scripts are server-side JavaScript executables that run once with full administrative privileges against your ServiceNow instance database. Unlike Business Rules or Script Includes that fire automatically based on database events or application logic, Background Scripts execute immediately when you manually trigger them through the platform interface. They solve the critical problem of needing to perform one-time data operations, test complex logic, or execute maintenance tasks that would be impossible or inappropriate to handle through standard application functionality.

Architecturally, Background Scripts live within the System Definition application module and execute in the global application scope by default, though you can switch scope before execution. They run in the same server-side Rhino JavaScript engine that powers all ServiceNow server scripting, with full access to the GlideSystem, GlideRecord, and GlideAggregate APIs. The execution happens synchronously on a single application node, making them suitable for operations that need immediate completion and verification.

The underlying execution environment treats Background Scripts as privileged operations that bypass normal Access Control Rules, Business Rules, and UI Policies during database operations. This means your script runs with the same permissions as the admin user regardless of your actual user role, allowing you to read, write, or delete records across any table. The script execution is logged in the System Logs and Script Execution History, providing an audit trail of what ran and when.

You cannot function without Background Scripts when performing data migration cleanups, testing complex GlideRecord queries before implementing them in production code, or executing emergency fixes that need to bypass normal application workflows. They're essential for situations like updating thousands of records based on complex business logic, testing Script Include functionality in isolation, or performing one-time data transformations during upgrades. Unlike Update Sets or Business Rules, Background Scripts don't get packaged or deployed—they're meant for immediate, local execution against your current instance state.

Platform owners and system administrators typically manage Background Script access through the admin role requirement, while developers use them for testing and data manipulation during development cycles. The relationship is typically controlled—most implementations restrict Background Script access to senior developers and administrators because of the potential for data corruption or performance impact. Some organizations create custom roles with script_background table access for specific users who need this functionality without full admin privileges.

Recent ServiceNow releases haven't fundamentally changed Background Script behavior, but Vancouver and later versions improved the execution history tracking and added better error logging in the Script Execution History module. The Xanadu release enhanced the script editor interface with better syntax highlighting and auto-completion, making it easier to write complex scripts without switching to an external editor. However, the core execution model and API access patterns remain consistent across recent releases.

Where to Find and Configure It

Navigate to System Definition > Scripts - Background to access the primary Background Script execution interface where you write, test, and run your JavaScript code. From Studio, access Background Scripts through Create Application File > Server Development > Background Script though this creates a reusable script file rather than executing immediately. The execution history lives at System Definition > Script Execution History where you can review past executions, their duration, and any output or errors.

Background Scripts execute in Global scope by default, but you can change the application scope using the scope picker in the top-right corner of the interface before running your script. The underlying table script_background stores saved background scripts when you use the Save as functionality, and you can access this table directly at script_background.list to manage saved scripts. App Engine Studio provides access through the Logic and automation > Scripts section for scoped application development.

How It Works Step by Step

Background Scripts execute immediately when you click Run script, running synchronously on the current application server node with full database access. The platform creates a temporary execution context with administrative privileges, loading the complete ServiceNow server-side API including GlideRecord, GlideSystem, GlideAggregate, and all available Script Includes within the selected scope. Unlike scheduled scripts or Business Rules, there's no event-driven trigger—the execution is immediate and blocking until completion or timeout.

The execution bypasses normal database security constraints, meaning ACLs, Business Rules, and UI Policies don't automatically fire during GlideRecord operations unless explicitly enabled through setWorkflow(true) calls. The script runs within a transaction scope that can be committed or rolled back, and any output from gs.print() or gs.log() statements gets captured and displayed in the output section below the script editor. Error handling follows standard JavaScript patterns, with unhandled exceptions terminating script execution and logging details to both the output panel and system logs.

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 Execution Order

  1. Platform validates user has admin role or specific script_background table access rights
  2. Current application scope context is captured (Global by default, or whatever scope is selected)
  3. Server-side Rhino JavaScript engine initializes with full ServiceNow API access and admin privileges
  4. Script code executes line by line with database operations running without ACL enforcement by default
  5. Output from gs.print() and gs.log() is captured and queued for display
  6. Execution completes successfully or terminates on error, with results logged to Script Execution History

The most common Background Script pattern involves querying records, processing them with business logic, and updating fields while providing progress feedback:

typical-background-script.js
// Query incidents created in last 30 days without resolution codes
var gr = new GlideRecord('incident');
gr.addQuery('opened_at', '>=', gs.daysAgoStart(30));
gr.addQuery('close_code', '');
gr.addQuery('state', '7'); // Closed state
gr.query();

var updateCount = 0;
while (gr.next()) {
    // Apply business logic to determine resolution code
    if (gr.getValue('category') == 'hardware') {
        gr.setValue('close_code', 'Solved (Permanently)');
    } else {
        gr.setValue('close_code', 'Solved (Work Around)');
    }
    
    gr.update();
    updateCount++;
    
    // Progress indicator every 100 records
    if (updateCount % 100 == 0) {
        gs.print('Updated ' + updateCount + ' incident records so far...');
    }
}

gs.print('COMPLETED: Updated ' + updateCount + ' total incident records');

Real-World Scenarios

Fixing Orphaned Assignment Group References After Department Merger

After a department merger, 2,400 active tickets reference assignment groups that were deactivated, causing workflow failures and preventing proper escalation. The business needs these tickets reassigned to the new consolidated groups immediately to restore normal operations.

fix-orphaned-assignments.js
// Map old assignment group sys_ids to new ones
var groupMapping = {
    '1234567890abcdef1234567890abcdef': '9876543210fedcba9876543210fedcba', // IT Hardware -> IT Operations
    '2345678901bcdef12345678901abcdef': '9876543210fedcba9876543210fedcba', // IT Software -> IT Operations
    'abcdef1234567890abcdef1234567890': '8765432109edcba98765432109edcba9'  // Network Team -> Infrastructure
};

var totalUpdated = 0;
for (var oldGroupId in groupMapping) {
    var gr = new GlideRecord('incident');
    gr.addQuery('assignment_group', oldGroupId);
    gr.addQuery('state', 'NOT IN', '6,7,8'); // Exclude resolved/closed/cancelled
    gr.query();
    
    while (gr.next()) {
        gr.setValue('assignment_group', groupMapping[oldGroupId]);
        gr.setValue('assigned_to', ''); // Clear individual assignment
        gr.update();
        totalUpdated++;
    }
}

gs.print('Successfully reassigned ' + totalUpdated + ' active incidents to new groups');

Watch for Business Rules that might fire on assignment group changes—you may need gr.setWorkflow(false) to prevent unwanted notifications or state transitions. Always verify the new assignment groups are active and have proper role assignments before running. Consider running a test query with gr.getRowCount() first to confirm you're affecting the expected number of records.

Testing Complex GlideAggregate Logic Before Production Deployment

You're developing a dashboard widget that needs to count incidents by priority and assignment group, but the GlideAggregate query logic is complex and needs validation against real data. Testing this in a Business Rule or Script Include risks breaking production functionality if the logic is flawed.

test-aggregate-logic.js
// Test complex aggregate query for dashboard widget
var ga = new GlideAggregate('incident');
ga.addQuery('opened_at', '>=', gs.daysAgoStart(30));
ga.addQuery('assignment_group.active', true);
ga.addQuery('state', 'NOT IN', '6,7,8');
ga.groupBy('priority');
ga.groupBy('assignment_group');
ga.addAggregate('COUNT');
ga.query();

var results = {};
while (ga.next()) {
    var priority = ga.getDisplayValue('priority');
    var groupName = ga.getDisplayValue('assignment_group');
    var count = ga.getAggregate('COUNT');
    
    if (!results[priority]) {
        results[priority] = {};
    }
    results[priority][groupName] = parseInt(count);
    
    gs.print('Priority: ' + priority + ', Group: ' + groupName + ', Count: ' + count);
}

// Verify data structure matches widget expectations
gs.print('\nFinal results object: ' + JSON.stringify(results, null, 2));

GlideAggregate performance can degrade quickly with large datasets—monitor execution time in the script output and consider adding more restrictive filters if needed. Test with different date ranges to ensure the query performs acceptably under various conditions. The getDisplayValue() calls will resolve reference fields which adds database overhead, so switch to getValue() if you only need sys_ids for the final implementation.

Emergency Data Cleanup After Failed Import

A CSV import created 5,000 duplicate user records with malformed email addresses, and the standard UI mass delete would timeout. You need to identify and remove these specific records immediately before they cause authentication issues or appear in user selection lists.

emergency-cleanup.js
// Find and delete duplicate users created by failed import
// Identify by malformed email pattern and recent creation date
var gr = new GlideRecord('sys_user');
gr.addQuery('email', 'CONTAINS', '@com@'); // Malformed pattern from bad import
gr.addQuery('sys_created_on', '>=', '2024-01-15 14:00:00'); // After import timestamp
gr.addQuery('last_login_time', ''); // Never logged in
gr.query();

var deleteCount = 0;
var duplicates = [];

while (gr.next()) {
    // Verify this is actually a duplicate before deletion
    var realUser = new GlideRecord('sys_user');
    realUser.addQuery('email', gr.getValue('email').replace('@com@', '@com'));
    realUser.query();
    
    if (realUser.hasNext()) {
        duplicates.push(gr.getValue('sys_id'));
        gs.print('Found duplicate: ' + gr.getDisplayValue('name') + ' (' + gr.getValue('email') + ')');
    }
}

// Delete confirmed duplicates
for (var i = 0; i < duplicates.length; i++) {
    var deleteGr = new GlideRecord('sys_user');
    if (deleteGr.get(duplicates[i])) {
        deleteGr.deleteRecord();
        deleteCount++;
    }
}

gs.print('\nDeleted ' + deleteCount + ' duplicate user records');

Always run a verification query first to ensure you're targeting the right records—consider commenting out the actual deleteRecord() calls on the first execution to review what would be deleted. User deletions can cascade to other tables and break referential integrity, so check for related records in groups, roles, or approval histories. The script bypasses normal deletion Business Rules, so any cleanup logic that normally fires won't execute unless you add gr.setWorkflow(true) before the delete operations.

The Classic Mistake

⚠️

Running bulk operations without batch processing or progress tracking, causing timeouts and partial data corruption.

BAD_bulk_update.js
// BAD: Processing all records at once
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.query();

while (gr.next()) {
    // Complex processing that takes 2-3 seconds per record
    var relatedTasks = new GlideRecord('task');
    relatedTasks.addQuery('parent', gr.sys_id);
    relatedTasks.query();
    
    while (relatedTasks.next()) {
        relatedTasks.setValue('priority', gr.priority);
        relatedTasks.update();
    }
    
    gr.setValue('u_processed', true);
    gr.update();
    gs.log('Processed incident: ' + gr.number);
}

This approach fails because background scripts have a 5-minute execution limit, and processing stops abruptly when the timeout hits. You'll see a generic "Script execution cancelled" message with no indication of how many records were processed. ServiceNow kills the transaction mid-execution, leaving some records updated and others untouched, creating inconsistent data states. The script appears to run successfully in the execution history, but the timeout isn't logged as an error, making the partial failure non-obvious.

GOOD_batch_processing.js
// GOOD: Batch processing with progress tracking
var batchSize = 100;
var startTime = new GlideDateTime();
var processed = 0;
var maxRunTime = 240000; // 4 minutes in milliseconds

var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.addQuery('u_processed', false);
gr.setLimit(batchSize);
gr.query();

while (gr.next() && (new Date().getTime() - startTime.getNumericValue()) < maxRunTime) {
    var relatedTasks = new GlideRecord('task');
    relatedTasks.addQuery('parent', gr.sys_id);
    relatedTasks.query();
    
    while (relatedTasks.next()) {
        relatedTasks.setValue('priority', gr.priority);
        relatedTasks.update();
    }
    
    gr.setValue('u_processed', true);
    gr.update();
    processed++;
}

gs.log('Processed ' + processed + ' incidents in batch. Re-run to continue.');
💡

Always process records in batches of 50-200 with a runtime check every iteration — if your script takes longer than 3 minutes to complete, it needs batching.

When to Use This vs Alternatives

Background Scripts are the right choice for one-time data fixes, testing API calls, and emergency production changes that can't wait for a scheduled job. Use them when you need immediate execution with full admin privileges and don't need the operation to repeat.

Choose Background Scripts When

You need immediate execution for data cleanup, testing integrations, or fixing production issues where Scheduled Script Execution is too slow and Business Rules would create unwanted side effects. Background scripts bypass all business logic and run with elevated privileges that scheduled jobs can't match. They're perfect for bulk updates where you need to skip workflow, notifications, and validation rules.

Use Scheduled Script Execution Instead When

The operation needs to run repeatedly, takes longer than 4 minutes, or processes more than 1000 records. Scheduled jobs have no timeout limits and can be configured to run automatically. They also provide better error handling, job queue management, and don't consume your browser session while running.

Use Both Together When

You need to test and debug the logic immediately with a background script, then deploy the same code as a scheduled job for production execution. This approach lets you validate the script against live data, check performance, and verify results before committing to an automated schedule. Always prototype complex data operations as background scripts first.

Platform Interactions & Side Effects

  • Business Rules, ACLs, and Client Scripts are completely bypassed when using setWorkflow(false) on GlideRecord operations
  • All database changes are logged to sys_audit table with the admin user as the source, not the script name
  • Script execution history is stored in sys_script_execution_history with full script source code and output logs
  • Update Sets capture background script runs but not as transferable payloads — scripts must be manually recreated on target instances
  • Email notifications triggered by record updates will still fire unless explicitly disabled with gs.eventQueue('',null,null,true)
  • Memory consumption is not garbage collected during execution, causing browser slowdown on scripts processing thousands of records
  • Session timeout is suspended while script executes, but the browser tab becomes unresponsive during long-running operations
  • Database locks can occur on heavily updated tables, causing form saves and other operations to queue or timeout
  • Transform maps and Import Sets ignore background script changes to staging tables — they maintain their own transaction context
  • Script output is limited to 1MB and truncates silently, making large log outputs incomplete without warning

Debugging and Troubleshooting

The most common failure symptoms include scripts that appear to run successfully but produce no results, partial data updates with no error messages, and browser tabs that freeze during execution. Users typically see "Script execution cancelled" messages or background scripts that complete instantly with empty output logs. These symptoms usually indicate timeout issues, insufficient privileges on target records, or logic errors in query conditions.

Check System Logs > System Log > All for detailed error messages that don't appear in script output, particularly ACL violations and database constraint errors. The sys_script_execution_history table shows execution duration — anything under 500ms for data operations usually indicates a query returned zero results. Look for "ReferenceViolation" and "IllegalArgumentException" messages in the application logs, which indicate foreign key constraints and invalid GlideRecord operations respectively.

Enable debug logging by adding gs.log() statements at every major step, including record counts, query results, and variable values before database operations. Watch for "java.lang.OutOfMemoryError" in node logs when processing large datasets, and "Transaction timeout" messages that appear 5 minutes after script start. Use gr.getRowCount() to verify your queries return expected results before processing loops.

Diagnostic Checklist:

  • Check sys_script_execution_history for execution duration and output length
  • Verify query conditions by running gr.getRowCount() before any while loops
  • Review System Logs > Application Logs for ACL and constraint violations
  • Test with a single record first using gr.setLimit(1) to isolate logic issues
  • Check node log files for memory and timeout errors during long operations
  • Verify field names and table access by testing individual gr.getValue() operations
  • Add runtime monitoring with new Date().getTime() checks every 100 iterations

Quick Reference

  • Hard timeout limit of 5 minutes (300 seconds) with no warning — scripts stop mid-execution
  • Output is truncated at 1MB without notification — large logs silently cut off
  • Scripts run in admin context but still respect table-level ACLs on some system tables
  • GlideRecord operations with setWorkflow(false) bypass business rules but still trigger notifications
  • Maximum 50 concurrent background scripts per instance — additional attempts queue
  • Script source and results stored in sys_script_execution_history for 30 days by default
  • Cannot access UI elements, g_form, g_list, or client-side objects — server-side only
  • Memory usage compounds during loops — processing 10,000+ records can cause browser crashes
  • Database changes appear instantly without commit statements — no transaction rollback capability
  • REST API calls inherit the executing user's session and can modify authentication state