agentleFS
Happy-Technologies-LLCHappy-Technologies-LLC/happy-platform-skills/skills/development/script-includes/SKILL.md

script-includes

Skill41 starsChanged 6 days agoApache-2.0View on GitHub

Comprehensive guide to developing Script Includes - class-based, client-callable (GlideAjax), inheritance patterns, and best practices

We split this skill into the rules it gives an agent

18 rules. About 0 are common, 13 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈13

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When to use script includes vs business rules

    line 38
  2. Distinctive:

    Use when you need exactly one instance shared across the application.

    line 785
  3. Distinctive:

    Use when you need to create objects without specifying the exact class.

    line 866
  4. Distinctive:

    Use when you need interchangeable algorithms.

    line 982
  5. Distinctive:

    No namespace prefix required

    line 1136
  6. Distinctive:

    JSDoc comments: - Document all public methods

    line 1480
  7. Distinctive:

    Lazy loading: - Don't load dependencies until needed

    line 1484
  8. Distinctive:

    Avoid global state: - Use instance variables or parameters

    line 1486
  9. Distinctive:

    Validate inputs: - Never trust client data in GlideAjax

    line 1491
  10. Very specific:

    Check permissions: - Use canRead(), canWrite() before operations

    line 1492
  11. Distinctive:

    Limit exposure: - Only mark methods client_callable when necessary

    line 1493
  12. Very specific:

    Verify client_callable is true

    line 1527
  13. Distinctive:

    Verify method returns a value (string)

    line 1528
  14. Very specific:

    Check isPublic() includes your method

    line 1529
  15. Distinctive:

    Check system logs for server errors

    line 1530
  16. Very specific:

    Check access field (should be 'public' for cross-scope)

    line 1553
  17. Very specific:

    Use full API name: new x_scope.ClassName()

    line 1554
  18. Distinctive:

    Verify calling code is in allowed scope

    line 1555

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Script Include Development

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_include table
  • Related Skills:
    • admin/script-sync - Local development workflow
    • admin/update-set-management - Track changes
    • admin/application-scope - Scoped development

Table Architecture

sys_script_include Schema

Tool: SN-Get-Table-Schema
Parameters:
  table_name: sys_script_include

Key Fields:

FieldTypePurpose
nameStringClass name (must match class definition)
api_nameStringFull API name (scope.ClassName)
scriptScriptJavaScript class definition
client_callableBooleanExpose via GlideAjax
activeBooleanEnable/disable
accessChoicepublic, package_private, private
sys_scopeReferenceApplication scope
descriptionStringDocumentation

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.

var NotificationFactory = Class.create();
NotificationFactory.prototype = {

    initialize: function() {
        // Register notification types
        this.types = {
            'email': EmailNotification,
            'sms': SMSNotification,
            'slack': SlackNotification,
            'teams': TeamsNotification
        };
    },

    /**
     * Create notification handler based on type
     * @param {String} type - Notification type
     * @param {Object} options - Configuration options
     * @returns {Object} Notification handler
     */
    create: function(type, options) {
        var NotificationClass = this.types[type.toLowerCase()];

        if (!NotificationClass) {
            throw new Error('Unknown notification type: ' + type);
        }

        return new NotificationClass(options);
    },

    /**
     * Register custom notification type
     */
    register: function(type, handlerClass) {
        this.types[type.toLowerCase()] = handlerClass;
    },

    /**
     * Get available types
     */
    getAvailableTypes: function() {
        return Object.keys(this.types);
    },

    type: 'NotificationFactory'
};

// Base notification class
var BaseNotification = Class.create();
BaseNotification.prototype = {
    initialize: function(options) {
        this.options = options || {};
    },
    send: function(recipient, message) {
        throw new Error('send() must be implemented');
    },
    type: 'BaseNotification'
};

