Script Extension in ServiceNow

One of the great powers (And great responsibilities) of the ServiceNow platform is it’s great Script Extension in ServiceNow capabilities. In this article, we will go over the different ways you can allow customers to OVERRIDE or “Extend” your scripts and functionality, while still maintaining a easy way in which your customers can ‘Revert to” or test “Baseline”.

Base “App” Script include, with “Customer Modifiable” Extended Script Include

  • Extension must be done within the same application scope as your application
  • Fastest to Execute at “runtime”
  • Easy to setup as App Developer (The *Get out of jail free card*)
  • Most Flexible
  • Widest amount of difficulty levels for modifying by customers (from “Easy” to “What have I got myself into?”)

Script Extension Points

  • You want to allow your customers to extend your scripts from the same or other scopes
  • A little bit slower to Execute at “Runtime” and can run out of control if there are many “Customer Extensions”
  • Requires Forethought by App Developer
  • Moderate Flexibility
  • Easy – Moderate difficulty for modifying by customers, as you provide an example script and a “focused” output/function

Script Fields on Custom Application Files

Stay tuned! For a blog post detailing the usage of Script fields on Application Files, GlideScopedEvaluator and advanced uses for that!

  • You want to allow your customers to extend your scripts from the same or other scopes
  • Similar Execution time to “Script Extension Points”
  • Requires the most Forethought by the App Developer (You’ll *know* when you want to use this)
  • Similar flexibility as Extension points, but allows App Developer and customer more fine tuned control.
  • Easy – Moderate Difficulty for modifying by customers.

App Script Include with “Customer Modifiable” extended script include

In this scenario, we will go over the design pattern for setting up a Read-Only script include that is controlled by You the app developer and an Editable script include that can be used to Override methods configured in the App provided Script Include.

The goal here is to have a way for customers to do whatever they need to override your script include functionality, and still give a way to fallback to baseline functionality by removing or commenting out the script include methods they overwrote.

When to use?

  • You want a fast and easy way for customers to override a function you created, setting up their custom behavior

Pros

  • Requires little effort from the app developer to give you (through supporting your customers) and your customers a “Get out of Jail” card at implementation time.
  • Allows ability to completely override a function being called and do whatever they want in there.
  • Fastest possible execution times.

Cons

  • If customer wants to modify a single point in your script include function, they must interpret and understand the entirety of that function (It doesn’t allow us as app devleopers to be specific like a scripted extension point)
    • Yes, yes, I know you can DESIGN your script include that way, and this is totally fine too.
  • Customers must fully understand your code to provide and ensure they are providing the correct output expected by other code.

The Scenario / Requirements

As an implementer of this application, there should be a way for the system to automatically import Artists Titles and information utilizing the Soptify API. This should load the artists using the Search API and allow customers to modify the behavior of how artists are searched.

  • As an app developer: We need a script include that will load Artists data from Spotify and save that into our Database. We also need a way for customers to modify this behavior and wish to allow them the most flexibility.
  • As a Customer: We need a way to modify how the artist information retrieved from Spotify and Saved into the Database. Such as expanding on the attributes being loaded.

Our Script Include Layout

Below is the architectural layout of this design pattern. Please take note of the Do’s and Don’ts when using this methodology.

Do

  • Do Put all of your code and methods in the Read-Only _APP script include.
  • Do Call the Customer Editable script include in your code outside these script includes
    • Yes: new x_yala_blog_core.MusicGenerator().loadArtists()
    • No: new x_yala_blog_core.MusicGenerator_APP().loadArtists()

Don’t

  • Don’t Put any of your Application code in the Customer Editable script include
  • Don’t put an “initialize” method in your Customer Editable script include.
    • Exception: You’ll know when you want to put one in there (See advanced concepts)
  • Don’t Call your Read-Only Base script include in ANY of your code. Ever. For Reals.

So How do I Set one of these up?

