Scripted Value Columns, Enhanced, Part II

“Alone we can do so little; together we can do so much.”
Helen Keller

Last time, we tinkered with the Scripted Value Columns section of the SNH Data Table Widgets, and created an example Task list using an aggregation of labor hours as a demonstration of how you can include HTML with your returned value. Today we will have a look at another scripted column in that example, the Customer column.

There are quite a number of extensions to the core Task table in ServiceNow, all of which share the same core list of fields. One of those fields is called opened_by, and identifies the person who was signed on to the system when the task was initially opened. That person is not necessarily the customer, though. In the case of an Incident, for example, that is more likely to be a call center agent who took the call, not the person who is reporting the issue. There is no core table field to represent the customer. Each extension of the Task table has its own way of identifying the person awaiting the completion of the task. Using the scripted value column feature, though, you can create a “customer” column by looking at the type of task, and then grabbing the value of the field that is appropriate for that particular type.

getScriptedValue: function(item, config) {
	var response = '';

	var table = item.sys_class_name.value;
	var field = 'opened_by';
	if (table == 'incident') {
		field = 'caller_id';
	} else if (table == 'sc_request') {
		field = 'requested_for';
	} else if (table == 'sc_req_item') {
		field = 'request.requested_for';
	} else if (table == 'sc_task') {
		field = 'request_item.request.requested_for';
	} else if (table == 'change_request') {
		field = 'requested_by';
	}
	var taskGR = new GlideRecord(table);
	if (taskGR.get(item.sys_id)) {
		if (taskGR.getValue(field) > '') {
			response = taskGR.getDisplayValue(field);
		}
	}

	return response;
}

This particular example handles incidents, catalog activity, and change requests, but could easily be modified to handle any number of other types of extensions to the core task table. This just returns the value, though. If you wanted to make the field a link to the user profile page, for example, you could do something like this.

getScriptedValue: function(item, config) {
	var response = '';

	var table = item.sys_class_name.value;
	var field = 'opened_by';
	if (table == 'incident') {
		field = 'caller_id';
	} else if (table == 'sc_request') {
		field = 'requested_for';
	} else if (table == 'sc_req_item') {
		field = 'request.requested_for';
	} else if (table == 'sc_task') {
		field = 'request_item.request.requested_for';
	} else if (table == 'change_request') {
		field = 'requested_by';
	}
	var taskGR = new GlideRecord(table);
	if (taskGR.get(item.sys_id)) {
		if (taskGR.getValue(field) > '') {
			var sysId = taskGR.getValue(field);
			var name = taskGR.getDisplayValue(field);
			response += '<a href="?id=user_profile&table=sys_user&sys_id=';
			response += sysId;
			response += '" aria-label="${Click for more on }';
			response += name;
			response += '">';
			response += name;
			response += '</a>\n';
		}
	}

	return response;
}

That would provide an output such as this.

Customer column as link to user profile page

The one thing that the Customer column does not have that the Assigned to column, which is a standard SNH Data Table user reference, does have is the user avatar image. That’s easily done with the sn-avatar tag, but that tag is not simple HTML, and not processed using the ng-bind-html attribute. Theoretically, it can be processed using the sc-bind-html-compile attribute, but I have never had any luck in utilizing this component. I tried it, and couldn’t get it to work, so I gave up and went back to using the ng-bind-html attribute. So, there is no user avatar in this version. Maybe one day I will figure that out, but that day is not today.

Speaking of figuring things out one day, I still have not been able to get rid of that extra line that appears when there is an image available for the avatar. If you look at the example above, the lines with an avatar image are wider than the lines where that field is blank, or the initials are used in place of an image. This because there is an extra carriage return in there that expands the height of the line. Only when there is an actual image does this occur, but it shouldn’t occur at all. If there is some grand master CSS wizard out there who understands why this is happening and can explain it to me, or better yet, tell me what I can do to prevent it, I would be forever in your debt. I have not been able to figure that one out.

One other thing that you may have noticed in that image is that I got rid of the 0.00 value for tasks that had no hours charged to them. I like that better, and that was a pretty simple thing to do. I still want to have some kind of mouse-over display of who logged the hours, so maybe we will take a look at that next time out.

Scripted Value Columns, Part V

“Make incremental progress; change comes not by the yard, but by the inch.”
Rick Pitino

Last time, we had enough parts cobbled together to demonstrate that the concept actually works. Of course, all we had to show for it was some random numbers, but that told us that the specified script was being called for each row, which is what we were after. Now that we know that the basic structure is performing as desired, we can revisit the configurable Script Include component and see if we can come up with some actual use cases that might be of value to someone.

One of the questions that triggered this idea was related to comments and work notes on Incidents. Assuming that the main record in the table is an Incident, we can clone our example Script Include to create one dedicated to pulling data out of the latest comment or work note on an Incident. We can call this new Script Include ScriptedJournalValueProvider.

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

	getScriptedValue: function(item, config) {
		return Math.floor(Math.random() * 100) + '';
	},

	type: 'ScriptedJournalValueProvider'
};

We will want to delete the example code in the getScriptedValue function and come up with our own, but other than that, the basic structure remains the same. Assuming that we want our script to be able to handle a number of attributes of an Incident Journal entry, we can use the name of the column to determine which function will fetch us our value.

getScriptedValue: function(item, config) {
	var response = '';

	var column = config.name;
	if (column == 'last_comment') {
		response = this.getLastComment(item, config);
	} else if (column == 'last_comment_by') {
		response = this.getLastCommentBy(item, config);
	} else if (column == 'last_comment_on') {
		response = this.getLastCommentOn(item, config);
	} else if (column == 'last_comment_type') {
		response = this.getLastCommentType(item, config);
	}

	return response;
}

This way, we can point to this same script in multiple columns and the name of the column will determine which value from the last comment or work note gets returned.

Since all of the functions will need the data for the last entry, we should create a shared function that they all can leverage to obtain the record. As with many things on the ServiceNow platform, there are a number of ways to go about this, but for our demonstration purposes, we will read the sys_journal_field table looking for the last entry for the Incident in the current row.