// Email implementation
var EmailNotification = Class.create();
EmailNotification.prototype = Object.extendsObject(BaseNotification, {
    send: function(recipient, message) {
        var email = new GlideEmailOutbound();
        email.setSubject(this.options.subject || 'Notification');
        email.setFrom(this.options.from || 'noreply@company.com');
        email.addAddress(recipient);
        email.setBody(message);
        email.send();
        return true;
    },
    type: 'EmailNotification'
});

// SMS implementation
var SMSNotification = Class.create();
SMSNotification.prototype = Object.extendsObject(BaseNotification, {
    send: function(recipient, message) {
        // SMS API integration
        var rest = new sn_ws.RESTMessageV2('SMS_API', 'send');
        rest.setStringParameterNoEscape('to', recipient);
        rest.setStringParameterNoEscape('message', message);
        var response = rest.execute();
        return response.getStatusCode() == 200;
    },
    type: 'SMSNotification'
});

Usage:

var factory = new NotificationFactory();

// Create email notification
var emailHandler = factory.create('email', {
    subject: 'Important Alert',
    from: 'alerts@company.com'
});
emailHandler.send('user@company.com', 'Your incident was updated.');

// Create SMS notification
var smsHandler = factory.create('sms', {});
smsHandler.send('+1234567890', 'P1 Alert: Check your email.');

// Use multiple notification types
var types = ['email', 'slack'];
types.forEach(function(type) {
    var handler = factory.create(type, {});
    handler.send(recipient, message);
});

Step 4.3: Strategy Pattern

Use when you need interchangeable algorithms.

var IncidentAssigner = Class.create();
IncidentAssigner.prototype = {

    initialize: function() {
        this.strategies = {
            'round_robin': new RoundRobinStrategy(),
            'least_busy': new LeastBusyStrategy(),
            'skills_based': new SkillsBasedStrategy(),
            'random': new RandomStrategy()
        };
        this.currentStrategy = 'round_robin';
    },

    /**
     * Set assignment strategy
     */
    setStrategy: function(strategyName) {
        if (!this.strategies[strategyName]) {
            throw new Error('Unknown strategy: ' + strategyName);
        }
        this.currentStrategy = strategyName;
    },

    /**
     * Assign incident using current strategy
     */
    assign: function(incidentGR) {
        var strategy = this.strategies[this.currentStrategy];
        var assignee = strategy.findAssignee(incidentGR);

        if (assignee) {
            incidentGR.assigned_to = assignee;
            return true;
        }
        return false;
    },

    type: 'IncidentAssigner'
};

// Strategy interface
var AssignmentStrategy = Class.create();
AssignmentStrategy.prototype = {
    findAssignee: function(incidentGR) {
        throw new Error('findAssignee() must be implemented');
    },
    type: 'AssignmentStrategy'
};

// Round Robin implementation
var RoundRobinStrategy = Class.create();
RoundRobinStrategy.prototype = Object.extendsObject(AssignmentStrategy, {
    findAssignee: function(incidentGR) {
        var groupId = incidentGR.assignment_group.toString();
        if (!groupId) return null;

        // Get last assigned user for this group
        var lastAssigned = gs.getProperty('round_robin.' + groupId, '');

        // Get all active members
        var members = [];
        var gm = new GlideRecord('sys_user_grmember');
        gm.addQuery('group', groupId);
        gm.addQuery('user.active', true);
        gm.query();

        while (gm.next()) {
            members.push(gm.user.toString());
        }

        if (members.length === 0) return null;

        // Find next user in rotation
        var lastIndex = members.indexOf(lastAssigned);
        var nextIndex = (lastIndex + 1) % members.length;
        var nextUser = members[nextIndex];

        // Save for next rotation
        gs.setProperty('round_robin.' + groupId, nextUser);

        return nextUser;
    },
    type: 'RoundRobinStrategy'
});