Below is a set of sample Script Includes on how these should look in your app, and how they will look when delivered to your Customer.

How should I write my “Read-Only _APP” Script Include?

You should write it like any other script include. However, you should make sure you follow naming consistency in this model and use a suffix like _APP to indicate to you and your customer what is “Application Provided”.

Example

This is how your script might look on First Installation of your application

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

    initialize: function () {
        //do nothing
    },
    
    /**
     * Load artists from Spotify, using their Search function to get multiple artists at once.
     */
    loadArtists: function () {
        var maxCharacters = 26;

        //go through all 26 characters in the alphabet, searching for Artists using that letter
        for (var i = 0; i < maxCharacters; i++) {
            var character = (i + 10).toString(36);

            //Check to see if our loading job has been cancelled
            var cancelledResult = this._haveBeenCancelledCheck();
            if (cancelledResult) {
                break;
            }

            //make our API Call to search for artists.
            var artists = this.searchToMaxAmount(character, 'artist');

            //loop through the artists found, checking to see if we've imported them yet, or insert new
            artists.forEach(function (artist) {
                var artistGR = new GlideRecord('x_yala_blog_core_artist');
                artistGR.addQuery('correlation_id', artist.id);
                artistGR.query();
                if (!artistGR.hasNext()) {
                    artistGR.newRecord();
                    artistGR.name = artist.name;
                    artistGR.correlation_id = artist.id;
                    artistGR.insert();
                } else if (artistGR.next()) {
                    artistGR.name = artist.name;
                    artistGR.correlation_id = artist.id;
                    artistGR.update();
                }
            });

        }
    },

    type: 'MusicDataGenerator_APP'
};

Note: This is simply a snippet of this script include, the other functions called can be reviewed in the Full App (See instructions for install this app to test/demo yourself!)

What should my “Customer Editable” script include look like?

This is how the script include will look on First Installation of your application

In the Customer Editable script include, we Extend our Read-Only _APP script include. What this does is “Copy” that script include, into this one at Runtime . It means when we use this script include, we’ll have access to all the methods and properties of that _APP script include.

We can then override those Methods and Properties once installed on a Customer environment (See Customer override example below)

Example

var MusicDataGenerator = Class.create();

//use ServiceNows extendsObject functionality, to take our Base Script Include, and copy it into this one.
MusicDataGenerator.prototype = Object.extendsObject(x_yala_blog_core.MusicDataGenerator_APP, {

	type: "MusicDataGenerator"
});

So what in this example is doing the magic?