getLastJournalEntry: function(sys_id) {
	var journalGR = new GlideRecord('sys_journal_field');
	journalGR.orderByDesc('sys_created_on');
	journalGR.addQuery('name', 'incident');
	journalGR.addQuery('element_id', sys_id);
	journalGR.setLimit(1);
	journalGR.query();
	journalGR.next();
	return journalGR;
}

Now that we have a common way to obtain the GlideRecord for the latest entry, we can start building our functions that extract the requested data value. Here is the one for the comment text.

getLastComment: function(item, config) {
	var response = '';

	var journalGR = this.getLastJournalEntry(item.sys_id);
	if (journalGR.isValidRecord()) {
		response = journalGR.getDisplayValue('value');
	}

	return response;
}

The others will basically be copies of the above, modified to return different values based on their purpose. The whole thing, all put together, now looks like this.

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

	getScriptedValue: function(item, config) {
		var response = '';

		var column = config.name;
		if (column == 'last_comment') {
			response = this.getLastComment(item, config);
		} else if (column == 'last_comment_by') {
			response = this.getLastCommentBy(item, config);
		} else if (column == 'last_comment_on') {
			response = this.getLastCommentOn(item, config);
		} else if (column == 'last_comment_type') {
			response = this.getLastCommentType(item, config);
		}

		return response;
	},

	getLastComment: function(item, config) {
		var response = '';

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('value');
		}

		return response;
	},

	getLastCommentBy: function(item, config) {
		var response = '';

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('sys_created_by');
		}

		return response;
	},

	getLastCommentOn: function(item, config) {
		var response = '';

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('sys_created_on');
		}

		return response;
	},

	getLastCommentType: function(item, config) {
		var response = '';

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('element');
		}

		return response;
	},

	getLastJournalEntry: function(sys_id) {
		var journalGR = new GlideRecord('sys_journal_field');
		journalGR.orderByDesc('sys_created_on');
		journalGR.addQuery('name', 'incident');
		journalGR.addQuery('element_id', sys_id);
		journalGR.setLimit(1);
		journalGR.query();
		journalGR.next();
		return journalGR;
	},

	type: 'ScriptedJournalValueProvider'
};

Now that we have a Script Include to utilize, we need to put together a new page so that we can configure it to make use of it so that we can test it out. Let’s make a quick copy of the page that we were using for testing last time and call it scripted_value_test. Also, let’s make a quick copy of the test configuration script that we were using earlier and call it ScriptedValueConfig.

var ScriptedValueConfig = Class.create();
ScriptedValueConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'all',
		roles: ''
	}],

	state: [{
		name: 'all',
		label: 'All'
	}],

	table: {
		all: [{
			name: 'incident',
			displayName: 'Incident',
			all: {
				filter: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe^active=true',
				fields: 'number,opened_by,opened_at,short_description',
				svcarray: [{
					name: 'last_comment_on',
					label: 'Last Comment',
					heading: 'Last Comment',
					script: 'global.ScriptedJournalValueProvider'
				},{
					name: 'last_comment_by',
					label: 'Last Comment By',
					heading: 'Last Comment By',
					script: 'global.ScriptedJournalValueProvider'
				}],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			}
		}]
	},

	type: 'ScriptedValueConfig'
});

Now let’s pull up our new page in the Service Portal Designer and point the table widget to our new configuration script.

Configuring the new test page to use the new test configuration script

Once we save that, we can pop over to the Service Portal and pull up our new page to try it out.

First test of our first real world utilization of this feature

Beautiful! Our new scripted value provider Script Include was called by the core SNH Data Table widget and it returned the requested values, which were then displayed on the list with all of the other standard table columns. That wasn’t so hard, now was it?

Of course, we still have a couple more wrapper widgets to modify (and test!), and I would like to produce another example, maybe something to do with catalog item variables, but I think we are close. One thing I see that I never noticed before, though, is that the added columns don’t quite line up with the original columns. Maybe it is a CSS thing, or maybe it is something a little more diabolical, but I want to take a look at that and see what is going on there. All of the data in the columns should be displayed consistently; I don’t like it when things don’t all line up correctly. I need to figure out what is going on there and see what I can do about it.

Anyway, we still have a little more work to do before we can wrap this all up into a new Update Set and post a new version out on Share, but we will keep plugging along in our next installment.

Scripted Value Columns in the SNH Data Table Widgets

“Daring ideas are like chessmen moved forward; they may be beaten, but they may start a winning game.”
Johann Wolfgang von Goethe

Every once in a while, I go out to the Developer’s Forum, just to see what other folks are talking about or struggling with. I rarely comment; I leave that to people smarter or faster than myself. But occasionally, the things that people ask about will trigger a thought or idea that sounds interesting enough to pursue. The other day there were a couple of posts related to the Data Table widget, something that I spent a little time playing around with over the years, and it got me to wondering if I could do a little something with my SNH Data Table Widgets to address some of those requirements. One individual was looking to add catalog item variables to a table and another was looking to add some data from the last comment or work note on an Incident. In both cases, the response was essentially that it cannot be done with the out-of-the-box data table widgets. I never did like hearing that as an answer.

It occurred to me that these and other similar columns just needed a little code to fish out the values that they wanted to have displayed on the table with the rest of the standard table fields. We are already doing something like that with the aggregate columns, so it didn’t seem that much of a stretch to clone some of that code and adapt it to handle these types of requirements. If we took the stock properties for aggregate columns and buttons & icons (label, name, and heading) and added one more for the name of a Script Include, as the data was loaded from the base table, we could pass the row data and the column configuration to a standard function in that Script Include to obtain the value for that column. You know what I always ask myself: How hard could it be?

So here is the plan: create a new configuration option called Scripted Value Columns, update the configuration file editor to maintain the properties for those columns, and then update the Data Table widgets to process them based on the configuration. When the data for the table is loading, we’ll get an instance of the Script Include specified in the configuration and then call the function on that instance to get the value for the column. Sounded simple enough to me, but let’s see if we can actually make it work.

To begin, let’s create an example Script Include that we can use for testing purposes. At this point, it doesn’t matter where or how we get the values. We can simply return random values for now; we just want something that returns something so that we can demonstrate the concept. Here is the ScriptedValueExample that I came up with for this purpose.

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

	getScriptedValue: function(item, config) {
		return Math.floor(Math.random() * 100) + '';
	},

	type: 'ScriptedValueExample'
};