// Least Busy implementation
var LeastBusyStrategy = Class.create();
LeastBusyStrategy.prototype = Object.extendsObject(AssignmentStrategy, {
    findAssignee: function(incidentGR) {
        var groupId = incidentGR.assignment_group.toString();
        if (!groupId) return null;

        var leastBusy = null;
        var lowestCount = Infinity;

        var gm = new GlideRecord('sys_user_grmember');
        gm.addQuery('group', groupId);
        gm.addQuery('user.active', true);
        gm.query();

        while (gm.next()) {
            var userId = gm.user.toString();
            var count = this._getOpenIncidentCount(userId);

            if (count < lowestCount) {
                lowestCount = count;
                leastBusy = userId;
            }
        }

        return leastBusy;
    },

    _getOpenIncidentCount: function(userId) {
        var ga = new GlideAggregate('incident');
        ga.addQuery('assigned_to', userId);
        ga.addQuery('active', true);
        ga.addAggregate('COUNT');
        ga.query();

        if (ga.next()) {
            return parseInt(ga.getAggregate('COUNT'));
        }
        return 0;
    },
    type: 'LeastBusyStrategy'
});

Usage:

var assigner = new IncidentAssigner();

// Configure strategy based on system property
var strategy = gs.getProperty('incident.assignment.strategy', 'round_robin');
assigner.setStrategy(strategy);

// Assign incident
if (assigner.assign(current)) {
    gs.info('Assigned to: ' + current.assigned_to.getDisplayValue());
} else {
    gs.warn('No assignee found');
}

Phase 5: Scoped vs Global Script Includes

Step 5.1: Global Script Includes

Characteristics:

  • Available across all scopes
  • No namespace prefix required
  • Can access all platform APIs
  • Risk of naming conflicts
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "GlobalUtils"
    api_name: "global.GlobalUtils"
    sys_scope: "global"
    access: "public"
    script: |
      var GlobalUtils = Class.create();
      GlobalUtils.prototype = {
          initialize: function() {},

          formatDate: function(gdt) {
              // Available to all scopes
              return gdt.getDisplayValue();
          },

          type: 'GlobalUtils'
      };

Step 5.2: Scoped Script Includes

Characteristics:

  • Isolated within application scope
  • Prefixed with scope name (e.g., x_company_app.ClassName)
  • Access controlled by access field
  • Safer namespace management
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "ScopedUtils"
    api_name: "x_company_app.ScopedUtils"
    sys_scope: "[scoped_app_sys_id]"
    access: "public"
    script: |
      var ScopedUtils = Class.create();
      ScopedUtils.prototype = {
          initialize: function() {},

          /**
           * This method is accessible from other scopes
           * because access is 'public'
           */
          publicMethod: function() {
              return 'Accessible anywhere';
          },

          type: 'ScopedUtils'
      };

Step 5.3: Access Modifiers

Access LevelScope AccessUse Case
publicAll scopesShared utilities, APIs
package_privateSame scope onlyInternal helpers
privateSame scope, no inheritanceProtected utilities

Calling Scoped Script Include from Global:

// From global scope, need full API name
var utils = new x_company_app.ScopedUtils();
var result = utils.publicMethod();

Calling Global Script Include from Scoped:

// From scoped app, global is accessible
var globalUtils = new GlobalUtils();
var result = globalUtils.formatDate(new GlideDateTime());

Phase 6: When to Use Script Includes vs Business Rules

Decision Matrix

ScenarioUse Script IncludeUse Business Rule
Reusable logic (multiple places)YesNo
Single table, single triggerNoYes
Client-server communicationYes (GlideAjax)No
Unit testing requiredYesPossible but harder
Complex algorithm/calculationYesExtract to Script Include
Simple field validationNoYes
Workflow/Flow activityYesN/A
Scheduled job logicExtract to Script IncludeN/A
Cross-table operationsYesPossible

Anti-Patterns to Avoid

BAD: Duplicating Logic in Business Rules

// Business Rule 1 on Incident
if (current.priority == 1 && current.assignment_group.nil()) {
    current.assignment_group = getDefaultGroup('incident');
}