MusicDataGenerator.prototype = Object.extendsObject(x_yala_blog_core.MusicDataGenerator_APP, {

This line here is essentially saying, on my MusicDataGenerator class / include, first copy all of the functions/properties from MusicDataGenerator_APP onto my script include. Then, the second parameter we tell it what methods / functions to overlay / copy onto our copy. If we name a function or property the same, it will effectively override what was copied in from the _APP script include.

I’ve followed your pattern, how do I actually use this script include now?

In short, use it like any other script include. The only thing to keep in mind:

  • When using this design pattern, it is Imperative that we DO NOT call our Read Only _APP script include, otherwise customers will not be able to override functionality.

Instead, we want to make sure to call our Customer Editable Extension.

Example (Asynchronous Business Rule)

Here is an example Async business rule, set to run when a record changes it’s state to “Queued”

(function executeRule(current, previous /*null when async*/) {

    //because we're async, update our current record so it shows in_progress
    current.state = 'in_progress';
    current.update();

    //call our "Customer Editable" script include, remember our _APP script include will be copied into this one
    var mdg = new x_yala_blog_core.MusicDataGenerator();

    /** 
     * call our loadArtists method we wrote in the _APP script include. 
     * If a customer has written this method in the Customer Editable script include, 
     * it will run theirs instead! 
     */
    mdg.loadArtists();


})(current, previous);

How does my Customer override our functionality?

In order for a customer to override your functionality they will need to know/do the following.

  • Copy/Paste the method they wish to override from the _APP script include to the Customer Editable script include
    • Alternatively, they can manually write the method name and code manually.
  • Modify the Method as needed
  • Save their script include
  • Test their Changes

So let’s say your customer has determined they want to change the search behavior to search from a list of “Search terms” instead of just going through the alphabet.

They can override your loadArtists call in the following fashion:

Example

var MusicDataGenerator = Class.create();
MusicDataGenerator.prototype = Object.extendsObject(x_yala_blog_core.MusicDataGenerator_APP, {

    loadArtists: function () {
		//we have a table of search terms making our list of search terms to look for.
        var searchTerms = new GlideRecord('x_yala_blog_extscr_artist_search_terms');
		searchTerms.addActiveQuery();
		searchTerms.query();

		//loop through our search terms found, and search for artists based on that term
		while(searchTerms.next()){
			/** 
			 * NOTE: we can still use functions from our "Read-Only _APP" script include 
			 * because they've been *copied* into this one.
			 */ 
            var cancelledResult = this._haveBeenCancelledCheck();
            if (cancelledResult) {
                break;
            }

			var searchTermText = searchTerms.getValue('search_term');

            //make our API Call from the "Read-only Base" to search for artists.
            var artists = this.searchToMaxAmount(searchTermText, 'artist');

            //loop through the artists found, checking to see if we've imported them yet, or insert new
            artists.forEach(function (artist) {
                var artistGR = new GlideRecord('x_yala_blog_core_artist');
                artistGR.addQuery('correlation_id', artist.id);
                artistGR.query();
                if (!artistGR.hasNext()) {
                    artistGR.newRecord();
                    artistGR.name = artist.name;
                    artistGR.correlation_id = artist.id;
                    artistGR.insert();
                } else if (artistGR.next()) {
                    artistGR.name = artist.name;
                    artistGR.correlation_id = artist.id;
                    artistGR.update();
                }
            });
		}
    },
	type: "MusicDataGenerator"
});

Scripted Extension Points

An underused feature in ServiceNow, and a somewhat newish feature is the ability to create Scripted Extension Points. In this scenario, we will show case how Script Extension points can be utilized to provide your customer a focused piece of scripting functionality where they can plugin whatever they want. Then, you can control and validate that response and leverage it in your own code.

When To Use?

  • You do not need an additional table, with metadata, to be the source of your script execution
  • You want to allow customers to have a place to modify the behavior of your code, and the use cases are few.
  • For example, when someone uses an extension point, those extension points have to be evaluated EVERY time your script include / function is called that uses them, and their “Applies to” has to be evaluated.
  • Like a “case” statement without breaks, for “fall through logic” if you’re not careful..

Pros

  • Can pre-define script and give an example of what their script include should look like
  • Maintains scope matching the application the scripted extension implementation is made in
  • Does not need another “Table” to allow custom functionality in your code
  • Has the ability to let you modify the default “extension point example script”, so that if you need to deploy updates that script example and methods can be provided, and then subsequently customers can more easily adapt to them.

Cons

  • Will slow execution time, as their is added Overhead to query to see if scripted extensions exist and load them all into memory, then you have to loop through them all “checking your AppliesTo”
    • Note: This is not a significant amount of lost performance, but if you have a While/For loop that could call this extension point more than a few hundred times in one go, you should consider the Script Extension method above
  • Limited to where *you as the developer* could think of places the customer would want to “Change/modify” behavior via script
  • Requires that an “appliesTo” or “handles” function of some kind is added, and called
    • Every “Implementation” of your script extension is loaded, in order, with no way to tell them apart
    • This means you have to evaluate every single one to see if it “applies” or not, and handle throwing errors if the scripted extension point does not have the necessary methods, and/or does not “apply”

The Scenario / Requirements

  • Your app provides a baseline way to allow customers to blacklist tracks, by sys_id, in a System Property
  • You want to provide a way customers can write any script they desire, have it evaluated by you, and change the behavior of how the tracks blacklist is determined.
    • You have chosen to design in a Scripted Extension point in a script include you use to determine the list of Track sys_ids that should be in the blacklist.
    • You have chosen to provide a way for customers to determine if their Extension point should be executed or not

Where do I start?

It can sometimes be difficult to understand where you should start when writing your extension point. I’ve often found that this usually comes AFTER writing your baseline functionality so it is recommended to:

  • Write your script include and default business logic first.
  • Identify in your code / script include where you might want your Scripted Extension point to go and what the “Outputs” should be.

You said this requires more forethought?

Correct, when using Scripted Extension Points you need to take into account the following considerations.

  • What does your code do when no Scripted Extension Points are defined? Is there a default behavior?
  • What does your code do if the Scripted Extension Point is not designed in the way you expected?
    • Did they return the value / type you expected? (String, number, GlideRecord, Array, Object, etc)
    • Did their extension have the correct functions you needed? (Did they diverge from the pattern in your Script Extension Point definition?)
  • Should your code take the first Extension point that matches?
  • Should it Accumulate the results of all of them?

Let’s get into the details.

Lets get to setting up our Scripted Extension Point, modifying our existing Script Include, and showcasing an external application modifying our behavior by creating an Extension Implementation.

Our Initial Black List Script Include

Below you’ll see that we have a very simple approach to how the Black List of tracks is being managed. It is simply a comma separated list in a system property, converted into an Array. Over time we have found many customers wanting to override this functionality.

Instead of customers needing to understand the entire code base, we want to provide a scripted Extension point with a well defined Input and Output for the customer to consume and for us to overlay back into our application.

var BlackLists = Class.create();
BlackLists.prototype = {
    initialize: function () { },

    /**
     * Get the blacklist track IDs.
     * @returns {string[]} an array of sys_id strings of tracks that should be in the black list.
     */
    getTrackIDs: function () {
        var blackList = [];
        //should be comma separated list.
        var propIds = gs.getProperty('x_yala_blog_core.track.black_list');
        if (propIds) {
            //we do a test, as splitting an empty string, creates an array with one item in it still..
            blackList = propIds.split(',');
        }

        return blackList;
    },

    type: 'BlackLists'
};

Setting up our Extension Point

To setup our extension point, we have 2 primary things to decide on and know that this is not the limits at all.

What methods should I put in my Extension Point?

While it is possible to have any number of methods in here, that you can then call in your code using the Extension Point, it is recommended to keep in minimal, for example:

  • Should we have an AppliesTo function?
  • What is the name of our primary method that customers will need to implement?

In our example

  • We use an AppliesTo function, where customers will be able to put their custom code to return back a boolean on whether or not we should continue processing their extension point, and subsequently calling our getBlacklist method.
  • We have a getBlacklist method, where customers will be able to put their custom code to determine the sys_ids in the blacklist and return it back to us.
var PlaylistBlacklist = Class.create();
PlaylistBlacklist.prototype = {
    initialize: function() {
    },
	
	appliesTo: function(){
		//Does this extension point (after you create an implementation) apply and should we run the getBlacklist method?
		return true;
	},

    getBlacklist: function() {
		//A list of track sys_ids to exclude when querying the tracks table.
        var songSysIds = [];
		return songSysIds;
    },

    type: 'PlaylistBlacklist'
};

Calling our Extension Point

Here we can see the usage of calling ServiceNows “Extension Point” API. When called, it will return ALL Implementations of that Extension Point that are Active in Order. This will be an Array of instantiated Script Includes for each Extension Point.

So what would this look like in code?

  • We ask SN for any Implementations in our Extension Point.
  • If we do not find any, we default to our original behavior (a system property)
  • If we do find Extension Points, check to see if they “Apply” and we should get their Blacklist.
  • Check to make sure the getBlacklist function returned an array.
  • Concat all of the Extension Point blacklists together and finally return the blacklist IDs.

Example

var BlackLists = Class.create();
BlackLists.prototype = {
    initialize: function () { },

    /**
     * Get the blacklist track IDs.
     * 
     * @returns {string[]} an array of sys_id strings of tracks that should be in the black list.
     */
    getTrackIDs: function () {

        var blackList = [];

        //returns an Array of Instantiated Script Includes
        var extensionPoints = new GlideScriptedExtensionPoint().getExtensions("x_yala_blog_core.PlaylistBlacklist");

        //Always include our system property in the black list.
        var propIds = gs.getProperty('x_yala_blog_core.track.black_list');
        if (propIds) {
            blackList = propIds.split(',');
        }

        //If we have extension points, add any that "Apply" to our blacklist.
        if (extensionPoints.length > 0) {

            //this is an array of Instantiated Script Includes/Classes from your extension point.
            extensionPoints.forEach(function (extPoint) {
                if (!extPoint.appliesTo || !extPoint.getBlacklist) {
                    gs.error('Missing appliesTo function or getBlacklist function... please ensure your extension point is setup correctly. Ignoring this implementation for now.');
                } else if (ep.appliesTo && ep.getBlacklist) {
                    
                    //check to see if this Implementation "applies" and we should get it's blacklist.
                    if (ep.appliesTo()) {
                        var blackListSysIds = ep.getBlacklist();
                        if (Array.isArray(blackListSysIds)) {
                            blackList = blackList.concat(blackListSysIds);
                        } else {
                            gs.error('Returned black list was not an array. Please ensure it is an array of sys_ids. Ignoring blacklist.');
                        }
                    }
                }
            });
        }

        return blackList;
    },

    type: 'BlackLists'
};

Okay, I’ve got my Script Include, and My Extension Point, where does this get used now?

Now that you have your script include updated with your Extension Point you can use your Script Include just as you normally would! Call it in Business Rules, UI Actions, wherever you need to “Get a List of Backlisted Track IDs”.

In the companion Application (see above) for this, you will see we use it in the Custom Relationship for “Songs” on a Playlist. System Definition > Relationships > “Song” relationship record.

How do my customers add Implementations or “Extensions”.

In order for customers to add extensions or “Implementations” to your extension point, they will need to:

  1. Navigate to System Extension Points > Scripted Extension Points
  2. If you have not provided a list to your customer, they can find the Extension Points for your app using the Application column
    • Ours is: x_yala_blog_core.PlaylistBlacklist
  3. Have them open the form.
  4. NOTE: Before going further, make sure they have set their current Application Picker / Scope to the desired scope of the Implementation.
    • This can be useful / is required if wanting to access data or call code “Cross-Scope”
  5. Use the Create Implementation related link in the bottom left of the form.
    • This will automatically create a Script Include in your current app scope, and associate that script include as an “Implementation” to the Extension Point.
  6. After clicking that action, ServiceNow will take you to the newly created Script Include to write your extension point functionality.
  7. Update the appliesTo function as needed
  8. Update the getBlacklist function to return a list of track sys_ids to be in the blacklist.
  9. Test your changes by trying to create a playlist that includes those tracks.

Try it out!

We have created a scoped app that is available on GitLab for you to Import via Source Control into your Developer Instance (PDI). There you will be able to review all of the code, and test the functionality out described in here!

If you are not familiar with Importing from Source Control, please visit the ServiceNow Developer portal, and the training regarding that.

Note: These are public repos, and DO NOT require credentials / authentication in order to import into your PDI.

Setup

  1. Make sure you install Yansa Blog – CORE and follow it’s setup instructions on the GitLab page
  2. Load the Artist, Album, Track Data in CORE
  3. Install the Yansa Blog – Extensible Scripts Application
  4. Review the code mentioned in this blog post in the Extensible Scripts application.