Quickly Move Changes Between Update Sets

We've all been there. You've been wired in, hammering away on code, making great progress, when all of a sudden you're gripped with dread. You check the sprocket menu at the top-right, cautiously hopeful... but as it loads, and recognition sets in, the reality of the situation crashes into you like a sack of turtles.

You forgot to select your update set. 

At this point, it is perfectly reasonable to curl up into a ball and weep at the fact that you're going to have to sift through hundreds or even thousands of updates in the Default update set in your busy environment, picking out the ones that are yours and that are relevant to your current work, manually opening each record one at a time, and moving them to the correct update set.

But here's a better idea -- What if you could just tick the box next to each update, click a button, and dump them all into your current update set? 

"Move to current update set" list choice UI action.

This is actually really simple to implement; it's all done server-side, through a single UI action script.

The first thing you ought to know is that Update Sets in ServiceNow, are (unsurprisingly) "sets" (groups) of "updates" (changes). The Update Set table (sys_update_set) contains some details about the Update Set itself, but doesn't actually contain any of the actual changes.

The "updates" are the actual data about the records that were modified. These are stored as XML in a separate table called Customer Updates (sys_update_xml). 

When you make a change to a record, the XML data representing that change is stored in the Customer Updates table, and associated (via a reference field on that table) to your currently selected Update Set.

Make sense? Good. 

To quickly move changes between update sets, first create a UI action on the sys_update_xml table. 
You can do this either by navigating to the sys_update_xml table, right-clicking the header, and choosing Configure->UI Actions, or by navigating to the UI actions table (sys_ui_action), creating a new record, and selecting 'Customer Update' as the table. 

Pro Tip: Even though you can view the updates from the Update Set record, the updates are shown in a related list. This means that the UI action has to be on the table containing the updates: sys_update_xml. 

Once you've got a UI action created and pointing to the Customer Update (sys_update_xml) table, give it a Name (like Move to current update set). This will be what shows up in the drop-down at the bottom of the list of updates. 

Next, give your UI action an Action name (I used move_update), and select what sort of UI action it's going to be (check the List choice check-box). 

The rest of the top section you can leave as default, though you may want to fill out a Comment. 

In the Condition box, enter a condition. The following condition will ensure that the user has an update set selected and that the script can access it, as well as verify that the user is an Admin. If they are not, they won't be able to use the UI action. 

gs.getPreference('sys_update_set') != '' && gs.hasRole('admin')

Unfortunately, List Choice UI actions are not very well documented (if at all) in ServiceNow, so before we move on, let's discuss how they work. 

The way a List Choice UI action works in ServiceNow, is it executes your script once on each selected record. The script state however, is not persistent. That is to say,  the following script would always log "iteration 1", once for every record you selected - it would not increment:

var i = 1; 
gs.log('iteration ' + i);
i++; 

This behavior is a good thing though, because it means that we can treat each record separately, and we will have access to the current object. 

Just to drive this point home... imagine I visited the incident table, and selected these 3 incidents:

With these 3 incidents selected, imagine that I ran the following code. What do you think would happen? 

var bow = 'f298d2d2c611227b0106c6be7f154bc8';   //Bow's sys_id
if (current.getValue('caller_id') == bow) {
    gs.addInfoMessage('Bow detected');
}
else {
    gs.addErrorMessage('Bow not detected');
}

Remember that the script will run once, in a unique context, for each selected record. With that in mind, here is the result: 

The script spit out three separate messages (one for each selected record), reflecting the results of the script when run against each record. It did not print a result for the record that I did not select. Make sense? 

Great! Now, let's write the script that's going to do the annoying and tedious work of fixing our screw-ups for us: 

//Get link to the 'current' object's parent update set, and set it as the redirect URL.
var updateSetLink = gs.getProperty('glide.servlet.uri') + 'sys_update_set.do?sys_id=' + current.getValue('update_set');
action.setRedirectURL(updateSetLink);
//Get link to the current 'customer update' record.
var updateLink = gs.getProperty('glide.servlet.uri') + 'sys_update_xml.do?sys_id=' + current.getValue('sys_id');
//Get the 'type' of the record being updated.
var type = current.getValue('type');
//Get a record containing the parent update set.
var currentUpdateSetID = gs.getPreference('sys_update_set');
var updateSetGR = new GlideRecord('sys_update_set');
updateSetGR.get(currentUpdateSetID);
//If the customer update record is already in the currently selected update set, alert the user and provide a link to the record that failed to update.
if (current.getValue('update_set') == gs.getPreference('sys_update_set')) {
    gs.addErrorMessage('The selected <a href="' + updateLink + '">' + type + '</a> is already in your currently selected update set: ' + updateSetGR.getValue('name') + '.');
}
//If the customer update record is not already in the currently selected update set, move it to the selected update set and alert the user. 
else {
    try {
        current.setValue('update_set', currentUpdateSetID);
        current.update();
        gs.addInfoMessage('The selected <a href="' + updateLink + '">' + type + '</a> has been moved to your currently selected update set: ' + updateSetGR.getValue('name') + '.');
    }
    catch(e) {
        gs.log('ERROR while moving customer updates: ' + e);
        gs.addErrorMessage('There was an error. Please review the system logs for more details.');
    }
}

Let's walk through the code above, and see how it works!

On line 2 (the first line of actual code), we declare updateSetLink, and construct the URL to the update set which is the parent of the update records that are selected. By using the glide.servlet.uri system property, we ensure that this link will be accurate no matter which instance we're on. 

On line 3, we use the update set link variable to set the redirectURL, which ensures that after the changes are made from the UI action, we're redirected back to the update set we were viewing initially. 

On line 5, we construct the link to the current update record the same way we made the link for the update set on line 2. Along with the 'type' that we declare and set on line 7, we're going to use these values to construct the notification messages that display after the UI action runs. 

Line 9 gets us the sys_id of the user's currently selected update set. This sys_id is then immediately used on lines 10 and 11, as we get a GlideRecord object containing the user's current update set. 

On lines 13-15, we check if the current user's update set is the same as the update set that the selected update is in. We check this by comparing the variable we previously stored the sys_id of the current update set in, with current.getValue('update_set'). Since the update_set field on 'current' (which, remember, refers in turn to each record we've selected) is a reference field, we're really comparing a sys_id to a sys_id here. 
If the current update set IS the same as the parent of the selected change, we throw an error on line 14 and don't do anything else. 

Lines 17 and on only fire if the condition mentioned above is false. The rest is done inside a try { } catch() { } block so that we can log any errors that might crop up, just to be safe. 

On lines 19 and 20, we update the update_set value of the update record to the currently selected update set's sys_id, then do current.update() to save the changes to the record. 

Finally, on line 21, we throw a message with a link to the update record, telling the user that the selected update(s) have been moved to their selected update set. 

All that's left is to save the UI action, and you're all set! 


Don't feel like doing all that work? No problem, you lazy jerk - I've done it for you! Just download the Update Set XML below, and import it into your instance! Here's how: 

  1. Download this XML file, and save it to your computer.
  2. Elevate privileges to the security_admin role.
  3. Navigate to System Update Sets -> Retrieved Update Sets.
  4. Click the Import Update Set from XML link.
  5. Click Choose File and select the XML file you downloaded, then click Upload
  6. Commit the update set. 
    1. Open the update set you just uploaded
    2. Click Commit Update Set

That's all there is to it!
Thanks for reading, don't forget to subscribe, and happy developing!