// Business Rule 2 on Change - DUPLICATE LOGIC!
if (current.priority == 1 && current.assignment_group.nil()) {
    current.assignment_group = getDefaultGroup('change_request');
}

GOOD: Extract to Script Include

// Script Include: AssignmentUtils
var AssignmentUtils = Class.create();
AssignmentUtils.prototype = {
    initialize: function() {},

    ensureAssignment: function(record, tableName) {
        if (record.priority == 1 && record.assignment_group.nil()) {
            record.assignment_group = this.getDefaultGroup(tableName);
        }
    },

    getDefaultGroup: function(tableName) {
        // Lookup logic
    },

    type: 'AssignmentUtils'
};

// Business Rule - Simple delegation
var utils = new AssignmentUtils();
utils.ensureAssignment(current, 'incident');

Phase 7: Unit Testing Script Includes

Step 7.1: Create ATF Test for Script Include

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_test
  data:
    name: "Test IncidentUtils - calculatePriority"
    description: "Unit tests for IncidentUtils.calculatePriority method"
    active: true

Step 7.2: Create Test Steps

Test Step 1: Test P1 Calculation

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_step
  data:
    test: [test_sys_id]
    order: 100
    step_config: "atf_test_step_config:run_server_side_script"
    description: "Test P1 priority calculation"
    inputs:
      script: |
        (function(outputs, steps, params, stepResult, assertEqual) {
            // Arrange
            var incident = new GlideRecord('incident');
            incident.newRecord();
            incident.impact = 1;
            incident.urgency = 1;

            // Act
            var utils = new IncidentUtils();
            var priority = utils.calculatePriority(incident);

            // Assert
            assertEqual({
                name: 'Priority should be 1 for high impact + high urgency',
                shouldbe: 1,
                value: priority
            });

        })(outputs, steps, params, stepResult, assertEqual);

Test Step 2: Test P3 Calculation

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_step
  data:
    test: [test_sys_id]
    order: 200
    step_config: "atf_test_step_config:run_server_side_script"
    description: "Test P3 priority calculation"
    inputs:
      script: |
        (function(outputs, steps, params, stepResult, assertEqual) {
            // Arrange
            var incident = new GlideRecord('incident');
            incident.newRecord();
            incident.impact = 2;
            incident.urgency = 2;

            // Act
            var utils = new IncidentUtils();
            var priority = utils.calculatePriority(incident);

            // Assert
            assertEqual({
                name: 'Priority should be 3 for medium impact + medium urgency',
                shouldbe: 3,
                value: priority
            });

        })(outputs, steps, params, stepResult, assertEqual);

Step 7.3: Test Mock Pattern

Mockable Script Include:

var IncidentService = Class.create();
IncidentService.prototype = {

    initialize: function(dependencies) {
        // Dependency injection for testability
        this.notificationService = dependencies && dependencies.notificationService
            ? dependencies.notificationService
            : new NotificationService();

        this.assignmentUtils = dependencies && dependencies.assignmentUtils
            ? dependencies.assignmentUtils
            : new AssignmentUtils();
    },

    processNewIncident: function(incidentGR) {
        // Assign
        this.assignmentUtils.autoAssign(incidentGR);

        // Notify
        if (incidentGR.priority == 1) {
            this.notificationService.notifyOnCall(incidentGR);
        }

        return true;
    },

    type: 'IncidentService'
};

ATF Test with Mocks:

(function(outputs, steps, params, stepResult, assertEqual) {

    // Create mock notification service
    var mockNotificationService = {
        notifyCalled: false,
        lastIncident: null,
        notifyOnCall: function(incidentGR) {
            this.notifyCalled = true;
            this.lastIncident = incidentGR;
        }
    };

    // Create mock assignment utils
    var mockAssignmentUtils = {
        autoAssign: function(incidentGR) {
            incidentGR.assigned_to = 'mock_user_id';
        }
    };

    // Inject mocks
    var service = new IncidentService({
        notificationService: mockNotificationService,
        assignmentUtils: mockAssignmentUtils
    });

    // Arrange
    var incident = new GlideRecord('incident');
    incident.newRecord();
    incident.priority = 1;
    incident.short_description = 'Test incident';

    // Act
    service.processNewIncident(incident);

    // Assert
    assertEqual({
        name: 'Notification should be called for P1',
        shouldbe: true,
        value: mockNotificationService.notifyCalled
    });

    assertEqual({
        name: 'Incident should be assigned',
        shouldbe: 'mock_user_id',
        value: incident.assigned_to.toString()
    });

})(outputs, steps, params, stepResult, assertEqual);

Tool Usage Summary

MCP Tools

ToolPurpose
SN-Create-RecordCreate new script includes
SN-Update-RecordUpdate existing script includes
SN-Query-TableFind script includes
SN-Get-RecordGet script include details
SN-Get-RecordPull script and freshness metadata for local development
SN-Update-RecordPush a reviewed script after a remote freshness check
SN-Execute-Background-ScriptTest script includes
SN-Get-Table-SchemaExplore sys_script_include fields

Happy Platform MCP 6.1 has no automatic watch or synchronization tool. For local development, retrieve script, sys_updated_on, and sys_mod_count with SN-Get-Record; edit locally; retrieve them again to detect conflicts; then push only the reviewed script field with SN-Update-Record. Include the explicit target instance on each call.

REST API

EndpointMethodPurpose
/api/now/table/sys_script_includePOSTCreate script include
/api/now/table/sys_script_include/{id}PATCHUpdate script include
/api/now/table/sys_script_includeGETQuery script includes

Best Practices

Code Organization

  • One class per file - Makes version control cleaner
  • Meaningful names - CustomerOrderProcessor not Utils2
  • Consistent naming - PascalCase for classes, camelCase for methods
  • Private methods - Prefix with underscore: _internalMethod
  • JSDoc comments - Document all public methods

Performance

  • Lazy loading - Don't load dependencies until needed
  • Caching - Cache expensive lookups (GlideRecord queries)
  • Avoid global state - Use instance variables or parameters
  • Minimize GlideRecord queries - Batch where possible

Security

  • Validate inputs - Never trust client data in GlideAjax
  • Check permissions - Use canRead(), canWrite() before operations
  • Limit exposure - Only mark methods client_callable when necessary
  • Implement isPublic() - Whitelist allowed methods

Testing

  • Write tests first - TDD improves design
  • Use dependency injection - Makes mocking easier
  • Test edge cases - Null values, empty strings, boundary conditions
  • Keep tests fast - Mock expensive operations

Troubleshooting

Script Include Not Found

Symptom: TypeError: ClassName is not a constructor Cause: Script include not active or name mismatch Solution:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_include
  query: name=ClassName
  fields: name,api_name,active,sys_scope.name

Verify:

  1. active is true
  2. name matches class definition exactly
  3. Calling from correct scope

GlideAjax Returns Empty

Symptom: getAnswer() returns empty or undefined Cause: Method not returning value or not client_callable Solution:

  1. Verify client_callable is true
  2. Verify method returns a value (string)
  3. Check isPublic() includes your method
  4. Check system logs for server errors

Cannot Extend Class

Symptom: Parent methods not available Cause: Incorrect extension syntax Solution:

// WRONG
ChildClass.prototype = ParentClass.prototype;

// CORRECT
ChildClass.prototype = Object.extendsObject(ParentClass, {
    // child methods
    type: 'ChildClass'
});

Scoped Class Not Accessible

Symptom: Cannot instantiate scoped script include Cause: Access modifier or scope issue Solution:

  1. Check access field (should be 'public' for cross-scope)
  2. Use full API name: new x_scope.ClassName()
  3. Verify calling code is in allowed scope

