Useful Scripts

In my years as a ServiceNow developer, I’ve collected a large number of abstract, reusable scripts for various purposes. This practice has saved me more time and headaches than almost anything else I’ve done as a developer, because just about every interesting problem I’ve solved with code, I have inevitably had to solve twice.

On this page, you’ll find a few of the scripts I’ve saved over the years, and maybe a few scripts submitted by our most clever and dashing readers. The scripts you’ll find here, are one-off blocks of code that solve one problem as efficiently as they can. You won’t find detailed deep-dive explanations here, like you would in my articles on the blog; just examples, prototypes, and quick fixes to common coding conundrums. You also won’t find any of the scripts that I’ve already written articles with detailed explanations on, such as the Journal Redactor.

Most of these scripts will be specific to ServiceNow, but not all. Some are pure-JS. Some solve problems in ES5 that could easily be solved if we had ES6 on the server. Some aren’t even clever, but I use them so frequently that I figured I’d write them down.

This page is always under construction, so keep an eye on this page as I expand upon this repository. If you’d like to contribute your own scripts (and of course, be credited), please send me an email with your script, what it does, what problem it solves, and any other info you think I ought to know about it. Don’t forget to also let me know how you’d like to be credited (your name, and if you’d like, your website that I’ll link to).

Index

  1. Retrieve Decrypted Field Value from Password2 Field

  2. Add One Object to Another Object or Array

  3. Get Catalog Item Variables (Organized Into Containers)

  4. Get URI Parameters (Portal & Classic UI)

  5. Check if a Specific User Has Access to Something

  6. Check if a Given String Contains Any of a List of Values

  7. Query a Base Table - Get a Record/Fields on an Extended Table

  8. Use Different Credentials/Mid-servers/Endpoints if Instance is Prod Vs. Sub-Prod

  9. Retrieve the Body of a "text/plain" (or Other Unsupported) REST Message

  10. Format Object Output Using JSON.stringify() For Prettier Logs

  11. Get User's Date/Time Format and Validate Date/Time Field in Client

  12. Add Variables to Catalog Task From RITM Workflow Script

  13. Generate sys_history_set records from sys_audit data, after a clone, rebuild, or audit mod.

  14. Get Latest Journal Entry as HTML Without Header Info

  15. Populate an "Image" Field From a Script (Default)

  16. Display a Form in a Dialog Window (and Set Some Values)

  17. Show an Expanding Info Message with Functional Elements

  18. Get and Parse ServiceNow Journal Entries as Text/HTML

  19. Check if One Table is in the Hierarchy of Another (SCOPED)


Scripts

Retrieve Decrypted Field Value from Password2 Field

/*
    The following example retrieves and prints the value of
     a password2 from a basic auth profile, with the sys_id:
     "26da5b40db66e380eeb32ad94b96193e".
    Be careful with this! Passwords are obviously SENSITIVE!
*/
var encryptedPassword,
    decryptedPassword,
    Encrypter = new GlideEncrypter(),
    grBasicAuthProfile = new GlideRecord('sys_auth_profile_basic');

grBasicAuthProfile.get('26da5b40db66e380eeb32ad94b96193e');
encryptedPassword = grBasicAuthProfile.getValue('password');

decryptedPassword = Encrypter.decrypt(encryptedPassword);

gs.print(decryptedPassword);



/*****Client-callable example*****/
/*****GLIDEAJAX SCRIPT INCLUDE*****/
var PasswordDecryptor = Class.create();
PasswordDecryptor.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    getPasswordValue: function() {
        if (!gs.hasRoleExactly('admin')) {
            return;
        }
        
        var decryptedPassword;
        var tableName = this.getParameter('sysparm_table');
        var recordID = this.getParameter('sysparm_record_id');
        var fieldName = this.getParameter('sysparm_field_name');
        
        var geDecryptor = new GlideEncrypter();
        var grCredential = new GlideRecord(tableName);
        
        grCredential.get(recordID);
        
        decryptedPassword = geDecryptor.decrypt(
            grCredential.getValue(fieldName)
        );
        
        return decryptedPassword;
    }
});

