CAD Guide

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.

Keep reading this guide

Log in to access the full study guide and supercharge your preparation.