The function that we will be calling from the core Data Table widget will be getScriptedValue. In the editor, our pick list of Script Includes can be limited to just those that contain the following text:

getScriptedValue: function(item, config)

This will mean that the function in your custom Script Include will need to have the exact same spacing and argument naming conventions in order for it to show up on the list, but if you clone the script from the example, that shouldn’t be a problem.

Now that we have our example script, we can jump over to the list of portal widgets and pull up one of the existing pop-up configuration editors and clone it to create our new Scripted Value Column Editor. For the HTML portion, we can keep the label, name, and heading fields, delete the rest, and then add our new script property.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.script"
      snh-name="script"
      snh-type="reference"
      snh-required="true"
      placeholder="Choose a Script Include"
      table="'sys_script_include'"
      display-field="'name'"
      value-field="'api_name'"
      search-fields="'name'"
      default-query="'scriptLIKEgetScriptedValue: function(item, config)'"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

The default-query attribute of the sn-record-picker will limit our list of Script Includes to just those that will work as scripted value providers. Other than that, this pop-up editor will be very similar to all of the others that we are using for the other types of configurable columns.

There is no Server script needed for this one, and the Client script is the same as the one from which we made our copy, so there are no changes needed there.

function ScriptedValueColumnEditor($scope, $timeout, spModal) {
	var c = this;

	$scope.spModal = spModal;

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

So that takes care of that. Now we need to modify the main configuration editor to utilize this new pop-up widget. That sounds like a good project for our next installment.

Aggregate List Columns

“Get a good idea and stay with it. Do it, and work at it until it’s done right.”
Walt Disney

We have had a lot of fun with the Service Portal Data Table Widget on this site. So much so, in fact, that we had to make our own copy to avoid extensive modifications to a core component of the Service Portal. So far, we have created a Configurable Data Table Widget Content Selector, allowed individual columns to be links to referenced records, added buttons and icons, added User Avatars to user columns, set up the Configurable Data Table Widget Content Selector to use a JSON object to configure the widget, created an Editor for the JSON configuration object, added check boxes to the rows, added an additional extension of the base Data Table Widget to use the JSON configuration object directly, and built a User Directory using all of these custom components. That’s quite a bit of customization, but there is at least one more thing that we could do to make this all even better.

The feature that I have in mind is to have one or more columns that would include counts of related records. For example, on a list of Assignment Groups, you might want to include columns for how many people are in the group or how many open Incidents are assigned to the group. These are not columns on the Group table; these are counts of related records. It seems as if we could borrow some of the concepts from our Buttons and Icons strategy to come up with a similar approach to configuring Aggregate List Columns that could be defined when setting up the configuration for the table. You know what I always like to ask myself: How hard could it be?

Let’s take a quick look at what we need to configure a button or icon using the current version of the tools. Then we can compare that to what we would need to configure an Aggregate List Column using a similar approach. Here is the data that we collect when defining a button/icon:

  • Label
  • Name
  • Heading
  • Icon
  • Color
  • Hint
  • Page

The first three would appear to apply to our new requirement as well, but the rest are specific to the button/icon configuration. Still, we could snag those first three, and copy all of the code that deals with those first three, and then ditch the rest and replace them with data points that will be useful to our new purpose. Our list, then, would end up looking something like this:

  • Label
  • Name
  • Heading
  • Table
  • Field
  • Filter

The first three would be treated exactly the same way that their counterparts are treated in the button/icon code. The rest would be unique to our purpose. Table would be the name of the table that contains the related records to be counted. Field would be the name of the reference field on that table that would contain the sys_id of the current row. Filter would be an additional query filter that would be used to further limit the records to be counted. The purpose for the Filter would be to provide for the ability to count only those records desired. For example, a Table of Incident and a Field of assigned_to would count all of the Incidents ever assigned to that person, which is of questionable value. With the ability to add a Filter of active=true, that would limit the count to just those Incidents that were currently open. That is actually a useful statistic to add to a list.

One other thing that would be useful would be the addition of a link URL that would allow you to pull up a list of the records represented in the count. Although I definitely see the value in this additional functionality, anyone who has followed this web site for any length of time knows that I don’t really like to get too wild and crazy right out of the gate. My intent is to just see if I can get the count to appear on the list before I worry too much about other features that would be nice to have, regardless of their obvious value.

So, it seems as if the place to start here would be to pull up a JSON configuration object and see if we can add a configuration for an Aggregate List Column. When we built the User Directory, we added a simple configuration file to support both the Location Roster and the Department Roster. This looks like a good candidate to use as a starting point to see if we can set up a test case. Here is the original configuration file used in the User Directory project:

var RosterConfig = Class.create();
RosterConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'department',
		label: 'Department'
	},{
		name: 'location',
		label: 'Location'
	}],

	table: {
		all: [{
			name: 'sys_user',
			displayName: 'User',
			department: {
				filter: 'active=true^department={{sys_id}}',
				fields: 'name,title,email,location',
				btnarray: [],
				refmap: {
					cmn_location: 'location_roster'
				},
				actarray: []
			},
			location: {
				filter: 'active=true^location={{sys_id}}',
				fields: 'name,title,department,email',
				btnarray: [],
				refmap: {
					cmn_department: 'department_roster'
				},
				actarray: []
			}
		}]
	},

	type: 'RosterConfig'
});

For our purpose, we don’t really need two state options, so we can simplify this even further by reducing this down to just one that we can call all. Then we can add our example aggregate configuration just above the button configuration. Also, since this is just a test, we will want to limit our list of people to just members of a single Assignment Group, so we can update the filter accordingly to limit the number of rows. Here is the configuration that I came up with for an initial test.

var AggregateTestConfig = Class.create();
AggregateTestConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'all',
		label: 'All',
	}],

	table: {
		all: [{
			name: 'sys_user',
			displayName: 'User',
			all: {
				filter: 'active=true^sys_idIN46c4aeb7a9fe1981002bbd372644a37b,46d44a23a9fe19810012d100cca80666,5137153cc611227c000bbd1bd8cd2005,5137153cc611227c000bbd1bd8cd2007,9ee1b13dc6112271007f9d0efdb69cd0,f298d2d2c611227b0106c6be7f154bc8',
				fields: 'name,title,email,department,location',
				aggarray: [{
					label: 'Incidents',
					name: 'incidents',
					heading: 'Incidents',
					table: 'incident',
					field: 'assigned_to',
					filter: 'active=true'
				}],
				btnarray: [],
				refmap: {},
				actarray: []
			}
		}]
	},

	type: 'AggregateTestConfig'
});