/*****CLIENT SCRIPT*****/
function onLoad() {
    if (g_user.hasRoleExactly('admin')) {
        var gaDecryptor = new GlideAjax('PasswordDecryptor');
        gaDecryptor.addParam('sysparm_name', 'getPasswordValue');
        gaDecryptor.addParam('sysparm_table', g_form.getTableName());
        gaDecryptor.addParam('sysparm_record_id', g_form.getUniqueValue());
        gaDecryptor.addParam('sysparm_field_name', 'password'); //replace with password field name
        
        gaDecryptor.getXML(cbShowPassword);
    }
    
    /**
     *
     * @param response
     */
    function cbShowPassword(response) {
        var decryptedPassword = response.responseXML.documentElement.getAttribute('answer');
        g_form.showFieldMsg(
            'password', //Replace with password field name
            decryptedPassword,
            'info',
            false
        );
    }
}

Add One Object to Another Object or Array

/**
 * Adds one object to another, nesting the child object into the parent.
 * this is to get around javascript's immutable handling of objects.
 * @param name {String} - The name of the property of the parent object, in which to nest the child object. <br />For example, if the name parameter is set to "pickles" then "parent.pickles" will return the child object.
 * @param child {Object} - The object that should be nested within the parent object.
 * @param [parent={}] {Object} - The parent object in which to nest the child object. If the parent object is not specified, then the child object is simple nested into an otherwise empty object, which is then returned.
 * @returns {Object} - A new object consisting of the parent (or an otherwise empty object) with the child object nested within it.
 * @example
 * //sets myNewObject to a copy of originalObject, which now also contains the original (yet un-linked) version of itself as a child, under the property name "original"
 * var myNewObject = addObjToObj("original", originalObj, originalObj);
 */
function addObjToObj(name, child, parent) {
    if (!parent) {
        parent = {};
    }
    parent[name] = child;
    return parent;
}

//Here's another way you can add an object to an array or another object -- break the "pass-by-reference" model, by stringifying and then parsing the object.
function addObjToArray(arr, obj) {
    arr.push(JSON.parse(JSON.stringify(obj)));
    return arr;
}

//And here's an object that has a method for adding other objects to itself!
var myObj = {
    addObject: function(propName, obj) {
        this[propName] = JSON.parse(JSON.stringify(obj));
    }
};

//Remember that arrays are just numerically indexed objects, so this should all work with them too

Get Catalog Item Variables (Organized Into Containers)

//init
var CATALOG_ITEM_SYS_ID = '', //TODO: REPLACE THIS WITH THE SYS_ID OF THE CATALOG ITEM
    i,
    currentVar,
    fieldName = 'sys_id',
    varMap = {},
    currentContainer,
    containerLevel = [],
    item = GlideappCatalogItem.get(CATALOG_ITEM_SYS_ID),
    grVariables = item.getVariables();

//For every variable found...
while (grVariables.next()) {

    var varName = grVariables.getValue('name');
    var varSid = grVariables.getValue('sys_id');
    var varType = grVariables.type.getDisplayValue();
    var varQuestion = grVariables.getValue('question_text');
    var varActive = grVariables.getValue('active');

    if (varType === 'Container Start') {
        containerLevel.push(varSid);
    } else if (varType === 'Container End') {
        containerLevel.pop();
    }
    currentContainer = varMap;
    for (i = 0; i < containerLevel.length; i++) {
        if (!currentContainer.hasOwnProperty(containerLevel[i])) {
            currentContainer[containerLevel[i]] = {};
        }
        currentContainer = currentContainer[containerLevel[i]];
    }

    currentContainer[varSid] = new Variable(varName, varSid, varType, varQuestion, varActive);
}

gs.print(JSON.stringify(varMap));

function Variable(varName, varSid, varType, varQuestion, varActive, varOrder) {
    this.varName = varName;
    this.varSid = varSid;
    this.varType = varType;
    this.varQuestion = varQuestion;
    this.varActive = varActive;
    this.varOrder = varOrder;
}

Get URI Parameters (Portal & Classic UI)

//Note: Read from innermost, outward
getAllUrlParams(
    decodeURIComponent(
        getAllUrlParams( //Get all URL params. Since SN re-encodes everything it passes into page processors like "nav_to.do" for example, this will have one key-val pair.
                         (this.location.href ? this.location.href : window.location.href) //The document URL. Should work for SP, and desktop view.
        )['uri']
    )
);

