Advanced Script Includes: Object-Oriented ServiceNow
Introduction
Script Includes are ServiceNow's server-side reusable code libraries. While many developers use them simply to wrap a few GlideRecord queries, advanced practitioners use Script Includes to implement object-oriented patterns that make complex applications maintainable, testable, and extensible.
This guide assumes you're comfortable with basic Script Includes and ready to move into inheritance, mixins, abstract patterns, and enterprise-grade library design.
Script Include Foundation: Class.create()
All class-based Script Includes use Class.create(), ServiceNow's prototype-based OOP foundation:
var IncidentManager = Class.create();
IncidentManager.prototype = {
initialize: function(incidentSysId) {
this.incident = new GlideRecord('incident');
if (!this.incident.get(incidentSysId)) {
throw new Error('Incident not found: ' + incidentSysId);
}
this._log = new GSLog('com.mycompany.incident', 'IncidentManager');
},
getAssignmentGroup: function() {
return this.incident.assignment_group.getDisplayValue();
},
escalateToPriority: function(newPriority) {
var oldPriority = this.incident.getValue('priority');
this.incident.setValue('priority', newPriority);
this.incident.setValue('work_notes',
'Priority escalated from ' + oldPriority + ' to ' + newPriority);
this.incident.update();
this._log.info('Escalated incident ' +
this.incident.getValue('number') +
' from P' + oldPriority + ' to P' + newPriority);
return this;
},
assignToGroup: function(groupName) {
var group = new GlideRecord('sys_user_group');
if (group.get('name', groupName)) {
this.incident.setValue('assignment_group', group.getUniqueValue());
this.incident.update();
}
return this; // Enable method chaining
},
type: 'IncidentManager'
};
Usage:
// In a Business Rule or other server-side script:
new IncidentManager(current.getUniqueValue())
.escalateToPriority('1')
.assignToGroup('Major Incident Team');
Method Chaining Pattern
The return this pattern in every mutating method enables fluent, readable call chains:
var ChangeBuilder = Class.create();
ChangeBuilder.prototype = {
initialize: function() {
this._record = new GlideRecord('change_request');
this._record.initialize();
},
setTitle: function(title) {
this._record.setValue('short_description', title);
return this;
},
setRisk: function(risk) {
this._record.setValue('risk', risk);
return this;
},
setType: function(changeType) {
this._record.setValue('type', changeType);
return this;
},
assignTo: function(groupName) {
var group = new GlideRecord('sys_user_group');
if (group.get('name', groupName)) {
this._record.setValue('assignment_group',
group.getUniqueValue());
}
return this;
},
scheduledFor: function(startDateTime, endDateTime) {
this._record.setValue('start_date', startDateTime);
this._record.setValue('end_date', endDateTime);
return this;
},
build: function() {
var sysId = this._record.insert();
if (!sysId) {
throw new Error('Failed to create Change Request');
}
return sysId;
},
type: 'ChangeBuilder'
};
// Clean, readable usage:
var changeId = new ChangeBuilder()
.setTitle('Upgrade database server OS')
.setRisk('3') // Moderate
.setType('normal')
.assignTo('Database Team')
.scheduledFor('2024-04-15 22:00:00', '2024-04-16 02:00:00')
.build();
Inheritance in ServiceNow
// Base class
var BaseNotifier = Class.create();
BaseNotifier.prototype = {
initialize: function(record) {
this.record = record;
this.template = '';
this.recipients = [];
},
addRecipient: function(email) {
this.recipients.push(email);
return this;
},
_buildBody: function() {
// To be overridden by subclasses
return this.template;
},
send: function() {
var body = this._buildBody();
if (this.recipients.length === 0) {
gs.warn('BaseNotifier.send(): No recipients configured');
return false;
}
// Send email
var email = new GlideEmailOutbound();
email.setSubject(this._buildSubject());
email.setBody(body);
this.recipients.forEach(function(r) {
email.addRecipient(r);
});
email.send();
return true;
},
_buildSubject: function() {
return 'Notification from ServiceNow';
},
type: 'BaseNotifier'
};
// Subclass with inheritance
var IncidentNotifier = Class.create();
IncidentNotifier.prototype = Object.extendsObject(BaseNotifier, {
initialize: function(incidentRecord) {
BaseNotifier.prototype.initialize.call(this, incidentRecord);
// Auto-add caller and assigned user
if (incidentRecord.caller_id.email != '') {
this.addRecipient(incidentRecord.caller_id.email.toString());
}
},
_buildSubject: function() {
return '[' + this.record.getValue('number') + '] ' +
this.record.getValue('short_description');
},
_buildBody: function() {
return 'Your incident ' + this.record.getValue('number') +
' has been updated.\n\n' +
'State: ' + this.record.state.getDisplayValue() + '\n' +
'Priority: ' + this.record.priority.getDisplayValue() + '\n' +
'Assigned to: ' +
this.record.assigned_to.getDisplayValue();
},
type: 'IncidentNotifier'
});
Mixin Pattern for Shared Capabilities
When you want to share capabilities across unrelated classes without deep inheritance:
// Mixin: Adds logging capability to any class
var LoggableMixin = {
_initLogging: function(scope) {
this._logger = new GSLog(scope, this.type || 'Unknown');
},
log: function(msg) { this._logger && this._logger.info(msg); },
warn: function(msg) { this._logger && this._logger.warn(msg); },
error: function(msg) { this._logger && this._logger.error(msg); }
};
// Apply mixin to a class
var MyProcessor = Class.create();
MyProcessor.prototype = Object.assign(
Object.create({}),
LoggableMixin,
{
initialize: function() {
this._initLogging('com.mycompany.processor');
},
process: function() {
this.log('Processing started');
// ... processing logic ...
this.log('Processing complete');
},
type: 'MyProcessor'
}
);
Singleton Pattern
For utility classes that should have only one instance:
var ConfigManager = (function() {
var _instance = null;
var _cache = {};
function ConfigManagerImpl() {
this.type = 'ConfigManager';
}
ConfigManagerImpl.prototype.get = function(propName) {
if (!_cache[propName]) {
_cache[propName] = gs.getProperty(propName);
}
return _cache[propName];
};
ConfigManagerImpl.prototype.getBoolean = function(propName) {
return this.get(propName) === 'true';
};
ConfigManagerImpl.prototype.clearCache = function() {
_cache = {};
};
return {
getInstance: function() {
if (!_instance) {
_instance = new ConfigManagerImpl();
}
return _instance;
}
};
})();
// Usage:
var config = ConfigManager.getInstance();
var maxRetries = config.get('com.myapp.max_retries');
Performance Best Practices
// ❌ Anti-pattern: GlideRecord query inside a loop
while (incidents.next()) {
var gr = new GlideRecord('sys_user');
gr.get(incidents.getValue('caller_id')); // N+1 queries!
gs.info(gr.getValue('email'));
}
// ✅ Better: Use getDisplayValue() for reference fields
while (incidents.next()) {
gs.info(incidents.caller_id.email.toString()); // Single joined query
}
// ✅ Best for large sets: Pre-fetch with a map
var userMap = {};
var users = new GlideRecord('sys_user');
users.addQuery('sys_id', 'IN', callerIdList.join(','));
users.query();
while (users.next()) {
userMap[users.getUniqueValue()] = users.getValue('email');
}
Testing Script Includes
Use ATF (Automated Test Framework) for unit tests:
// ATF Server-side test step
var testResult = steps[0];
try {
var manager = new IncidentManager(testIncidentSysId);
manager.escalateToPriority('1');
// Verify
var inc = new GlideRecord('incident');
inc.get(testIncidentSysId);
if (inc.getValue('priority') === '1') {
testResult.setSuccess();
} else {
testResult.setFailed('Priority not updated. Expected: 1, Got: ' +
inc.getValue('priority'));
}
} catch(e) {
testResult.setFailed('Exception: ' + e.message);
}
Conclusion
Advanced Script Includes transform scattered Business Rule scripts into a coherent, maintainable codebase. Method chaining produces readable call sites. Inheritance and mixins reduce duplication. The singleton pattern manages shared state efficiently. Invest in this architectural layer early, and your ServiceNow applications will scale to enterprise complexity without becoming a maintenance nightmare.