For now, I just created a simple filter using all of the sys_ids of all of the members of the Hardware group rather than bringing in the group member table and filtering on the group itself. This is not optimum, but this is just a test, and it will avoid other issues that we can discuss at a later time. The main focus at this point, though is the new aggarray property, which includes all of the configuration information that we listed earlier.

aggarray: [{
   label: 'Incidents',
   name: 'incidents',
   heading: 'Incidents',
   table: 'incident',
   field: 'assigned_to',
   filter: 'active=true'
}]

Now that we have a configuration script, we can create a page and try it out, even though we have not yet done any coding to support the new configuration attributes. At this point, we just want to see if the list comes up, and since nothing will be looking for our new data, that portion of the configuration object will just be ignored. I grabbed a copy of the Location Roster page from the User Directory project and then cloned it to create a page that I called Aggregate Test. Then I edited the configuration for the table to change the Configuration Script value to our new Script Include, AggregateTestConfig. I also removed the value that was present in the State field, as we only have one state, so no value is needed.

Updating the Configuration Script on the cloned portal page

With that saved, we can run out to the Service Portal and pull up the new page and see how it looks.

First look at the new test page using our new configuration script

Well, that’s not too bad for just a few minutes of effort. Of course, that was just the easy part. Now we have to tinker with the widgets to actually do something with this new configuration data that we are passing into the modules. That’s going to be a bit of work, of course, so we’ll start taking a look at how we want to do that next time out.

Automatically Link Referenced Tasks, Improved

“The secret of change is to focus all of your energy not on fighting the old, but on building the new”
Socrates

After running my LinkReferencedTasks Business Rule for a while, it has become apparent that there was a flaw in my approach. Whenever someone separates different task numbers with a special character, neither one gets picked up in the process. For example, if you enter something like REQ0010005/RITM0010009, the process that converts all special characters to an empty string ends up converting that to REQ0010005RITM0010009, which starts with REQ, but is not a valid Task number. A little Corrective Maintenance should solve that problem. Let’s convert this:

text = text.replace(/\n/g, ' ');
text = text.replace(/[^A-Z0-9 ]/g, '');

… to this:

text = text.replace(/[^A-Z0-9 ]/g, ' ');

Originally, I had converted all line feeds to spaces and then all characters that were not letters, numbers, or spaces to an empty string. Once I decided to change all characters that were not letters, numbers, or spaces to a space, I no longer needed the line above, as line feeds fall into that same category. That solved that problem.

However, while I was in there, I decided to do a little Perfective Maintenance as well. By changing all of the special characters to single spaces, I thought that I might end up with several spaces in a row, which would result in one or more empty strings in my array of words. To screen those out, I thought about discarding words with a length of zero, but then it occurred to me that short words of any kind will not be task numbers, so I settled on an arbitrary minimum length of 6 instead. Now the code that unduplicates the list of words looks like this:

var unduplicated = {};
for (var i=0; i<words.length; i++) {
	var thisWord = words[i];
	if (thisWord.length > 5) {
		unduplicated[thisWord] = thisWord;
	}
}

This should reduce the clutter quite a bit and minimize the number of words that need to be checked to see if they start with one of the specified prefixes.

And finally, I added an Enhancement, which I had actually thought about earlier, but didn’t implement. This enhancement allows you to specify the relationship type instead of just accepting the hard-coded Investigates relationship that I had put in the original. For backward compatibility, I did not want to make that mandatory, so I kept that as a default for those implementations that did not want to specify their own. The new version of the Script Include now looks like this:

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

	linkReferencedTasks: function(taskGR, prefix, relationship) {
		if (!relationship) {
			relationship = 'd80dc65b0a25810200fe91a7c64e9cac';
		}
		var text = taskGR.short_description + ' ' + taskGR.description;
		text = text.toUpperCase();
		text = text.replace(/[^A-Z0-9 ]/g, ' ');
		var words = text.split(' ');
		var unduplicated = {};
		for (var i=0; i<words.length; i++) {
			var thisWord = words[i];
			if (thisWord.length > 5) {
				unduplicated[thisWord] = thisWord;
			}
		}
		for (var word in unduplicated) {
			for (var x in prefix) {
				if (word.startsWith(prefix[x])) {
					this._findTask(word, taskGR.getUniqueValue(), relationship);
				}
			}
		}
	},

	_findTask: function(number, child) {
		var taskGR = new GlideRecord('task');
		if (taskGR.get('number', number)) {
			this._documentRelationship(taskGR.getUniqueValue(), child, relationship);
		}
	},

	_documentRelationship: function(parent, child, relationship) {
		var relGR = new GlideRecord('task_rel_task');
		relGR.addQuery('parent', parent);
		relGR.addQuery('child', child);
		relGR.query();
		if (!relGR.next()) {
			relGR.initialize();
			relGR.parent = parent;
			relGR.child = child;
			relGR.type = relationship;
			relGR.insert();
		}
	},

    type: 'TaskReferenceUtils'
};

So, a little fix here, a little improvement there, and a brand new feature over there, and suddenly we have a new version better than the last. Stuff’s getting better. Stuff’s getting better every day. Here’s the improved Update Set.

Automatically Link Referenced Tasks

“It is necessity and not pleasure that compels us.”
Dante Alighieri

Occasionally, we will get an Incident Ticket that references another task present in the system. This is usually a status request on a Service Catalog Request or a problem with a recent Change. It would be nice to be able to just click on those recognizable task numbers, but both the Short Description and the Description are plain text fields where that is not an option. There is, however, a handy out-of-the-box UI Formatter that you can include on your Incident Form that provides the capability to link other tasks to your Incident. The name of the Formatter is Task Relations, and I like to drag it onto the Incident Form in the Forms Designer right underneath the Formatter for Contextual Search Results. Once you have that Formatter present on your Incident Form, you can click on the green plus sign and link other tasks of various types to your Incident.