var yourParamValue = getAllUrlParams(this.location.href)['YOUR_PARAM_NAME'];

/**
 * Get the values of each URI param as properties of an object (name-value pairs).
 * @param url
 * @returns {{}}
 * @example - In the URL www.google.com/page?urip=pickles&otheruriparam=bananas, you could use getAllUrlParams().urip to get "pickles".
 */
function getAllUrlParams(url) {
    
    // get query string from url (optional) or window
    var queryString = url ? url.split('?')[1] : window.location.search.slice(1);
    
    // we'll store the parameters here
    var obj = {};
    
    // if query string exists
    if (queryString) {
        
        // stuff after # is not part of query string, so get rid of it
        queryString = queryString.split('#')[0];
        
        // split our query string into its component parts
        var arr = queryString.split('&');
        
        for (var i = 0; i < arr.length; i++) {
            // separate the keys and the values
            var a = arr[i].split('=');
            
            // in case params look like: list[]=thing1&list[]=thing2
            var paramNum = undefined;
            var paramName = a[0].replace(/\[\d*\]/, function(v) {
                paramNum = v.slice(1, -1);
                return '';
            });
            
            // set parameter value (use 'true' if empty)
            var paramValue = typeof(a[1]) === 'undefined' ? true : a[1];
            
            // (optional) keep case consistent
            paramName = paramName.toLowerCase();
            paramValue = paramValue.toLowerCase();
            
            // if parameter name already exists
            if (obj[paramName]) {
                // convert value to array (if still string)
                if (typeof obj[paramName] === 'string') {
                    obj[paramName] = [obj[paramName]];
                }
                // if no array index number specified...
                if (typeof paramNum === 'undefined') {
                    // put the value on the end of the array
                    obj[paramName].push(paramValue);
                }
                // if array index number specified...
                else {
                    // put the value at that index number
                    obj[paramName][paramNum] = paramValue;
                }
            }
            // if param name doesn't exist yet, set it
            else {
                obj[paramName] = paramValue;
            }
        }
    }
    
    return obj;
}

Check if a Specific User Has Access to Something

var secManager = new GlideSecurityManager.get().setUser(gs.getUser().getUserByID('user_sys_id'));
var checkType = 'record';
var checkTable = 'incident';
var checkOperation = 'read';
var userSysID = '' //todo: Specify user Sys ID
if (hasRights(checkType, checkTable, checkOperation, userSysID)) {
    //todo: do stuff if the user has permissions
} else {
    //todo: do stuff if the user doesn't have permissions
}

function hasRights(checkType, checkTable, checkOperation, userSysID) {
    var secManager = new GlideSecurityManager.get().setUser(gs.getUser().getUserByID(userSysID));
    return secManager.hasRightsTo(
        checkType + '/' + checkTable + '/' + checkOperation //This string has the format ['record', 'table' or 'field']/[table name, such as 'incident']/[operation: read/write/delete/report]
    );
}

Check if a Given String Contains Any of a List of Values

/**Checks whether a given string contains any of the values in a specified array or comma-separated list.
 * @param stringToCheck {string} The string to check against the list of values to check for.
 * @param listOfValuesToCheckFor {object|string} An array or comma separated string, containing a list of strings to check. If any of these strings
 * @returns {object} An object with two properties: answer (a boolean indicating whether any string from the second arg was found in the first) and values (an array of the values from the second arg, that were found in the first).
 */
function stringContains(stringToCheck, listOfValuesToCheckFor) {
    var i;
    var response = {
        answer: false,
        values: []
    };
    //If the subProdKeywords variablr is a string with at least one character, convert it to an array.
    if (typeof listOfValuesToCheckFor == 'string' && listOfValuesToCheckFor.length > 0) {
        listOfValuesToCheckFor = listOfValuesToCheckFor.split(',');
    }
    else if (typeof listOfValuesToCheckFor !== 'object' || !(listOfValuesToCheckFor.length > 0)) {
        //If the first condition (subProdKeywords is a string) is NOT met, then we expect an object (array) with at least one value.
        //If either of the mentioned conditions is not met and the value was not a string, then we return undefined and halt execution while logging the error.
        gs.warn('Invalid value or data type passed into stringContains() method, or value is null-length. Value passed in: ' + listOfValuesToCheckFor);
        return undefined;
    }
    for (i = 0; i < listOfValuesToCheckFor.length; i++) {
        if (stringToCheck.indexOf(listOfValuesToCheckFor[i])) {
            response.values.push(listOfValuesToCheckFor[i]);
            response.answer = true;
        }
    }
    return response;
}