Examples

Example 1: Complete CRUD Service

var CRUDService = Class.create();
CRUDService.prototype = {

    initialize: function(tableName) {
        this.tableName = tableName;
        this.log = new GSLog('com.company.crud', 'CRUDService');
    },

    create: function(data) {
        var gr = new GlideRecord(this.tableName);
        gr.initialize();

        for (var field in data) {
            if (data.hasOwnProperty(field) && gr.isValidField(field)) {
                gr.setValue(field, data[field]);
            }
        }

        var sysId = gr.insert();
        this.log.info('Created ' + this.tableName + ': ' + sysId);
        return sysId;
    },

    read: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            return this._toObject(gr);
        }
        return null;
    },

    update: function(sysId, data) {
        var gr = new GlideRecord(this.tableName);
        if (!gr.get(sysId)) {
            return false;
        }

        for (var field in data) {
            if (data.hasOwnProperty(field) && gr.isValidField(field)) {
                gr.setValue(field, data[field]);
            }
        }

        gr.update();
        this.log.info('Updated ' + this.tableName + ': ' + sysId);
        return true;
    },

    delete: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            gr.deleteRecord();
            this.log.info('Deleted ' + this.tableName + ': ' + sysId);
            return true;
        }
        return false;
    },

    findBy: function(query) {
        var results = [];
        var gr = new GlideRecord(this.tableName);
        gr.addEncodedQuery(query);
        gr.query();

        while (gr.next()) {
            results.push(this._toObject(gr));
        }

        return results;
    },

    _toObject: function(gr) {
        var obj = {};
        var fields = new GlideRecordUtil().getFields(gr);

        for (var i = 0; i < fields.size(); i++) {
            var field = fields.get(i);
            obj[field] = gr.getValue(field);
        }

        return obj;
    },

    type: 'CRUDService'
};

Example 2: Complete GlideAjax Service

var FormHelperAjax = Class.create();
FormHelperAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    getUserDetails: function() {
        var userId = this.getParameter('sysparm_user_id');
        var result = { success: false };

        var gr = new GlideRecord('sys_user');
        if (gr.get(userId)) {
            result.success = true;
            result.data = {
                sys_id: gr.sys_id.toString(),
                name: gr.name.toString(),
                email: gr.email.toString(),
                department: gr.department.getDisplayValue(),
                manager: gr.manager.getDisplayValue(),
                phone: gr.phone.toString()
            };
        } else {
            result.error = 'User not found';
        }

        return JSON.stringify(result);
    },

    getCIDetails: function() {
        var ciId = this.getParameter('sysparm_ci_id');
        var result = { success: false };

        var gr = new GlideRecord('cmdb_ci');
        if (gr.get(ciId)) {
            result.success = true;
            result.data = {
                sys_id: gr.sys_id.toString(),
                name: gr.name.toString(),
                sys_class_name: gr.sys_class_name.getDisplayValue(),
                operational_status: gr.operational_status.getDisplayValue(),
                support_group: gr.support_group.getDisplayValue()
            };
        } else {
            result.error = 'CI not found';
        }

        return JSON.stringify(result);
    },

    validateEmail: function() {
        var email = this.getParameter('sysparm_email');
        var result = { valid: false };

        // Basic email regex
        var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        result.valid = emailPattern.test(email);

        if (result.valid) {
            // Check if email exists in system
            var gr = new GlideRecord('sys_user');
            gr.addQuery('email', email);
            gr.query();
            result.exists = gr.hasNext();
        }

        return JSON.stringify(result);
    },

    isPublic: function() {
        return true;
    },

    type: 'FormHelperAjax'
});

Related Skills

  • admin/script-sync - Local development and version control
  • admin/update-set-management - Track script changes
  • admin/application-scope - Scoped application development
  • admin/script-execution - Test scripts
  • catalog/variable-management - Using script includes in catalogs

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.