What It Is
A Script Include is a server-side JavaScript library stored in the sys_script_include table that solves the fundamental problem of code reuse across ServiceNow's server-side execution contexts. Unlike client scripts that run in the user's browser with limited database access, Script Includes execute on the ServiceNow application server with full GlideRecord privileges and access to all server-side APIs. They exist because you need a way to centralize business logic, perform complex database operations, and maintain DRY principles across Business Rules, Scheduled Jobs, REST APIs, and other server-side scripts. Without Script Includes, you'd be copy-pasting the same logic across dozens of scripts, creating a maintenance nightmare that every ServiceNow developer has experienced at least once.
Architecturally, Script Includes sit in the server-side execution layer, never in the browser. When a client script needs server-side functionality, it uses GlideAjax to make an asynchronous call to a Script Include method, which executes on the server and returns data back to the client. This separation is critical—the client-side JavaScript runs in the user's browser with read-only access to form fields, while the Script Include runs on the ServiceNow server with full database access, system properties, and all the heavy lifting capabilities you need for real business logic.
Under the hood, ServiceNow processes Script Includes through its Rhino JavaScript engine, loading them into memory and maintaining them as singleton objects during the script's execution context. When you call new MyScriptInclude(), ServiceNow instantiates the class, executes any initialization code in the constructor, and makes all public methods available for the duration of that server-side execution thread. The platform automatically handles dependency resolution, script caching, and memory management. Most developers don't realize that Script Includes are loaded fresh for each server request—there's no persistent state between different user sessions or separate server calls.
You cannot build any serious ServiceNow implementation without Script Includes. Try to put complex business logic directly in Business Rules and you'll hit the execution time limits. Try to perform database queries from client scripts and you'll discover they simply can't access GlideRecord. Try to integrate with external APIs from a client script and you'll find yourself blocked by CORS policies. Script Includes are where you put the logic that actually makes ServiceNow work: complex approval workflows, integration callouts, data transformations, custom calculations, and anything that needs to touch multiple tables or perform operations that require administrative privileges.
Developers use Script Includes daily for business logic and integrations. Admins occasionally create simple Script Includes for data cleanup or reporting utilities. Architects design Script Include libraries as the foundation of major implementations, establishing patterns for how business logic flows through the entire system. The context ranges from simple utility functions called by a single Business Rule to complex service layers that power multiple applications across different scoped applications.
Script Includes relate most directly to Business Rules (which often call Script Include methods to keep the rule logic clean), GlideAjax (which exists specifically to call Script Includes from client-side code), and Scripted REST APIs (which frequently delegate their implementation to Script Include methods for better testability and reuse). The key distinction is that Script Includes contain the reusable logic, while these other script types serve as trigger points or communication mechanisms that invoke that logic.
How It Works Under the Hood
When ServiceNow executes a Script Include, it loads the script into the Rhino JavaScript engine running on the application server, not in any user's browser. The engine parses the script, creates a class definition based on your function constructor, and makes it available for instantiation. Each time you call new ScriptIncludeName(), ServiceNow creates a fresh object instance with access to all server-side APIs like GlideRecord, gs, and GlideSystem.
For GlideAjax calls specifically, ServiceNow maintains a special execution context that handles the bridge between client and server. The platform automatically serializes method parameters from the client-side JavaScript, transmits them via AJAX to the server, instantiates your Script Include, calls the specified method, captures the response, and serializes it back to the client. What developers often don't realize is that each GlideAjax call creates a completely independent server-side execution—there's no shared state between calls, even from the same user session.
The GlideAjax Request Lifecycle
- Client-side JavaScript calls
getXMLWait()orgetXML()on aGlideAjaxobject, triggering an HTTP request to the ServiceNow server - ServiceNow receives the request and identifies the target Script Include name and method from the AJAX parameters
- The platform loads the Script Include code from
sys_script_includetable and parses it in the Rhino JavaScript engine - ServiceNow instantiates the Script Include class and calls the constructor with server-side API access
- The specified method executes with full database privileges and access to all server-side APIs
- Return values are serialized to XML and sent back to the client-side callback function
- The Script Include object is destroyed and garbage collected after the method completes
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 onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') return;
// Create GlideAjax object pointing to our Script Include
var ga = new GlideAjax('IncidentUtils');
// Set the method we want to call on the server
ga.addParam('sysparm_name', 'getAssignmentGroup');
// Pass parameters to the server-side method
ga.addParam('sysparm_category', newValue);
ga.addParam('sysparm_location', g_form.getValue('location'));
// Make asynchronous call with callback function
ga.getXML(function(response) {
var answer = response.responseXML.documentElement.getAttribute('answer');
if (answer) {
g_form.setValue('assignment_group', answer);
}
});
}var IncidentUtils = Class.create();
IncidentUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
// This method gets called by GlideAjax from client scripts
getAssignmentGroup: function() {
// Extract parameters sent from client
var category = this.getParameter('sysparm_category');
var location = this.getParameter('sysparm_location');
// Perform server-side database query
var gr = new GlideRecord('sys_user_group');
gr.addQuery('u_category', category);
gr.addQuery('u_location', location);
gr.addQuery('active', true);
gr.orderBy('order');
gr.setLimit(1);
if (gr.next()) {
// Return result back to client callback
return gr.getUniqueValue();
}
return '';
},
type: 'IncidentUtils'
});Real-World Scenarios
Dynamic Service Catalog Pricing
A service catalog item needs to calculate pricing based on user department, requested quantity, and current vendor contracts stored in a custom table. The pricing logic is too complex for a simple catalog client script and needs access to multiple database tables.
function onLoad() {
// Recalculate pricing when quantity or hardware type changes
g_form.getControl('quantity').onchange = updatePricing;
g_form.getControl('hardware_type').onchange = updatePricing;
}
function updatePricing() {
var quantity = parseInt(g_form.getValue('quantity')) || 0;
var hardwareType = g_form.getValue('hardware_type');
if (quantity > 0 && hardwareType) {
var ga = new GlideAjax('CatalogPricingUtils');
ga.addParam('sysparm_name', 'calculatePrice');
ga.addParam('sysparm_hardware_type', hardwareType);
ga.addParam('sysparm_quantity', quantity);
ga.addParam('sysparm_user', g_user.userID);
ga.getXML(function(response) {
var price = response.responseXML.documentElement.getAttribute('answer');
g_form.setValue('estimated_cost', price);
});
}
}var CatalogPricingUtils = Class.create();
CatalogPricingUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
calculatePrice: function() {
var hardwareType = this.getParameter('sysparm_hardware_type');
var quantity = parseInt(this.getParameter('sysparm_quantity'));
var userId = this.getParameter('sysparm_user');
// Get user's department for pricing tier
var userGr = new GlideRecord('sys_user');
if (!userGr.get(userId)) return '0';
// Look up current vendor pricing
var priceGr = new GlideRecord('u_vendor_pricing');
priceGr.addQuery('hardware_type', hardwareType);
priceGr.addQuery('department', userGr.department);
priceGr.addQuery('active', true);
priceGr.orderByDesc('sys_created_on');
priceGr.setLimit(1);
if (priceGr.next()) {
var unitPrice = parseFloat(priceGr.unit_price);
var volumeDiscount = quantity >= 10 ? 0.1 : 0;
return ((unitPrice * quantity) * (1 - volumeDiscount)).toFixed(2);
}
return '0';
},
type: 'CatalogPricingUtils'
});Watch for performance issues when this gets called frequently—consider caching vendor pricing data in the user session. Also handle edge cases where users change departments mid-request, which can cause pricing inconsistencies if you don't lock the department value at request submission time.
Intelligent Assignment Routing
Incidents need smart assignment based on technician workload, skill matching, and on-call schedules rather than simple round-robin assignment. The client script suggests the best assignee before the user saves the record.
function onChange(control, oldValue, newValue, isLoading) {
// Trigger when category or priority changes
if (isLoading || newValue == '') return;
var category = g_form.getValue('category');
var priority = g_form.getValue('priority');
var location = g_form.getValue('location');
if (category && priority) {
g_form.addInfoMessage('Finding best available technician...');
var ga = new GlideAjax('AssignmentRouter');
ga.addParam('sysparm_name', 'findBestAssignee');
ga.addParam('sysparm_category', category);
ga.addParam('sysparm_priority', priority);
ga.addParam('sysparm_location', location);
ga.getXML(function(response) {
var assignee = response.responseXML.documentElement.getAttribute('answer');
if (assignee && assignee != 'none') {
g_form.setValue('assigned_to', assignee);
g_form.clearMessages();
} else {
g_form.addWarningMessage('No available technicians found. Assignment will be queued.');
}
});
}
}var AssignmentRouter = Class.create();
AssignmentRouter.prototype = Object.extendsObject(AbstractAjaxProcessor, {
findBestAssignee: function() {
var category = this.getParameter('sysparm_category');
var priority = this.getParameter('sysparm_priority');
var location = this.getParameter('sysparm_location');
// Find technicians with required skills and low workload
var techGr = new GlideRecord('sys_user');
techGr.addQuery('active', true);
techGr.addQuery('u_skill_categories', 'CONTAINS', category);
techGr.addQuery('location', location);
var bestTech = null;
var lowestWorkload = 999;
while (techGr.next()) {
// Count active incidents assigned to this tech
var workloadGr = new GlideAggregate('incident');
workloadGr.addQuery('assigned_to', techGr.getUniqueValue());
workloadGr.addQuery('state', 'IN', '1,2,3'); // New, In Progress, On Hold
workloadGr.aggregate();
var currentWorkload = workloadGr.getAggregate('COUNT');
if (currentWorkload < lowestWorkload) {
lowestWorkload = currentWorkload;
bestTech = techGr.getUniqueValue();
}
}
return bestTech || 'none';
},
type: 'AssignmentRouter'
});This pattern can get expensive with large teams—the GlideAggregate query runs for every potential assignee. Consider maintaining a cached workload counter that updates via Business Rules on incident state changes. Also be careful with the skills matching logic—using CONTAINS on text fields doesn't scale well.
Real-Time SLA Progress Indicator
Support agents need a visual indicator showing how much time remains on critical SLAs without refreshing the form. The client script polls the server every 30 seconds to update a progress bar with current SLA status.
function onLoad() {
if (g_form.getValue('state') == '3' || g_form.getValue('state') == '6') {
return; // Don't monitor closed/resolved incidents
}
// Start monitoring SLA progress
startSLAMonitoring();
// Update every 30 seconds
window.slaTimer = setInterval(startSLAMonitoring, 30000);
}
function startSLAMonitoring() {
var sysId = g_form.getUniqueValue();
var ga = new GlideAjax('SLAMonitor');
ga.addParam('sysparm_name', 'getSLAStatus');
ga.addParam('sysparm_incident_id', sysId);
ga.getXML(function(response) {
var status = response.responseXML.documentElement.getAttribute('answer');
updateSLAIndicator(JSON.parse(status));
});
}
function updateSLAIndicator(slaData) {
// Update custom SLA progress indicator in the form
if (slaData.percentRemaining < 20) {
g_form.addErrorMessage('SLA Critical: ' + slaData.timeRemaining + ' remaining');
}
}var SLAMonitor = Class.create();
SLAMonitor.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getSLAStatus: function() {
var incidentId = this.getParameter('sysparm_incident_id');
// Find active SLA records for this incident
var slaGr = new GlideRecord('task_sla');
slaGr.addQuery('task', incidentId);
slaGr.addQuery('stage', 'in_progress');
slaGr.orderBy('priority'); // Highest priority SLA first
slaGr.setLimit(1);
if (slaGr.next()) {
var now = new GlideDateTime();
var dueDate = new GlideDateTime(slaGr.planned_end_time);
var startDate = new GlideDateTime(slaGr.start_time);
var totalDuration = dueDate.getNumericValue() - startDate.getNumericValue();
var remainingTime = dueDate.getNumericValue() - now.getNumericValue();
var percentRemaining = Math.max(0, (remainingTime / totalDuration) * 100);
return JSON.stringify({
slaName: slaGr.sla.getDisplayValue(),
timeRemaining: this._formatDuration(remainingTime),
percentRemaining: Math.round(percentRemaining),
isBreached: remainingTime <= 0
});
}
return JSON.stringify({percentRemaining: 100});
},
_formatDuration: function(milliseconds) {
var hours = Math.floor(milliseconds / (1000 * 60 * 60));
var minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
return hours + 'h ' + minutes + 'm';
},
type: 'SLAMonitor'
});Polling every 30 seconds from multiple browser tabs can create significant server load. Consider using server-sent events or limiting polling to only the active tab. Also remember to clear the timer with clearInterval() when users navigate away from the form.
The Classic Mistake
Creating a Script Include without properly setting the initialize function or calling it from the wrong context.
var IncidentHelper = Class.create();
IncidentHelper.prototype = {
// Missing initialize function completely
createIncident: function(summary, description) {
var inc = new GlideRecord('incident');
inc.initialize();
inc.short_description = summary;
inc.description = description;
inc.caller_id = this.callerId; // This will be undefined
inc.category = this.category; // This will be undefined
return inc.insert();
},
type: 'IncidentHelper'
};This fails because ServiceNow's Class framework requires an initialize function to properly construct the object. Without it, any properties you try to set during instantiation become undefined, leading to null reference errors in the browser console like "Cannot read property 'callerId' of undefined". ServiceNow's internal object creation mechanism skips property assignment when initialize is missing. When called from GlideAjax, this manifests as silent failures where your Script Include methods return empty results. The server logs show "ReferenceError: callerId is not defined" but only if you're looking in System Log > All with source = "ScriptInclude".
var IncidentHelper = Class.create();
IncidentHelper.prototype = {
initialize: function(callerId, category) {
this.callerId = callerId || gs.getUserID();
this.category = category || 'inquiry';
},
createIncident: function(summary, description) {
var inc = new GlideRecord('incident');
inc.initialize();
inc.short_description = summary;
inc.description = description;
inc.caller_id = this.callerId;
inc.category = this.category;
return inc.insert();
},
type: 'IncidentHelper'
};Every Script Include prototype must have an initialize function, even if it's empty. Always instantiate Script Includes with 'new' and pass parameters to initialize, not to individual methods.
Performance Rules
- Never call
GlideRecord.query()in a loop without limits. Over 100 iterations causes database connection pool exhaustion and 30+ second response times that trigger ServiceNow's script timeout, killing the transaction. - Limit
GlideAggregateoperations to tables under 50,000 records. Aggregating larger tables without proper indexes causes memory spikes that crash the application node, requiring sys_admin intervention to restart services. - Always set
setLimit()on GlideRecord queries to 1000 or less. Unlimited queries on large tables consume heap memory exponentially, leading to OutOfMemoryError exceptions that appear in System Log > Errors. - Cache expensive calculations using
gs.getProperty()or session variables. Recalculating complex business logic on every method call creates CPU bottlenecks that slow down all users sharing the same application node. - Avoid calling
GlideSystem.sleep()for more than 5 seconds total per Script Include execution. Longer sleeps block the database connection thread, causing connection pool starvation that manifests as "Database connection timeout" errors for all users. - Use
getEncodedQuery()instead of chainingaddQuery()calls when you have more than 3 conditions. Multiple addQuery calls generate inefficient SQL with nested subqueries that take 10x longer to execute than encoded query strings. - Never instantiate Script Includes inside
while(gr.next())loops. Each instantiation allocates memory that isn't garbage collected until the parent script completes, causing heap exhaustion on queries returning more than 500 records. - Minimize REST API calls to external systems within Script Includes by batching requests. Individual REST calls average 200ms latency, so 10 sequential calls add 2 seconds of response time that users perceive as application slowness.
Side Effects & Platform Behavior
- Script Includes called from Business Rules inherit the Business Rule's execution context, triggering audit entries in the
sys_audittable with the Business Rule name in thereasonfield when they modify records. - GlideRecord operations within Script Includes bypass ACL evaluation when called from server-side contexts, but respect ACLs when called via GlideAjax from client scripts.
- Workflow activities calling Script Includes write execution details to
wf_executingtable including Script Include name and execution time, visible in Workflow > Admin > Executing Activities. - Script Includes updating records with active Notifications trigger email generation, with the Script Include name appearing in notification logs under System Logs > Email.
- Global Script Includes are accessible across all application scopes, but scoped Script Includes become unavailable if their containing application is deactivated, causing "Script Include not found" errors.
- Script Includes executed during Login processing write session data to
v_user_sessiontable, and failures during this phase prevent user login entirely. - Script Includes called from Scheduled Jobs inherit the job's
run_asuser context, affecting record access andgs.getUserID()return values. - Script Includes using
gs.eventQueue()create entries insyseventtable with the Script Include name in thecreated_byfield for event tracking. - Script Includes break when called from client-side scripts in Service Portal if they reference server-only APIs like
GlideSysAttachmentorGlideEmailOutbound. - Script Include modifications appear in Update Sets with dependency tracking, but changes to Script Includes referenced by other customizations don't automatically include those dependencies in the Update Set.
Debugging When It Breaks
The most common failure symptom is the dreaded "Script Include not found" error, which appears differently depending on context. From GlideAjax calls, users see blank form fields or dropdowns that fail to populate, while the browser console shows "Error: Class not found: YourScriptIncludeName". From server-side calls, you get immediate JavaScript errors in System Log > All with source="Application Server" and the stack trace points to the line attempting instantiation.
When Script Includes fail silently, check System Log > Script Debugger (set debug level to "debug" first) to see method entry and exit points. Failed GlideRecord operations within Script Includes show as "Access denied" messages in the same log, usually indicating ACL restrictions or missing role assignments. For performance issues, Script Includes taking longer than 30 seconds log "Script execution timeout" warnings in System Log > All, and you'll see the partial stack trace showing which line was executing when the timeout occurred.
Memory-related failures manifest as "OutOfMemoryError: Java heap space" in System Log > Errors, typically after Script Includes process large datasets or create too many object instances. The error message includes the thread name, which helps identify whether it's coming from a Business Rule, Scheduled Job, or REST API call context. Quick diagnostic checklist:
- Verify Script Include exists in
sys_script_includetable and is Active - Check Application Scope matches between Script Include and calling script
- Confirm
Client callablecheckbox is checked for GlideAjax usage - Test instantiation with
gs.log()statements in initialize function - Validate user has necessary roles for any privileged operations inside Script Include
Quick Reference
- Always include
type: 'ClassName'at the end of prototype definition—ServiceNow uses this for class identification and debugging - GlideAjax requires methods to call
this.setAnswer()to return values to client-side callbacks—return statements are ignored - Script Include names must match exactly between the Name field and the JavaScript class name or instantiation fails
- Use
AbstractAjaxProcessorinstead of basic Script Includes for GlideAjax—it provides built-in parameter handling and security - Global Script Includes can access scoped APIs, but scoped Script Includes cannot access global APIs without explicit grants
- Script Include changes require cache clearing in development instances—use
cache.doto flush script cache after modifications - Static methods using
ClassName.methodName()don't require instantiation but can't access instance variables set in initialize - Script Includes execute with database read/write capabilities by default—add explicit permission checks for sensitive operations
- Use
gs.include()for legacy Script Includes without Class.create() structure, but avoid for new development - Script Include execution context determines available APIs—methods like
current.update()fail when called outside Business Rule context