Query a Base Table - Get a Record/Fields on an Extended Table

var grChildTable;
var childFieldValues = [];
var baseTable = 'cmdb_ci'; //A base table that's extended
var childTableFieldName = 'nic'; //The name of a field on an extended table
var grBaseTable = new GlideRecord('cmdb_ci');

grBaseTable.addQuery('some_query');
grBaseTable.query();

while (grBaseTable.next()) {
    grChildTable = new GlideRecordUtil().getGR(
        grBaseTable.getValue('sys_class_name'), //Table name 
        grBaseTable.getValue('sys_id') //sys_id
    );
    
    childFieldValues.push(
        grChildTable.getValue(childTableFieldName)
    );
}

Use Different Credentials/Mid-servers/Endpoints if Instance is Prod Vs. Sub-Prod

/**
 * Checks the glide.installation.production property and returns true if the instance is production. <br/>
 * Also checks the instance name, and logs if there is a discrepancy between the instance name and glide.installation.production property.
 * @param [defaultValue=true] {boolean}
 * @returns {*}
 */
function isProd(defaultValue) {
    //set default value for defaultValue parameter to true
    defaultValue = (typeof defaultValue !== 'undefined') ? defaultValue : true;
    
    var prodProp = isProdPropTrue(defaultValue);
    var instanceNameProd = isInstanceNameProd();
    
    if ((prodProp && !instanceNameProd) || !prodProp && instanceNameProd) {
        gs.logError(
            'ERROR: INSTANCE NAME AND glide.installation.production PROPERTY DISAGREE ABOUT WHETHER THIS INSTANCE IS PRODUCTION.',
            'isProd Script Include'
        );
    }
    
    return prodProp;
    
    
    function isProdPropTrue(defaultValue) {
        //Hey look, JS's lack of strict typing comes in handy for once!
        //even if only to fix problems that it causes...
        var thingsThatAreTrue = [true, 'true', 1, '1'];
        var isProdProp = gs.getProperty('glide.installation.production', defaultValue);
        
        return (thingsThatAreTrue.indexOf(isProdProp) >= 0);
    }
    
    function isInstanceNameProd() {
        var instanceName = gs.getProperty('instance_name', null);
        var isDev = (instanceName.indexOf('dev') >= 0);
        var isTest = (instanceName.indexOf('test') >= 0);
        //If you have OTHER sub-prod instances like "qa", add those conditions too
        
        return (!isDev && !isTest);
    }
}

Retrieve the body of a "text/plain" (or other unsupported) REST message

var RESTPlainTextUtil = Class.create();
RESTPlainTextUtil.prototype = {
    initialize: function() {
    },
    
    /**
     * @description Get the body of a text/plain REST request as a string.
     * @param {sn_ws.RESTAPIRequest} request The RESTAPIRequest object (the whole request object, not just the body).
     * @param {boolean} [trim=false] Whether to trim the resulting body string before returning it.
     * @returns {string} The request body as a string. If the trim argument is specified and set to a truthy value, the request body will be trimmed before being returned.
     */
    getTextBody: function(request, trim) {
        var reqBodyLine;
        var reqBodyString = '';
        var streamBody = request.body.dataStream;
        var gtrRequestBodyReader = new GlideTextReader(streamBody);
        
        //get first line
        reqBodyLine = gtrRequestBodyReader.readLine();
        while(typeof reqBodyLine == 'string') {
            reqBodyString += reqBodyLine + '\n';
            
            //get next line
            reqBodyLine = gtrRequestBodyReader.readLine();
        }
        
        return trim ? reqBodyString.trim() : reqBodyString;
    },
    
    type: 'RESTPlainTextUtil'
};

Format Object Output Using JSON.stringify() For Prettier Logs