That’s a really cool feature that allows you click on the related tasks to open them up, but being the lazy developer that I am, I would prefer not to have to go through all of the manual work to set up all of the links to the things mentioned in the text of the ticket. In my ideal world, the system would be smart enough to read the text, recognize a task number, and then build the link for me so that I don’t have to do all of that work by hand, and also to make sure that I did not miss anything. How hard could that be?

My thought was that I could create a Business Rule linked to the Incident table that would examine the two primary text fields (Short Description and Description), look for anything that appeared to be a task number, search the Task table to see if it really was a task number, and if so, build the link for me. It seemed like a relatively simple thing to do, so I went to work.

It felt as if there might be a lot of code involved, so instead of putting all of that in the Business Rule itself, I decided to build a Script Include that I could call from the Business Rule. I thought that if I could make the function generic enough, I might be able to reuse it for other Business Rules linked to different Task tables. Plus, putting all of the code in the Script Include keeps the Business Rule a lot cleaner. Here are the things that I thought that I needed to do in order to get this all to work:

  • Combine the two text fields on the Incident into a single string variable for examination,
  • Convert the text to upper case,
  • Convert all line feeds to spaces,
  • Remove all characters that are not letters, numbers, or spaces,
  • Split the string by spaces creating an array of words,
  • Unduplicate the array of words so that we only looked at each unique word once,
  • Examine each word to see if it started with a known task number prefix,
  • Read the Task table for every word that started with a known task number prefix, and
  • Build a relationship record for every Task record that was found on the Task table.

All we need to do now is turn that into code.

Combine the two text fields on the Incident into a single string variable for examination:

var text = taskGR.short_description + ' ' + taskGR.description;

Convert the text to upper case:

text = text.toUpperCase();

Convert all line feeds to spaces:

text = text.replace(/\n/g, ' ');

Remove all characters that are not letters, numbers, or spaces:

text = text.replace(/[^A-Z0-9 ]/g, '');

Split the string by spaces creating an array of words:

var words = text.split(' ');

Unduplicate the array of words so that we only looked at each unique word once:

var unduplicated = {};
for (var i=0; i<words.length; i++) {
	var thisWord = words[i];
	unduplicated[thisWord] = thisWord;
}

Examine each word to see if it started with a known task number prefix:

for (var word in unduplicated) {
	for (var x in prefix) {
		if (word.startsWith(prefix[x])) {
			this._findTask(word, taskGR.getUniqueValue());
		}
	}
}

Read the Task table for every word that started with a known task number prefix:

_findTask: function(number, child) {
	var taskGR = new GlideRecord('task');
	if (taskGR.get('number', number)) {
		this._documentRelationship(taskGR.getUniqueValue(), child);
	}
},

Build a relationship record for every Task record that was found on the Task table:

_documentRelationship: function(parent, child) {
	var relGR = new GlideRecord('task_rel_task');
	relGR.addQuery('parent', parent);
	relGR.addQuery('child', child);
	relGR.query();
	if (!relGR.next()) {
		relGR.initialize();
		relGR.parent = parent;
		relGR.child = child;
		relGR.type = 'd80dc65b0a25810200fe91a7c64e9cac';
		relGR.insert();
	}
},

Before I create a relationship record, I want to make sure that there isn’t already a relationship record out there, so I do a quick query just to check before I commit to inserting a new one. The two records don’t need to be linked more than once. I also hard-coded the relationship type, which works for my current purpose, but if I ever want to expand this out to other use cases, I may eventually want to pass that in as an argument as I did with the task number prefixes. Here is the whole thing, all put together:

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

	linkReferencedTasks: function(taskGR, prefix) {
		var text = taskGR.short_description + ' ' + taskGR.description;
		text = text.toUpperCase();
		text = text.replace(/\n/g, ' ');
		text = text.replace(/[^A-Z0-9 ]/g, '');
		var words = text.split(' ');
		var unduplicated = {};
		for (var i=0; i<words.length; i++) {
			var thisWord = words[i];
			unduplicated[thisWord] = thisWord;
		}
		for (var word in unduplicated) {
			for (var x in prefix) {
				if (word.startsWith(prefix[x])) {
					this._findTask(word, taskGR.getUniqueValue());
				}
			}
		}
	},

	_findTask: function(number, child) {
		var taskGR = new GlideRecord('task');
		if (taskGR.get('number', number)) {
			this._documentRelationship(taskGR.getUniqueValue(), child);
		}
	},

	_documentRelationship: function(parent, child) {
		var relGR = new GlideRecord('task_rel_task');
		relGR.addQuery('parent', parent);
		relGR.addQuery('child', child);
		relGR.query();
		if (!relGR.next()) {
			relGR.initialize();
			relGR.parent = parent;
			relGR.child = child;
			relGR.type = 'd80dc65b0a25810200fe91a7c64e9cac';
			relGR.insert();
		}
	},

    type: 'TaskReferenceUtils'
};

Now that we our Script Include, we need to build the Business Rule that calls it. For my purpose, I added a Business Rule to the Incident table and called it LinkReferencedTasks. I checked the Advanced checkbox and made it active async on Insert whenever there was text in either of the two description fields. I could have also triggered it on Update as well, but in my experience, the Incident description is usually captured when the Incident is created an rarely updated after that.

LinkReferencedTasks Business Rule

Under the Advanced tab, I entered the following script:

(function executeRule(current, previous) {
	new TaskReferenceUtils().linkReferencedTasks(current, ['REQ','RITM','SCTASK','CHG']);
})(current, previous);

In addition to the current GlideRecord, you also need to pass in a string array of task table prefixes which will be used somewhat like a filter to only link tasks that start with those values. If you want a different mix of task types, you can just update that list. That’s it. We are done. Well, maybe we had better test it out first, but the building part is done anyway. Testing should be simple enough: we just need to find an existing task that we want to reference and then include that task number in the text of a new incident. Let’s do that now.

New test Incident with references to other tasks in the Short Description field

Now all we have to do is hit that Submit button and see if those tasks referenced in the text are automatically linked to the Incident. Seems as if a little drum-roll would be appropriate here …

Test results for the new Business Rule and Script Include

