Script Include Development
SkillDev toolsComprehensive guide to developing Script Includes - class-based, client-callable (GlideAjax), inheritance patterns, and best practices
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Script Include 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/script-includes/SKILL.md and read by ahel’s review.
Overview
Script Includes are the foundation of reusable server-side code in ServiceNow. This skill covers:
- Class-based script include architecture
- Client-callable script includes (GlideAjax pattern)
- Extending and inheriting from existing classes
- Prototype patterns and object-oriented design
- Utility libraries and helper functions
- When to use script includes vs business rules
- Scoped vs global considerations
- Design patterns (Singleton, Factory, Strategy)
- Unit testing script includes with ATF
When to use Script Includes:
- Reusable logic needed across multiple business rules, workflows, or UI actions
- Complex business logic that needs unit testing
- Client-server communication (GlideAjax)
- Extending or overriding out-of-box functionality
- Creating service layers and APIs
Who should use this: Intermediate to advanced ServiceNow developers building maintainable, testable, and reusable code.
Prerequisites
- Roles:
admin,scriptinclude_create, or scoped app developer - Knowledge:
- JavaScript fundamentals (objects, prototypes, closures)
- ServiceNow scripting basics (GlideRecord, gs)
- Basic understanding of OOP concepts
- Access:
sys_script_includetable - Related Skills:
admin/script-sync- Local development workflowadmin/update-set-management- Track changesadmin/application-scope- Scoped development
Table Architecture
sys_script_include Schema
Tool: SN-Get-Table-Schema
Parameters:
table_name: sys_script_include
Key Fields:
| Field | Type | Purpose |
|---|---|---|
name | String | Class name (must match class definition) |
api_name | String | Full API name (scope.ClassName) |
script | Script | JavaScript class definition |
client_callable | Boolean | Expose via GlideAjax |
active | Boolean | Enable/disable |
access | Choice | public, package_private, private |
sys_scope | Reference | Application scope |
description | String | Documentation |
Procedure
Phase 1: Basic Class-Based Script Include
Step 1.1: Create a Basic Script Include
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: "IncidentUtils"
api_name: "global.IncidentUtils"
description: "Utility methods for incident management"
active: true
client_callable: false
access: "public"
script: |
var IncidentUtils = Class.create();
IncidentUtils.prototype = {
initialize: function() {
// Constructor - called when new instance created
},
/**
* Calculate incident priority based on impact and urgency
* @param {GlideRecord} incidentGR - Incident GlideRecord
* @returns {Number} Calculated priority (1-5)
*/
calculatePriority: function(incidentGR) {
var impact = parseInt(incidentGR.impact);
var urgency = parseInt(incidentGR.urgency);
// Priority matrix: Lower number = higher priority
var priorityMatrix = {
'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;
return priorityMatrix[key] || 4;
},
/**
* Check if incident is a P1 (Critical)
* @param {GlideRecord} incidentGR - Incident GlideRecord
* @returns {Boolean} True if P1
*/
isCritical: function(incidentGR) {
return incidentGR.priority == 1;
},
/**
* Get assignment group based on category
* @param {String} category - Incident category
* @returns {String} sys_id of assignment group
*/
getAssignmentGroup: function(category) {
var groupMapping = {
'network': 'Network Support',
'hardware': 'Hardware Support',
'software': 'Software Support',
'database': 'Database Team'
};
var groupName = groupMapping[category] || 'Service Desk';
var gr = new GlideRecord('sys_user_group');
gr.addQuery('name', groupName);
gr.query();
if (gr.next()) {
return gr.sys_id.toString();
}
return '';
},
type: 'IncidentUtils'
};
Step 1.2: Understanding the Class Structure
Class.create() Pattern:
var ClassName = Class.create(); // Create class constructor
ClassName.prototype = { // Define prototype (methods)
initialize: function(param1, param2) {
// Constructor - runs when 'new ClassName(p1, p2)' called
this.param1 = param1;
this.param2 = param2;
},
methodOne: function() {
// Instance method - has access to 'this'
return this.param1;
},
methodTwo: function(arg) {
// Another instance method
return arg + this.param2;
},
type: 'ClassName' // Required: Must match class name
};
Usage in Business Rule or Other Script:
// Create instance
var utils = new IncidentUtils();
// Call methods
var priority = utils.calculatePriority(current);
var isCritical = utils.isCritical(current);
var groupId = utils.getAssignmentGroup(current.category);
Step 1.3: Constructor Parameters
Script Include with Constructor:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: "IncidentHandler"
description: "Handles incident operations with configurable options"
active: true
client_callable: false
script: |
var IncidentHandler = Class.create();
IncidentHandler.prototype = {
/**
* Initialize handler with incident record
* @param {GlideRecord} incidentGR - The incident to handle
* @param {Object} options - Configuration options
*/
initialize: function(incidentGR, options) {
this.incident = incidentGR;
this.options = options || {};
this.log = new GSLog('com.company.incident', 'IncidentHandler');
// Default options
this.autoAssign = this.options.autoAssign !== false;
this.sendNotifications = this.options.sendNotifications !== false;
},
/**
* Process the incident based on its state
* @returns {Boolean} Success status
*/
process: function() {
if (!this.incident || !this.incident.isValidRecord()) {
this.log.error('Invalid incident record');
return false;
}
if (this.autoAssign && this._needsAssignment()) {
this._assignToGroup();
}
if (this.sendNotifications) {
this._notifyStakeholders();
}
return true;
},
// Private method (convention: prefix with underscore)
_needsAssignment: function() {
return this.incident.assignment_group.nil();
},
_assignToGroup: function() {
var utils = new IncidentUtils();
var groupId = utils.getAssignmentGroup(this.incident.category);
if (groupId) {
this.incident.assignment_group = groupId;
}
},
_notifyStakeholders: function() {
// Notification logic
gs.eventQueue('incident.processed', this.incident);
},
type: 'IncidentHandler'
};
Usage:
// With options
var handler = new IncidentHandler(current, {
autoAssign: true,
sendNotifications: false
});
handler.process();
// Default options
var handler2 = new IncidentHandler(current);
handler2.process();
Phase 2: Client-Callable Script Includes (GlideAjax)
Step 2.1: Create Client-Callable Script Include
Client-callable script includes enable communication between client scripts and the server.
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: "IncidentAjax"
description: "AJAX methods for incident client scripts"
active: true
client_callable: true
access: "public"
script: |
var IncidentAjax = Class.create();
IncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
/**
* Get incident details for client display
* Called from client: new GlideAjax('IncidentAjax').addParam('sysparm_name', 'getIncidentDetails')
*/
getIncidentDetails: function() {
var incidentId = this.getParameter('sysparm_incident_id');
var result = {};
var gr = new GlideRecord('incident');
if (gr.get(incidentId)) {
result.number = gr.number.toString();
result.short_description = gr.short_description.toString();
result.priority = gr.priority.toString();
result.priority_display = gr.priority.getDisplayValue();
result.state = gr.state.toString();
result.state_display = gr.state.getDisplayValue();
result.assigned_to = gr.assigned_to.getDisplayValue();
result.assignment_group = gr.assignment_group.getDisplayValue();
}
return JSON.stringify(result);
},
/**
* Validate if user can close incident
* Security check performed server-side
*/
canCloseIncident: function() {
var incidentId = this.getParameter('sysparm_incident_id');
var gr = new GlideRecord('incident');
if (!gr.get(incidentId)) {
return 'false';
}
// Check if current user can close
if (!gr.canWrite()) {
return 'false';
}
// Check incident state allows closure
if (gr.state == 6 || gr.state == 7) { // Already closed/cancelled
return 'false';
}
// Check if required fields are filled
if (gr.resolution_code.nil() || gr.close_notes.nil()) {
return 'false';
}
return 'true';
},
/**
* Get related incidents count
* Used for UI display without full query
*/
getRelatedIncidentsCount: function() {
var callerId = this.getParameter('sysparm_caller_id');
var excludeId = this.getParameter('sysparm_exclude_id');
var ga = new GlideAggregate('incident');
ga.addQuery('caller_id', callerId);
ga.addQuery('active', true);
if (excludeId) {
ga.addQuery('sys_id', '!=', excludeId);
}
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
return ga.getAggregate('COUNT');
}
return '0';
},
/**
* Get assignment groups for category (for dropdown)
*/
getAssignmentGroups: function() {
var category = this.getParameter('sysparm_category');
var groups = [];
// Get groups based on category (simplified example)
var gr = new GlideRecord('sys_user_group');
gr.addQuery('active', true);
gr.addQuery('type', '!=', ''); // Has type defined
gr.orderBy('name');
gr.setLimit(50);
gr.query();
while (gr.next()) {
groups.push({
sys_id: gr.sys_id.toString(),
name: gr.name.toString()
});
}
return JSON.stringify(groups);
},
/**
* SECURITY: Define which methods are callable
* Methods NOT listed here cannot be called from client
*/
isPublic: function() {
return true; // All methods in this class are public
},
type: 'IncidentAjax'
});
Step 2.2: Client Script Calling GlideAjax
Client Script (Synchronous - Avoid in Production):
// NOT RECOMMENDED - blocks UI
function getIncidentSync(incidentId) {
var ga = new GlideAjax('IncidentAjax');
ga.addParam('sysparm_name', 'getIncidentDetails');
ga.addParam('sysparm_incident_id', incidentId);
ga.getXMLWait(); // SYNCHRONOUS - blocks browser
var answer = ga.getAnswer();
return JSON.parse(answer);
}
Client Script (Asynchronous - RECOMMENDED):
// RECOMMENDED - non-blocking
function getIncidentAsync(incidentId, callback) {
var ga = new GlideAjax('IncidentAjax');
ga.addParam('sysparm_name', 'getIncidentDetails');
ga.addParam('sysparm_incident_id', incidentId);
ga.getXMLAnswer(function(answer) {
var result = JSON.parse(answer);
callback(result);
});
}
// Usage in client script
getIncidentAsync(g_form.getValue('sys_id'), function(incident) {
console.log('Incident: ' + incident.number);
g_form.setValue('work_notes', 'Related: ' + incident.number);
});
Client Script with Error Handling:
function getIncidentWithErrorHandling(incidentId) {
var ga = new GlideAjax('IncidentAjax');
ga.addParam('sysparm_name', 'getIncidentDetails');
ga.addParam('sysparm_incident_id', incidentId);
ga.getXML(function(response) {
// Check for errors
var answer = response.responseXML.documentElement.getAttribute('answer');
if (!answer) {
g_form.addErrorMessage('Error retrieving incident details');
return;
}
try {
var result = JSON.parse(answer);
// Process result
console.log('Got incident: ', result);
} catch (e) {
g_form.addErrorMessage('Error parsing response: ' + e.message);
}
});
}
Step 2.3: Securing Client-Callable Methods
Method-Level Security:
var SecureIncidentAjax = Class.create();
SecureIncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
/**
* Public method - callable by any user
*/
getPublicData: function() {
return JSON.stringify({ status: 'ok' });
},
/**
* Restricted method - requires specific role
*/
getAdminData: function() {
// Check role before processing
if (!gs.hasRole('admin')) {
return JSON.stringify({ error: 'Access denied' });
}
// Process admin-only request
return JSON.stringify({ adminData: 'sensitive info' });
},
/**
* Method requiring record-level access
*/
updateIncident: function() {
var incidentId = this.getParameter('sysparm_incident_id');
var newValue = this.getParameter('sysparm_value');
var gr = new GlideRecord('incident');
if (!gr.get(incidentId)) {
return JSON.stringify({ error: 'Not found' });
}
// Check ACL - does user have write access?
if (!gr.canWrite()) {
return JSON.stringify({ error: 'Write access denied' });
}
// Safe to update
gr.short_description = newValue;
gr.update();
return JSON.stringify({ success: true });
},
/**
* Control which methods are exposed
*/
isPublic: function() {
var methodName = this.getParameter('sysparm_name');
var publicMethods = ['getPublicData', 'getAdminData', 'updateIncident'];
return publicMethods.indexOf(methodName) !== -1;
},
type: 'SecureIncidentAjax'
});
Phase 3: Extending Existing Classes
Step 3.1: Extend Out-of-Box Script Include
Extending AbstractAjaxProcessor (Most Common):
var MyAjaxProcessor = Class.create();
MyAjaxProcessor.prototype = Object.extendsObject(AbstractAjaxProcessor, {
// Your methods here
type: 'MyAjaxProcessor'
});
Extending GlideAjax on Server Side:
var EnhancedIncidentUtils = Class.create();
EnhancedIncidentUtils.prototype = Object.extendsObject(IncidentUtils, {
initialize: function(incidentGR) {
// Call parent constructor
IncidentUtils.prototype.initialize.call(this);
this.incident = incidentGR;
},
/**
* Override parent method with enhanced logic
*/
calculatePriority: function(incidentGR) {
var gr = incidentGR || this.incident;
// Call parent implementation first
var basePriority = IncidentUtils.prototype.calculatePriority.call(this, gr);
// Enhance with additional logic
if (this._isVIPCaller(gr)) {
basePriority = Math.max(1, basePriority - 1); // Increase priority
}
return basePriority;
},
/**
* New method specific to enhanced class
*/
_isVIPCaller: function(incidentGR) {
var caller = incidentGR.caller_id;
if (caller.nil()) {
return false;
}
return caller.vip == true;
},
/**
* Get full incident analysis
*/
getAnalysis: function() {
return {
priority: this.calculatePriority(),
isCritical: this.isCritical(this.incident),
isVIP: this._isVIPCaller(this.incident)
};
},
type: 'EnhancedIncidentUtils'
});
Step 3.2: Extend Platform Classes
Extend RESTMessageV2:
var EnhancedRESTMessage = Class.create();
EnhancedRESTMessage.prototype = Object.extendsObject(sn_ws.RESTMessageV2, {
initialize: function(messageName, methodName) {
sn_ws.RESTMessageV2.prototype.initialize.call(this, messageName, methodName);
this.log = new GSLog('com.company.rest', 'EnhancedRESTMessage');
},
/**
* Execute with automatic retry on failure
*/
executeWithRetry: function(maxRetries) {
var retries = maxRetries || 3;
var lastError = null;
for (var i = 0; i < retries; i++) {
try {
var response = this.execute();
if (response.getStatusCode() >= 200 && response.getStatusCode() < 300) {
return response;
}
lastError = 'HTTP ' + response.getStatusCode();
} catch (e) {
lastError = e.message;
this.log.warn('Retry ' + (i + 1) + ' failed: ' + lastError);
}
// Wait before retry (exponential backoff)
if (i < retries - 1) {
gs.sleep(1000 * Math.pow(2, i));
}
}
throw new Error('All retries failed: ' + lastError);
},
/**
* Execute and parse JSON response
*/
executeAndParseJSON: function() {
var response = this.execute();
var body = response.getBody();
try {
return JSON.parse(body);
} catch (e) {
this.log.error('Failed to parse JSON: ' + body);
throw new Error('Invalid JSON response');
}
},
type: 'EnhancedRESTMessage'
});
Step 3.3: Create Base Class for Custom Hierarchy
Base Class:
var BaseServiceHandler = Class.create();
BaseServiceHandler.prototype = {
initialize: function(tableName) {
this.tableName = tableName;
this.log = new GSLog('com.company.handlers', this.type);
},
/**
* Get record by sys_id
* @param {String} sysId - Record sys_id
* @returns {GlideRecord} Record or null
*/
getRecord: function(sysId) {
var gr = new GlideRecord(this.tableName);
if (gr.get(sysId)) {
return gr;
}
return null;
},
/**
* Abstract method - must be overridden
*/
process: function(record) {
throw new Error('process() must be implemented by subclass');
},
/**
* Abstract method - must be overridden
*/
validate: function(record) {
throw new Error('validate() must be implemented by subclass');
},
/**
* Common logging method
*/
logAction: function(action, recordId) {
this.log.info(action + ' on ' + this.tableName + ': ' + recordId);
},
type: 'BaseServiceHandler'
};
Concrete Implementation:
var IncidentServiceHandler = Class.create();
IncidentServiceHandler.prototype = Object.extendsObject(BaseServiceHandler, {
initialize: function() {
// Call parent constructor with table name
BaseServiceHandler.prototype.initialize.call(this, 'incident');
},
/**
* Override abstract method
*/
process: function(record) {
if (!this.validate(record)) {
return false;
}
// Incident-specific processing
this._assignToGroup(record);
this._calculatePriority(record);
this.logAction('Processed incident', record.sys_id);
return true;
},
/**
* Override abstract method
*/
validate: function(record) {
if (record.short_description.nil()) {
this.log.error('Validation failed: missing short description');
return false;
}
return true;
},
_assignToGroup: function(record) {
// Implementation
},
_calculatePriority: function(record) {
// Implementation
},
type: 'IncidentServiceHandler'
});
Phase 4: Design Patterns
Step 4.1: Singleton Pattern
Use when you need exactly one instance shared across the application.
var ConfigurationManager = Class.create();
ConfigurationManager.prototype = {
initialize: function() {
this._loadConfiguration();
},
_loadConfiguration: function() {
this.config = {};
var gr = new GlideRecord('sys_properties');
gr.addQuery('name', 'STARTSWITH', 'com.company.');
gr.query();
while (gr.next()) {
var key = gr.name.toString().replace('com.company.', '');
this.config[key] = gr.value.toString();
}
},
get: function(key) {
return this.config[key];
},
set: function(key, value) {
this.config[key] = value;
// Persist to database
var gr = new GlideRecord('sys_properties');
gr.addQuery('name', 'com.company.' + key);
gr.query();
if (gr.next()) {
gr.value = value;
gr.update();
} else {
gr.initialize();
gr.name = 'com.company.' + key;
gr.value = value;
gr.insert();
}
},
type: 'ConfigurationManager'
};
/**
* Singleton accessor
* Usage: var config = ConfigurationManager.getInstance();
*/
ConfigurationManager.getInstance = function() {
if (!ConfigurationManager._instance) {
ConfigurationManager._instance = new ConfigurationManager();
}
return ConfigurationManager._instance;
};
// Clear singleton (for testing or refresh)
ConfigurationManager.clearInstance = function() {
ConfigurationManager._instance = null;
};
Usage:
// Get singleton instance
var config = ConfigurationManager.getInstance();
// Use configuration
var apiKey = config.get('api_key');
var timeout = config.get('timeout') || '30000';
// Update configuration
config.set('last_sync', gs.nowDateTime());
Step 4.2: Factory Pattern
Use when you need to create objects without specifying the exact class.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 37
- Forks
- 13
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
script-includes- Source
- github.com/happy-technologies-llc/happy-platform-skills