var exampleObj = {
    "name": "Tim",
    "age": 31,
    "groovy": true,
    "dogs": [
        {
            "name": "Ezri",
            "age": 1
        },
        {
            "name": "Seven",
            "age": 3
        }
    ]
};

gs.info(
    'Logging JSON objects can be very useful, but they need to be stringified first. If we ' +
    'just stringify them in the usual way, we get something that\'s kind of hard to read, like this: \n' +
    '{0}\n\n' +
    'But, we can do better, by specifying some additional parameters to the .stringify() method like ' +
    'so: JSON.stringify(exampleObj, null, 2) : \n{1}',
    JSON.stringify(exampleObj),
    JSON.stringify(
        exampleObj,
        null, //A function or array to filter/replace some values of the object (useful if
        // you want to log an object, but not log potentially sensitive elements)
        2 //The number of spaces by which to indent child-elements. You can also specify a
        // string or escape character such as '\t' for tabs.
    )
);
/*
    Output: Logging JSON objects can be very useful, but they need to be stringified first. If we just
    stringify them in the usual way, we get something that's kind of hard to read, like this:
    {"name":"Tim","age":31,"groovy":true,"dogs":[{"name":"Ezri","age":1},{"name":"Seven","age":3}]}
    
    But, we can do better, by specifying some additional parameters to the .stringify() method like
    so: JSON.stringify(exampleObj, null, 2) :
    {
      "name": "Tim",
      "age": 31,
      "groovy": true,
      "dogs": [
        {
          "name": "Ezri",
          "age": 1
        },
        {
          "name": "Seven",
          "age": 3
        }
      ]
    }
*/

//More on the stringify method here:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

Get User's Date/Time Format and Validate Date/Time Field in Client Script

/**
 * Client Script (or Catalog Client Script)
 * Validates that a 'date' or 'date/time' field is no more than a number of miliseconds ago equal to the value of msAgo.
 * For example, to check that the value entered in the "some_date_time" field is no more than 1 hour ago, you would call:
 * validateChronoFieldValue("some_date_time", true, 3600000);
 *
 * @param {string} fieldName - The name of the date or date/time field to check.
 * @param {boolean} dateTime - If the field is a "Date" field, set this param to false. If it is a "Date/time" field, set this param to true.
 * @param {number} msAgo - An integer representing the number of miliseconds ago that is the maximum amount in the past that the field value can be set to. For example, one hour ago would be 3600000.
 * @param {boolean} [preventFutureDates=true] - Optionally, if you would like to ALLOW future dates (thus causing the function to return true if the selected date is in the future), set this param to false.
 * @param {boolean} [returnTrueIfEmpty=false] - Optionally, set this parameter to true if you'd like for this function to return true if the specified field is empty or not found.
 *
 * @returns {boolean} - True if the field's value is valid, or if it is empty and the returnTrueIfEmpty parameter has been set to true.
 */
function validateChronoFieldValue(fieldName, dateTime, msAgo, preventFutureDates, returnTrueIfEmpty) {
    //Set default value of returnTrueIfEmpty.
    returnTrueIfEmpty = (typeof returnTrueIfEmpty == 'undefined') ? false : returnTrueIfEmpty;
    preventFutureDates = (typeof preventFutureDates == 'undefined') ? true : preventFutureDates;
    
    var fieldVal = g_form.getValue(fieldName);
    
    if (!fieldVal) {
        return returnTrueIfEmpty;
    }
    
    var userFormat = dateTime ? g_user_date_time_format : g_user_date_format;
    
    var currentDateObj = new Date();
    var currentDateStr = formatDate(currentDateObj, userFormat);
    var currentDateNum = getDateFromFormat(currentDateStr, userFormat);
    var selectedDateNum = getDateFromFormat(fieldVal, userFormat);
    
    var epochTimeAtMsAgo = currentDateNum - msAgo;
    
    return !(
        (selectedDateNum < epochTimeAtMsAgo) ||
        (preventFutureDates && (selectedDateNum > currentDateNum))
    );
}

Add Variables to Catalog Task From RITM Workflow Script