Well, nothing ever goes right the very first time! It looks like we managed to create a link to the RITM, but not to the Request. Fortunately, it turns out that this is a tester error and not a developer error. When I typed in the Request number, I missed a zero. The actual Request number is REQ0010005, not REQ001005. Of course, my explanation for that is that I did that on purpose to demonstrate that it will only link real requests, and if your request number is not a real request, then it won’t bother to attempt to create a link to it. That’s it — I did it on purpose — it was all part of the plan. You know that you have to test for failure as well as success — I just did it all in one test because I am so efficient. Sometimes I even amaze myself!

Anyway, it all seems to work, so for those of you who like to play along at home, here’s an Update Set with all of the parts.

Update: There is a better (corrected) version here.

Fun with Webhooks, Part X

“Control is for beginners.”
Ane Størmer

I’ve been playing around with our little Incident Webhook subsystem to make sure that everything works, and to make sure that I had finally developed all of the pieces that I had intended to build. For the most part, I’m quite happy with what we have put together during this exercise, but like most end users who finally get their hands on something that they have ordered, now that I have a working model in my hands and have tried to use if for various things, I can envision a number of different enhancements that would make things even better. Still, what we have is pretty nice all on its own, although I did break down and make just a few minor adjustments.

One thing that I had thought about doing, but didn’t, was to skip the confirmation pop-up on the custom Webhook Registry page’s Cancel button when no changes had been made to the form. Going through that a few times was enough to motivate me to put that in there, and I like this version much better. While I was in there, I also built a goBack() function to house the code for returning to the previous page, and then called that function wherever it was appropriate. This didn’t really save that much in the way of code, since the current goBack() logic is only one line itself, but it consolidates the logic in a single place if I ever want to wire in support for something like my Dynamic Breadcrumbs. The entire client side code for the Webhook Registry widget now looks like this:

function WebhookRegistry($scope, $location, spModal) {
	var c = this;

	$scope.cancel = function() {
		if ($scope.form1.$dirty) {
			spModal.confirm('Abandond your changes and return to your Webhooks?').then(function(confirmed) {
				if (confirmed) {
					goBack();
				}
			});
		} else {
			goBack();
		}
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			c.server.update().then(function(response) {
				goBack();
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	function goBack() {
		$location.search('id=my_webhooks');
	}
}

One other thing that I noticed when attempting to integrate with various other targets is that many sites are looking for a property named text as opposed to message. I ended up renaming my message field to text to be more compatible with this convention, but it would really be nice to be able to pick and chose what properties you would like to have in your payload, as well as being able to specify what you wanted them to be named. That’s on my wish list for a future version for sure.

Something that I meant to include in this version, but forgot to do, was to emulate the Test URL UI Action on the Webhook Registry widget so that Service Portal users could have that same capability on that portal page. That was definitely on my plan to include, but I just spaced it out when I was putting that all together. I definitely want to be sure to include that at some point in the near future. I would do it now, but I already built the Update Set and I’m just too lazy to go back and fix it now.

One other thing that is on my wish list for some future version is the ability to set this up for more than just the Incident table. I thought about just switching over to the Task table, which includes Incident as well as quite a few other things derived from Task, but the base Task table does not include the Incident’s Caller or the Request’s Requested for, so there would have to be some special considerations included to cover that. The Task table has Opened by, but that’s not really the same thing when you are dealing with folks calling in and dealing with an Agent entering their information. I thought about adding some additional complexity to cover that, but in the end I just put all of that on my One Day … list and left well enough alone.

Based on what I first set out to do, I think it all came out OK, though. Yes, there are quite a few more things that we could add to make it applicable to a broader domain, and there are a number of things that we could do to make it more flexible, user-friendly, and user-customizable, but it’s a decent start. Certainly good enough to warrant the release of an initial version, which you can download here. Since this is a scoped app, I did not bundle any of the dependencies in the Update Set, so if you want to try this out in your own instance as is, you will need to also grab the latest version of SNH Form Fields and SNH ServiceNow Events, which you can find here. All in all, I am happy with the way that it came out, but I am also looking forward to making it even better one day, after I have spent some time attempting to use it as it is today.

Update: There is a better (improved) version here.

Fun with Webhooks, Part VI

“The most exciting phrase to hear in science, the one that heralds discoveries, is not ‘Eureka!’ but ‘Now that’s funny …'”
Isaac Asimov

Even though we are not quite finished building out our new Subflow just yet, we have enough elements in place that we can try out what we have so far, and see if it actually works. The simplest way to do that would be to create a Webhook Registry that watches a single Incident, and then go make a qualifying change to that Incident and see what happens. Let’s go find an open Incident and then we can set up our registry.

In my instance, INC0000002 seemed like a likely candidate, so I pulled up the list of Webhook Registrations, clicked on the New button, and created the following new Webhook Registry entry to track changes to that specific Incident.

New Webhook Registry entry

For the URL, I used that same webhook.site address that I had used before so that I could see the result on the other end of the transaction. Once I had saved my new Webhook Registry entry, all I needed to do in order to test it out was to make a change to the Incident. For my first test, I simply changed the Assignment Group from Network to Hardware, which also then removed the Assigned to field value, since the original person specified in that field was not a member of the new Assignment Group. After making the change, I went over to webhook.site to see if anything showed up as a result of that modification. Sure, enough, a new POST had just been received by that site shortly after I saved the record.

New Webhook posting after updating an Incident

Although that proves that the process does indeed work as intended, I did notice one thing that is going to require a little bit of rework. Take a look at the second line of the message property:

Assigned To changed from Howard Johnson to  on INC0000002.

The Assigned to field changed from having a value to not having a value. The code that we have in our buildPayload function doesn’t really handle that circumstance in a way that produces the appropriate text to explain that in plain English. Here is the logic that creates that portion of the message:

if (current.assigned_to != previous.assigned_to) {
	payload.assigned_to = current.assigned_to;
	if (previous.assigned_to) {
		payload.message += separator + 'Assigned To changed from ' + previous.assigned_to + ' to ' + current.assigned_to + ' on ' + payload.id + '.';
	} else {
		payload.message += separator + 'Assigned To set to ' + current.assigned_to + ' on ' + payload.id + '.';
	}
	separator = '\n\n';
}

There is a check to see if the previous value was blank, but there is no check to see if the new value is blank. It wouldn’t take much to throw that in there, though, so let’s go ahead and do that now while we are thinking about it.

if (current.assigned_to != previous.assigned_to) {
	payload.assigned_to = current.assigned_to;
	if (previous.assigned_to) {
		if (current.assigned_to) {
			payload.message += separator + 'Assigned To changed from ' + previous.assigned_to + ' to ' + current.assigned_to + ' on ' + payload.id + '.';
		} else {
			payload.message += separator + 'Assigned To ' + previous.assigned_to + ' removed.';
		}
	} else {
		payload.message += separator + 'Assigned To set to ' + current.assigned_to + ' on ' + payload.id + '.';
	}
	separator = '\n\n';
}

That’s better. While we are at it, we might as well make the same change to the code for the Assignment group, as that is pretty much a line for line copy of the code for the Assigned to field. Everything else looks to be OK. The State is mandatory, so it will never have a new blank value, and the Comments and Work Notes fields are based on new entries and not on changes to existing entries, so if either of those are ever blank, we won’t be doing anything with them at all.

Still, other than that one little annoying language problem, the test was actually quite successful. Our Business Rule fired, the change was detected, our new Subflow was launched, our payload was generated, and the payload was successfully POSTed to the appropriate URL. All in all, I would have to say that that was a pretty good first test of the process. Obviously, we need to do a lot more testing, but this was a great start.

At this point, all I really wanted to do was to make sure that everything was working so far, which I think we accomplished. Before we do too much more testing, though, I think we need to go back and finish up the rest of the Subflow. We still have to add in the step that logs the activity, and we also have to add any error handling for when things don’t go as planned. Once we build all of that out, then we can do a lot more testing to validate the entire process from end to end. It was good to take a quick break and make sure that everything was working up to this point, but now that we know that it is, we need to get back to work on finishing up our development efforts.

Fun with Webhooks

“Good ideas are common – what’s uncommon are people who’ll work hard enough to bring them about.”
Ashleigh Brilliant

There is quite a bit of Webhook stuff in various IntegrationHub spokes, but it all seems to be oriented towards consuming incoming events from different external event publishers. I want to actually be the publisher, and send out information based on some preferences selected by the consumer. That may be hidden somewhere in the Now Platform already, but I can’t seem to find it, so I have decided that I would try to develop a Scoped Application to do just that. This may very well be recreating something that already exists in the platform today, but it sounds like a fun exercise, so I am going to give it the old college try.

As always, I will attempt to start out with the most basic of offerings, and then incrementally expand to add more and better features. My approach is to treat this feature as somewhat analogous to a Watch List, in that you sign up to follow certain events, but instead of sending a notification to a User when the event occurs, the result will be that the information is posted to a specified URL. This can apply to any number of things, but to start off, I am going to focus on some very specific changes to one particular table (Incident), and then later expand from there.

To make this work, there will need to be some kind of Webhook Registry where a consumer would sign up to receive these posts. When registering your webhook, you would enter the URL to which you want the data posted along with the specifics of what type or types of events you would like to have included. I’m thinking about linking them directly to an owner, and having some kind of My Webhooks Portal Page where you could manage your existing registrations and add new ones. When adding a new one, you should be able to enter and test your URL, and for our first iteration, that may be the only choice that you get. Later on, we will want to add the ability to choose what you want to follow, which specific updates should trigger a new post, and even what you would like to have included in the payload. But we will also want to start out as simple as possible, so the initial registry may turn out to be quite barren as far as input fields go.

Once registered, there will need to be some process to actually send out the posts as requested in the registration. This could be a Business Rule on the source table, or maybe something created in the Flow Designer. Either way, the process should scan the registry for any condition matches and then send out a post for each match. Each post and response should be logged in some kind of Webhook Activity Log, and any bad HTTP Response Codes should be reported to Event Management. A robust service would attempt to repost any failures up to a certain limit before giving up completely, but all of that can be delegated to some Alert Management Rule at some later time. Again, we will want to start out simple, so our initial focus will just be on making that initial post attempt. Everything else can be pushed off until later on in the process.

Those would seem to be the two major functions: registering the webhook and sending out the posts. We may want some other things at some point, such as the ability to review the logs or to manually repost or to clone an existing registration, but for now, just those two things should get the ball rolling. We may also want to set up a sample receiver for testing purposes, but in practice, the receivers would be other products and outside the scope of this development exercise. There is actually an existing service out on the Internet called Webhook.site that might turn out to be just what I need in order to do a little testing. We should check that out when we get to that point.

For our parts list, then, I can see the need for the following artifacts:

  • A table to hold the webhook registrations,
  • A my_webhooks portal widget to list all webhooks owned by the user,
  • A webhook portal widget for editing a single webhook registration,
  • A Business Rule or Flow to send out the posts,
  • A log table to record the posts and response, and possibly
  • A Script Include to contain some common functions.

Of course, before we create any of that, we will have to create the Scoped Application itself, so that should be where we start next time when we initiate the actual construction phase of this effort.

Fun with Outbound REST Events, Part X

“No one has a problem with the first mile of a journey. Even an infant could do fine for a while. But it isn’t the start that matters. It’s the finish line.”
Julien Smith

After our last installment in this series, our Events now spawn Incidents that are pretty much just what we would like to see. The only remaining challenge at this point is to create a meaningful Description field value. Although we have set things up to produce this Description in a Script Include function, I should point out right here at the outset that everything that we are about to do in our script could also be accomplished in the Flow Designer itself. In fact, it probably should be done using the Flow Designer if we are to fully embrace the whole no-code future towards which we all seem to be herded. I’m still an old coder at heart, though, so it seems easier to me to scratch out another quick function than it does to build out all of those action steps using input forms. Still, it would probably be a worthwhile exercise to replace this script with a subflow one day; today is just not that day. Today we code!

Although we are passing the Alert to our function as an argument, much of the data we need is actually in the Event that spawned the Alert, so the first thing that we are going to want to do is go out and get that guy. That’s pretty basic GlideRecord stuff.

// get initial Event
var eventGR = new GlideRecord('em_event');
eventGR.addQuery('alert', alertGR);
eventGR.orderBy('sys_created_on');
eventGR.query();
eventGR.next();

Since it is possible that there could be more than one Event associated with our Alert, we include an orderBy directive to ensure that we get the very first Event out of the bunch. Once we have our Event in hand, we will have access to the additional_info JSON string, which we will want to convert to a Javascript object so that we can reference all of the various component parts.

// get addition info from Event
var additionalInfo = {};
try {
	additionalInfo = JSON.parse(eventGR.getValue('additional_info'));
} catch (e) {
	gs.info('Unable to parse additonal_info from Event ' + eventGR.number);
}

We also have access to the Event resource, which in our case is a User’s user_name. We can use that to get the sys_user record for that user, much in the same way that we retrieved the Event record.

// get affected User record
var userGR = new GlideRecord('sys_user');
userGR.get('user_name', alertGR.getValue('resource'));

This assumes, of course, that the only place that we using our address validation capability is on the User Profile page. If we ever expand its use to other places — say on the Building or Location form — then we would need to have some way to know whether the resource was a User or a Location or a Building or some other entity with an address to validate. Based on that information, we might be retrieving a Building record or a Location record instead of a User record. For now, though, we can safely assume that the resource is a User.

Now that we have gathered up all of the data that we need, we can start building out our Description. To begin, let’s start out with something that will be universal to all of our Incidents, regardless of the problem being reported.

// format description
var alertDesc = alertGR.getDisplayValue('description');
var section = '\n========================================\n';
var subsection = '\n----------------------------------------\n';
desc += additionalInfo.user.name;
desc += ' attempted to update the address on the user profile for user ';
desc += userGR.getDisplayValue('name');
desc += ', but was unable to verify the address using the US Address Validation service due to the following error:\n\n';
desc += alertDesc;
desc += '\n\nIncident Details:';
desc += section;

Beyond this point, we are going to want to be a little more specific based on what actually happened to trigger the Event. We can do that by introducing some conditional code based on the known values found in the Alert’s description field.

if (alertDesc.startsWith('The response code')) {
	// bad response code language will go here
} else if (alertDesc.startsWith('The response object')) {
	// bad response object language will go here
} else if (alertDesc.startsWith('The response content')) {
	// bad response content language will go here
} else {
	// we should never get here, but just in case ...
}

Since most of the Events that we have triggered up to this point have been of the bad response code variety, let’s do those first.

if (!additionalInfo.response.code) {
	desc += 'No response was received from the service, which could be an indication that the service is unavailable or unreachable. Check the status or the external service as well as the status of your connection to the Internet.';
} else if (additionalInfo.response.code == 401) {
	desc += 'A Response Code of 401 indicates an authentication error of some kind. Verify that your account credentials are correct and that your account is in good standing with the service provider.';
} else {
	desc += 'The service returned a Response Code of ';
	desc +=  additionalInfo.response.code;
	desc += '. Additional information on the meaning of this response may be found in the Response Body. Also, you can check with the service provider for further clarification on the appropriate handling of this response.';
	desc += '\n\nDetailed information on the ';
	desc += additionalInfo.response.code;
	desc += ' Response Code can be found on the web at https://httpstatuses.com/';
	desc += additionalInfo.response.code;
}

This gives us specialized language for no response code at all, and a response code of 401. Everything else is handled in a more generalized section that covers any other bad response code. As more knowledge of the potential response codes becomes available through experience with the service, more specialized language can be added that can be more specific to other known response codes.

Now let’s take a look at what we can do for bad response objects.

desc += 'The service returned a valid Response Code and a parsable response, but the response did not contain certain expected elements necessary to determine the validity of the address. Review the response received and check with the service provider to see if anything has changed with the API specifications.';

That one is about as simple as you can get; everyone gets the same language. For the bad response content issues, things are a little bit more sophisticated. Everyone still gets the same language, but there is a possibility for an exception with this group, so we include code to handle that as well.

desc += 'The service returned a valid Response Code, but the response was either empty or ill-formatted. Review the response received and check with the service provider to see if the service is experiencing problems, or if anything has changed with the API specifications.';
if (additionalInfo.exception) {
	desc += '\n\nException Details:';
	desc += subsection;
	desc += '   Exception: ';
	desc += additionalInfo.exception;
	desc += '\n   Stack Trace:\n';
	desc += additionalInfo.stackTrace;
}

Once we complete all of the conditional logic, we wrap things up with some more universal code that applies to everyone. This just serves to include the user’s input and the service’s response at the end of the body of the Description field for reference.

desc += '\n\nAddress Details:';
desc += subsection;
desc += '   Street: ';
desc += additionalInfo.input.street;
desc += '\n   City: ';
desc += additionalInfo.input.city;
desc += '\n   State: ';
desc += additionalInfo.input.state;
desc += '\n   Zip Code: ';
desc += additionalInfo.input.zip;
desc += '\n\nResponse Details:';
desc += subsection;
desc += '   Response Code: ';
desc += additionalInfo.response.code;
desc += '\n   Response Body: ';
desc += additionalInfo.response.content;
if (additionalInfo.response.object) {
	desc += '\n   Response Object: ';
	desc += JSON.stringify(additionalInfo.response.object, null, '\t');
}

There is still more helpful information that we could add, such as links to the service provider’s documentation, or in the case of the 401 error, the names of the system properties that contain the credentials, but this is good enough for a sample. Let’s just save what we have and then trigger another Event and see what comes out the other side.

Incident with updated description from the new Script Include function

Well, that’s much, much better than the description that the original Create Incident flow was producing. It’s not perfect, but I think it does provide the person receiving the Incident enough details about both what happened and what might be done about it that they can get to work on the ticket right away without a whole lot of research. Obviously, it can be fine-tuned over time, but this is a good foundation upon which to build for this particular use case.

That pretty much wraps up all that I had hoped to accomplish with this series. It took us 10 installments to get here, but much of that was due to the fact that we had to build out our own address validation infrastructure before we could use it to demonstrate applying Event Management tools and techniques to internal ServiceNow features and functions. For those of you who like to play along at home, I have bundled what I hope are all of the relevant parts and pieces into an Update Set that you are welcome to pull down and import into your own environment.