What It Is
Script Actions are server-side JavaScript functions that execute in response to Events being fired within the ServiceNow platform. Unlike Business Rules which are tightly coupled to database operations on specific tables, Script Actions provide a more flexible event-driven architecture where any code anywhere can fire a named event, and multiple Script Actions can respond to that same event through Event Registrations. This decouples the event producer from the event consumer, creating a publish-subscribe pattern that scales better than direct method calls or tightly coupled Business Rules.
Architecturally, Script Actions execute entirely on the server-side within the ServiceNow application server context, never in the browser. When an Event is fired using gs.eventQueue() or gs.eventQueueScheduled(), the event gets queued in the sysevent table and processed by background event processing threads. Script Actions run in this background context, which means they have access to server-side APIs but cannot directly manipulate the user interface or client-side state. They're part of ServiceNow's application layer, sitting between the data layer (where Business Rules operate) and the presentation layer (where Client Scripts and UI Actions operate).
The underlying execution mechanism starts when ServiceNow's event processor picks up queued events from the sysevent table and looks up matching Event Registrations. Each Event Registration links an event name to a Script Action, potentially with additional filtering conditions based on the event parameters. The platform creates a new execution context for each Script Action, injects the event parameters as global variables, and executes the script code. This happens asynchronously from the original event firing, which means Script Actions don't block the user's transaction but also can't return values back to the calling code.
Without Script Actions, you cannot implement truly decoupled event-driven architectures within ServiceNow. Business Rules force you into table-specific coupling where the triggering table must know about all its downstream dependencies. Script Actions break this coupling by allowing any code to fire a named event without knowing or caring what responds to it. This becomes critical in complex integrations where multiple systems need to react to the same business event, or when building modular applications where components should remain loosely coupled. You also cannot achieve the same level of processing flexibility since Script Actions can be conditionally registered based on event parameters, while Business Rules are always table-centric.
Developers primarily use Script Actions when building integration platforms, workflow orchestration systems, or any application requiring event-driven architecture patterns. System administrators typically avoid them since they require understanding of the Event/Script Action/Event Registration relationship and JavaScript programming. Enterprise architects leverage them heavily in large implementations where system decoupling and scalability matter more than administrative simplicity. They're particularly valuable in scoped applications where clean interfaces between applications are critical, and in environments with heavy customization where Business Rule proliferation becomes a performance and maintenance nightmare.
Script Actions relate closely to Business Rules as both provide server-side event-driven processing, but Business Rules are synchronous and table-coupled while Script Actions are asynchronous and event-coupled. They connect to Workflow Activities since many workflows fire events that Script Actions can respond to, creating hybrid declarative-programmatic processing chains. Script Actions also pair naturally with Import Sets and Transform Maps, where the transformation process can fire events that Script Actions use to trigger downstream processing, notifications, or integrations without cramming everything into transform scripts that become unmaintainable.
How It Works Under the Hood
When code calls gs.eventQueue('event.name', gr, parm1, parm2), ServiceNow immediately creates a record in the sysevent table containing the event name, any GlideRecord reference, and up to five custom parameters. The calling transaction continues without waiting. Background event processing threads continuously poll the sysevent table for unprocessed events. When they find one, they query the sysevent_register table to find all Event Registrations matching the event name. Each matching registration points to a Script Action in the sysevent_script_action table.
The event processing thread creates a new JavaScript execution context for each Script Action, then injects several global variables that most developers don't realize are available. The event variable contains a GlideRecord pointing to the current sysevent record. The current variable holds a GlideRecord to the original record if one was passed to gs.eventQueue(). Variables parm1 through parm5 contain any additional parameters passed during event firing. Most critically, all server-side APIs are available including gs, GlideRecord, GlideSystem, and any Script Includes, but no client-side APIs since this executes in a background server context.
The Event Processing Lifecycle
- Code executes
gs.eventQueue('incident.assigned', gr, 'urgent')in any server-side context (Business Rule, Script Include, etc.) - ServiceNow creates a
syseventrecord with state 'Ready', event name 'incident.assigned', table/record reference, and parm1='urgent' - The calling code continues execution immediately without waiting for event processing
- Background event processor threads (separate from user sessions) poll
syseventfor Ready events every few seconds - Event processor queries
sysevent_registertable for all registrations matching event name 'incident.assigned', evaluating any condition scripts - For each matching Event Registration, processor creates new JavaScript context and injects global variables (
event, current, parm1, parm2, etc.) - Script Action code executes with full server-side API access but no user session context
- After all Script Actions complete,
syseventrecord state changes to 'Processed' or 'Error' depending on execution results
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Core Pattern
(function executeRule(current, previous) {
// Only fire event when assignment actually changes
if (current.assigned_to.changes()) {
// Fire event with current record and priority as parameter
// This decouples assignment logic from downstream processing
gs.eventQueue(
'incident.assigned',
current, // Makes 'current' available in Script Action
current.getValue('priority'), // Available as parm1
current.getValue('assignment_group') // Available as parm2
);
// Log the event firing for debugging
gs.info('Fired incident.assigned event for INC' + current.number);
}
})(current, previous);// Global variables automatically injected by event processor:
// event = GlideRecord to sysevent record
// current = GlideRecord to incident that triggered the event
// parm1 = priority value
// parm2 = assignment_group value
// Validate we have the data we expect
if (!current || !current.isValid()) {
gs.error('Script Action received invalid current record');
return;
}
// Send notification to assigned user
var notification = new GlideEmailOutbound();
notification.setSubject('Incident ' + current.number + ' assigned to you');
notification.setBody('Priority: ' + parm1 + '\nAssignment Group: ' + parm2);
notification.setRecipient(current.assigned_to);
notification.send();
// Update assignment metrics
var metricsUtil = new IncidentMetrics();
metricsUtil.recordAssignment(current.sys_id, parm1);
gs.info('Processed assignment for incident ' + current.number);Script Actions execute asynchronously in background threads. They cannot return values to the calling code or directly manipulate the user interface. Always validate that injected global variables are populated before using them.
Real-World Scenarios
Multi-System Change Request Approval
When a high-risk change request gets approved, multiple external systems need immediate notification without blocking the approval workflow. The approval process fires an event that triggers parallel integrations to update external change management systems, security scanning tools, and deployment orchestration platforms.
(function executeRule(current, previous) {
// Only trigger when approval state moves to approved
if (current.approval == 'approved' && previous.approval != 'approved') {
// Fire event for any interested systems
gs.eventQueue(
'change_request.approved',
current,
current.getValue('risk'), // parm1: risk level
current.getValue('start_date'), // parm2: planned start
current.getValue('category') // parm3: change category
);
gs.info('Change approval event queued for ' + current.number);
}
})(current, previous);// Update external change management system via REST
var request = new sn_ws.RESTMessageV2();
request.setEndpoint('https://external-cm.company.com/api/changes');
request.setHttpMethod('POST');
// Build payload from event data
var payload = {
servicenow_id: current.sys_id.toString(),
number: current.number.toString(),
risk_level: parm1, // Risk level from event
start_date: parm2, // Planned start from event
category: parm3, // Category from event
status: 'approved'
};
request.setRequestBody(JSON.stringify(payload));
request.setRequestHeader('Content-Type', 'application/json');
try {
var response = request.execute();
if (response.getStatusCode() == 200) {
gs.info('Successfully notified external system of change approval: ' + current.number);
} else {
gs.error('External system notification failed: ' + response.getStatusCode());
}
} catch (ex) {
gs.error('Exception during external system integration: ' + ex.getMessage());
}Watch out for external system timeouts affecting event processing performance. Always wrap REST calls in try-catch blocks and consider using scheduled events (gs.eventQueueScheduled()) for non-critical integrations. The asynchronous nature means approval workflows continue even if external notifications fail.
Service Catalog Request Fulfillment Orchestration
Complex service catalog requests require coordination between multiple fulfillment teams without creating tight coupling between catalog items and fulfillment processes. Each approved catalog request fires events that multiple specialized Script Actions can respond to based on request type, urgency, and requested services.
(function executeRule(current, previous) {
// Fire event when request item moves to fulfillment
if (current.state == 2 && previous.state != 2) { // State 2 = Work in Progress
// Get parent request for additional context
var request = new GlideRecord('sc_request');
if (request.get(current.request)) {
gs.eventQueue(
'catalog.fulfill_request',
current, // The specific request item
current.getValue('cat_item'), // parm1: catalog item ID
request.getValue('priority'), // parm2: request priority
request.getValue('requested_for') // parm3: user requesting
);
gs.info('Fulfillment event fired for RITM' + current.number);
}
}
})(current, previous);// Only process laptop-related catalog items
var catalogItem = new GlideRecord('sc_cat_item');
if (!catalogItem.get(parm1) || catalogItem.name.indexOf('Laptop') == -1) {
return; // Not a laptop request, ignore
}
// Get user details for provisioning
var user = new GlideRecord('sys_user');
if (!user.get(parm3)) {
gs.error('Cannot find requested user: ' + parm3);
return;
}
// Create provisioning task with high priority for urgent requests
var task = new GlideRecord('sc_task');
task.initialize();
task.request_item = current.sys_id;
task.short_description = 'Provision laptop for ' + user.getDisplayValue();
task.assignment_group = 'Hardware Fulfillment';
task.priority = (parm2 == '1') ? '1' : '3'; // Match request priority
// Add laptop-specific provisioning details
task.work_notes = 'User department: ' + user.department +
'\nLocation: ' + user.location +
'\nManager: ' + user.manager;
task.insert();
gs.info('Created laptop provisioning task: ' + task.number);Consider using Event Registration condition scripts to filter which Script Actions run rather than putting filtering logic inside the Script Action itself. This prevents unnecessary script execution and makes the event-to-handler mapping more visible in the platform. Remember that multiple Script Actions can respond to the same event, so design for parallel processing rather than sequential dependencies.
Import Data Quality and Enrichment Pipeline
When importing large datasets through Transform Maps, data quality validation and enrichment should happen after the import commits but before downstream systems get notified. The transform process fires events containing information about imported records, which Script Actions use to perform validation, enrichment, and integration handoffs without blocking the import transaction.
// At end of transform script, fire event for post-processing
// This runs after record is inserted but within same transaction
if (target.isValid()) {
// Fire event with key data for post-processing
gs.eventQueue(
'user.imported',
target, // Newly created user record
source.u_employee_id, // parm1: external employee ID
source.u_department_code, // parm2: department from source
source.u_manager_email, // parm3: manager's email
target.isNewRecord() ? 'new' : 'update' // parm4: insert vs update
);
gs.info('User import event queued for ' + target.user_name);
}// Validate and enrich imported user data
if (!current || current.getTableName() != 'sys_user') {
gs.error('Script Action expected sys_user record');
return;
}
// Validate required fields and fix common issues
if (!current.email || current.email.toString().indexOf('@') == -1) {
// Construct email from username if missing
current.email = current.user_name + '@company.com';
gs.warn('Fixed missing email for user: ' + current.user_name);
}
// Enrich with manager relationship using email from import
if (parm3) { // Manager email from source system
var manager = new GlideRecord('sys_user');
manager.addQuery('email', parm3);
manager.query();
if (manager.next()) {
current.manager = manager.sys_id;
gs.info('Linked manager for user: ' + current.user_name);
}
}
// Set department based on department code
var deptMapping = {
'IT': 'Information Technology',
'HR': 'Human Resources',
'FIN': 'Finance'
};
current.department = deptMapping[parm2] || 'General';
current.update();
gs.info('Enriched imported user: ' + current.user_name);Be careful with record updates inside Script Actions triggered by imports. The current GlideRecord may be stale if other processes modified the record between import and event processing. Always call current.get(current.sys_id) to refresh before making updates, and consider using gs.eventQueueScheduled() with a small delay for heavy enrichment operations.
The Classic Mistake
Creating Script Actions that directly manipulate GlideRecord data without considering transaction scope and timing.
// Script Action for incident.updated event
var incidentGR = new GlideRecord('incident');
if (incidentGR.get(event.parm1)) {
// Trying to modify the same record that triggered the event
incidentGR.priority = '1';
incidentGR.update();
// Also creating related records without transaction awareness
var taskGR = new GlideRecord('sc_task');
taskGR.initialize();
taskGR.request_item = incidentGR.sys_id;
taskGR.short_description = 'Auto-generated task';
taskGR.insert();
gs.info('Updated incident and created task');
}This fails because Script Actions execute after the triggering database operation has already completed, but the transaction may still be active. When you try to update the same record that triggered the event, you create a circular dependency that can cause infinite loops or transaction deadlocks. ServiceNow will log "Maximum update limit exceeded" errors in the System Log, and users will see generic "An error has occurred" messages. The platform's internal event queue becomes congested as each update triggers more events, eventually causing the entire transaction to roll back.
// Script Action for incident.updated event
// Only create related records or external actions, never modify the triggering record
if (event.parm1 && event.parm2 == 'priority') {
var incidentSysId = event.parm1;
// Use GlideSchedule to defer the update to avoid circular triggers
var scheduleScript = "var gr = new GlideRecord('incident'); if(gr.get('" + incidentSysId + "')) { gr.work_notes = 'Priority auto-escalated'; gr.update(); }";
gs.eventQueue('incident.priority_escalation', null, incidentSysId, scheduleScript);
// Or perform non-modifying actions like notifications
gs.addInfoMessage('Incident priority has been escalated');
gs.info('Queued priority escalation action for ' + incidentSysId);
}Script Actions should never modify the record that triggered them — use them for side effects, related record creation, or queuing deferred actions.
Performance Rules
- Never query more than 100 records in a Script Action using
GlideRecord.query()— events fire synchronously and will cause 30+ second page load times, triggering browser timeouts and angry sys admin escalations. - Avoid
GlideSPScriptable.executeScript()orgs.eventQueue()calls that spawn more than 5 child events — the event processing queue will back up and cause form submissions to hang indefinitely. - Script Actions containing
RESTMessageV2calls must set timeout to maximum 10 seconds — longer timeouts will block the entire event queue and cause subsequent form saves across the instance to fail with "Transaction timeout" errors. - Never use
gs.sleep()orjava.lang.Thread.sleep()in Script Actions — they consume worker threads and will cause the instance to run out of available threads, resulting in complete platform unavailability. - Keep Script Action execution time under 5 seconds by avoiding
GlideAggregatequeries on tables with over 10,000 records — they block the database connection pool and cause "Unable to get connection" errors for other users. - Limit
gs.log()andgs.info()calls to one per Script Action execution — excessive logging creates millions ofsys_logrecords that consume disk space and slow down log queries for debugging. - Disable Script Actions during bulk data imports by checking
gs.isUpgrade()— otherwise they fire for every imported record and can turn a 1-hour data load into a 12-hour system outage.
Side Effects & Platform Behavior
- Script Actions execute after all Business Rules but before Notifications, meaning changes made in Script Actions won't trigger additional sync Business Rules but will be visible to email templates and notification scripts.
- Every Script Action execution creates a record in the
sys_script_execution_historytable with execution time, parameters, and error details — this table grows rapidly in high-transaction environments. - Script Actions inherit the security context of the triggering transaction, but
gs.getUser()may return 'system' rather than the actual user if the event was fired by a scheduled job or import. - Any
GlideRecord.insert()orupdate()operations within Script Actions will trigger their own Business Rules and potentially more events, but ACL checks are bypassed unless explicitly enabled withsetWorkflow(true). - Script Action failures are logged to
sys_logbut do not roll back the triggering transaction — the original database operation succeeds even if the Script Action throws an exception. - Variables set in Script Actions using
gs.getSession().putProperty()persist for the user's entire session and can affect subsequent form loads and UI actions. - Script Actions executing during scheduled imports or data loads will not have access to
g_formor client-side context, causing JavaScript errors if client-side APIs are referenced. - When a Script Action creates new Event Registrations dynamically using
gs.eventQueue(), those events are processed in the same transaction thread but after the current Script Action completes. - Script Actions can modify attachment records on the triggering record, but file content changes require special handling through
GlideSysAttachmentAPI to avoid corrupting binary data.
Debugging When It Breaks
The most common failure symptoms are silent failures where the Script Action simply doesn't execute — users see no error messages, but expected side effects don't occur. Developers typically notice this when automation workflows break or expected records aren't created. Unlike Business Rule failures that often cause visible form errors, Script Action failures are nearly invisible to end users, making them particularly tricky to diagnose.
For JavaScript errors in Script Actions, check System Log > All filtered by 'Script Action' source — look for "ReferenceError" or "TypeError" messages that include the Script Action name and line number. For execution flow issues, navigate to System Definition > Events > Event Registration to verify the Event Registration is active and properly configured with the correct table and event name. Performance-related failures appear in the System Log as "Transaction cancelled due to timeout" or in the Performance Analytics dashboard showing slow transactions tied to specific events.
When Script Actions fail to fire at all, use this diagnostic checklist:
- Verify the Event Registration is Active and matches the exact table name (not display name)
- Check that Business Rules haven't disabled event processing with
setWorkflow(false) - Confirm the triggering operation actually commits to the database (events don't fire on rolled-back transactions)
- Look for condition scripts in the Event Registration that might be filtering out executions
- Test with a simple
gs.log('Script Action fired')to isolate execution vs. logic issues
Quick Reference
- Use
event.parm1throughevent.parm10to access event parameters —event.parm1is always the record sys_id for table events - Script Actions run in the global scope by default — prefix custom function calls with your application scope like
x_myapp.myFunction() - Event Registrations with empty condition fields fire for every record change — always add conditions to prevent performance issues
- The
currentandpreviousobjects are not available in Script Actions — access record data throughGlideRecordqueries instead - Custom events can be fired with
gs.eventQueue('custom.event.name', gr, param1, param2)from any server-side script - Script Actions execute for import operations unless you add
if (gs.isUpgrade()) return;at the top - Event order is: table.insert → table.update → table.delete — never assume a specific Script Action executes before another
- Use
gs.getProperty('instance_name')to prevent Script Actions from running in non-production instances during testing - Clone detection affects Script Actions —
gs.getUser().getID()may return the cloned instance's user rather than the original - Script Actions paired with async Business Rules can create race conditions — use
GlideSchedulefor time-sensitive sequencing