function addVars(taskSysid) {
    var variablesForTask = ['var1', 'var2', 'var3', 'var4'];
    //could be optimized by building a list, then iterating through that list to insert.
    var grTaskVariable = new GlideRecord('sc_item_variables_task');
    var grVariableM2M = new GlideRecord('sc_item_option_mtom');
    
    grVariableM2M.addQuery('request_item', current.sys_id);
    grVariableM2M.addQuery('sc_item_option.item_option_new.name', 'IN', variablesForTask.join(','));
    grVariableM2M.query();
    while (grVariableM2M.next()) {
        grTaskVariable.initialize();
        grTaskVariable.setValue('task', taskSysid);
        grTaskVariable.setValue('variable', grVariableM2M.sc_item_option.item_option_new.toString());
        grTaskVariable.insert();
    }
}

Generate History Set (sys_history_set Without Viewing a Record

This script came out of the following conversation on the community Slack (or Discord; I forget):

I am looking for a way to generate sys_history_set records for a given task, without the user having to actually visit the task.
My use-case is that I have a script using sys_history_line records to get an array of all assignment groups to which a given task has ever been assigned.
However, if the record was updated programmatically but not "viewed", the history sets/lines are not generated.
I am also open to alternative solutions, such as a more robust mechanism by which to determine all of the groups to which a task has ever been assigned.
I have tried using the sys_audit table, but there are circumstances where even this table is not populated (such as after a clone... an extreme edge case, I know)... And, I know that sys_history is built from sys_audit, but I still want to see if there's a way to trigger the generation of the sys_history_set/line records programmatically, or if that code is completely black-boxed.

var hw, grI = new GlideRecord('incident');
grI.get('88fa35901b9dd058e253eac6bc4bcb32');
var hw = new sn_hw.HistoryWalker(grI.getTableName(), grI.getUniqueValue(), false); //last arg must be false to generate history set
gs.info(hw.walkTo(0)); //If true, walk was successful and history set is generated.

Get Latest Journal Entry as HTML Without Header Info

var journalFieldName = 'comments';
var journalText = current[journalFieldName]
    .getJournalEntry(1)
    .trim()
    .split('\n')
    .slice(1)
    .join('<br />\n');

Populate an "Image" Field From a Script (Default)

Display a Form in a Dialog Window (and Set Some Values)

Show an Expanding Info Message with Functional Elements

Get and Parse ServiceNow Journal Entries as Text/HTML

Check if One Table is in the Hierarchy of Another (SCOPED)

NOTE: This method only works in scoped apps. For a Global-scope alternative, use .getHierarchy().

/**
 * Returns true if tableName is a child of parentTableName, or false otherwise.
 * @param {string} tableName - The table to check. Must be a string. Will be converted to
 * lowercase. Will return true if this table is a child of parentTableName.
 * @param {string} parentTableName - The parent table to check. Must be a string. Will be
 * converted to lowercase. Will return true if tableName is a child of this table.
 * @returns {boolean} - True if tableName is a child of parentTableName, or false otherwise.
 * 
 * @example
 * //Returns true
 * isTableInHierarchy('incident', 'task');
 * @example
 * //Returns false
 * isTableInHierarchy('task', 'incident');
 * @example
 * //returns true
 * isTableInHierarchy('incident', 'incident');
 * @example
 * //returns false if EITHER table is invalid
 * isTableInHierarchy('incident', 'invalid_table_name');
 * @example
 * //Prints true to the log
 * var tableToCheck = 'sc_req_item';
 * var parentTableToCheck = 'task';
 * gs.info(
 *     'Is table ' + tableToCheck + ' a child of table ' + parentTableToCheck + '?: ' +
 *     isTaskTable(
 *         tableToCheck,
 *         parentTableToCheck
 *     )
 * );
 */
function isTableInHierarchy(tableName, parentTableName) {
    var tableUtil, tableParentHierarchy;

    tableName = (typeof tableName === 'string') ? tableName.toLowerCase() : '';
    parentTableName = (typeof parentTableName === 'string') ? parentTableName.toLowerCase() : '';

    if (!tableName || !parentTableName) {
        throw new Error(
            'isTableInHierarchy(): tableName and parentTableName must be specified ' +
            'and set to strings.'
        );
    }

    tableUtil = new GlideTableHierarchy(tableName);
    tableParentHierarchy = tableUtil.getTables();

    return (tableParentHierarchy.indexOf(parentTableName) >= 0);
}