Adam Stout
ServiceNow Employee
ServiceNow Employee

We have all been there, you just upgraded your instance, and you go to the Application Manager and the first thing you see is this:

 

AdamStout_0-1725645249895.png

 

 

199 updates available to install… Which ones do I need???  Where should I start??? Do I have to go through these one at a time???

 

In this article, we’ll explore how to review these updates in terms of major, minor, and patches in a list view and, most importantly, how to queue up batch installs. Once in place, you can quickly review all the patches available, select which you want to install, and then queue the install in just a few clicks.

 

Before diving into this walkthrough, I recommend watching the webinar that covers the general approach to Store updates and provides a detailed guide on the batch installer. Once you've watched it, this walkthrough will guide you through building the batch installer yourself. You can find the webinar here, with the batch installer walkthrough starting around minute 19.

 

AdamStout_1-1725645249899.png

 

Now that you have watched that, let’s get to the technical part of how to build it.

 

The batch installer has two main pieces. First, we start off with a remote table that helps us review the Store updates available. Then, there is a UI that allows us to choose which apps to update. Then, we call the CI/CD Batch installer to do the actual installation.

 

Disclaimer: The code provided in this blog post is intended as an example and is provided "as is" without any warranty of any kind, either express or implied. It is the responsibility of the user to thoroughly test the code in their own environment to ensure it meets their requirements and functions as expected. The author and publisher disclaim any liability for any damages or issues that may arise from the use of this code.

 

Creating the Remote Table to Review Available Updates

 

Create a new application

Start by creating a new application scope to house your custom app. This ensures that your app is self-contained and does not interfere with global settings. There are several ways to create an application.  I used ServiceNow Studio but you can use any method you prefer.

 

Subscription Note: This app will not have any physical tables (only a remote table) so it will generally not cause any entitlement issues. If you have any questions about your entitlement to create new applications, please reach out to your account team.

 

AdamStout_2-1725645249907.png

 

Docs: https://docs.servicenow.com/csh?topicname=create-application.html&version=latest

 

Create the remote table

Make sure you switch to the newly created app before creating any new objects. Navigate to the Tables module under Remote Tables and click New.

 

AdamStout_3-1725645249908.png

 

 

Create a table with the following fields:

Column label

Column name

Type

Reference

Application

application

Reference

Store Application [sys_store_app]

Avail. Major

major_count

Integer

 

Avail. Minor

minor_count

Integer

 

Avail. Patches

patch_count

Integer

 

Available Version

available_version

Reference

Application Version [sys_app_version]

Batch Level

batch_level

Choice

 

Installed Version

installed_version

String

 

Latest Major Version

latest_major_version

Reference

Application Version [sys_app_version]

Latest Minor Version

latest_minor_version

Reference

Application Version [sys_app_version]

Latest Patch Version

latest_patch_version

Reference

Application Version [sys_app_version]

Latest Version Level

latest_version_level

Choice

 

Level

level

Choice

 

Name

name

String

 

 

 

Write the script to populate the table.

Navigate to the Definitions module in Remote Tables and click New. There is a lot of room for customizations and enhancement, but this should get us close to what we are looking for:

 

 

(function executeQuery(v_table, v_query) {
      var res = {};
      var versionLevel = {'major': 3, 'minor': 2, 'patch': 1, 'none': 0};
      var versionLevelAscending = Object.keys(versionLevel).reverse();

      var getAvailableVersion = function (appSysId)
      {
          var vers = [];
          var v = new GlideRecord('sys_app_version');
          v.addQuery('source_app_id', '=', appSysId);
          v.orderBy('version');
          v.query();
          while(v.next())
          {
              vers.push({'sys_id': v.getUniqueValue(), 'name': v.getDisplayValue(), 'version': v.getValue('version'), 'publish_date': v.getValue('publish_date')});
          }
          return vers;
      };

      var compareVersion = function (currentVer, availableVer)
      {
          var currentVerParts = currentVer.split('.');
          var availableVerParts = availableVer.split('.');
          var diff = 'None';

          if (currentVerParts[0] !== availableVerParts[0]) {
              diff = 'Major';
          } else if (currentVerParts[1] !== availableVerParts[1]) {
              diff = 'Minor';
          } else if (currentVerParts[2] !== availableVerParts[2]) {
              diff = 'Patch';
          } else {
              diff = 'No';
          }
          return diff;
      };

      var storeAppVer = new GlideRecord('sys_store_app');
      storeAppVer.addQuery('active', '=', true);
      storeAppVer.addQuery('update_available', '=', true);
      storeAppVer.query();

      // Create scan finding
      while (storeAppVer.next())
      {
          var cur = storeAppVer.getUniqueValue();
          var availVersion = getAvailableVersion(cur);
          for(var v in availVersion)
          {
              var ver = availVersion[v];
              var diff = compareVersion(storeAppVer.getValue('version'), ver.version).toLowerCase();

              if(res.hasOwnProperty(cur) == false)
              {
                var versionTemplate = {'sys_id': null, 
                                        'application': null,
                                        'level': null,
                                        'available_version': null, 
                                        'installed_version': null, 
                                        'latest_major_version': null, 
                                        'latest_minor_version': null, 
                                        'latest_patch_version': null,
                                        'major_count': 0,
                                        'minor_count': 0,
                                        'patch_count': 0};
                res[cur] = {'major': Object.assign({}, versionTemplate), 'minor': Object.assign({}, versionTemplate), 'patch': Object.assign({}, versionTemplate), 'no': Object.assign({}, versionTemplate)};
              }

              res[cur][diff].sys_id = ver.sys_id;
              res[cur][diff].application = cur;
              res[cur][diff].level = diff;
              res[cur][diff].available_version = ver.sys_id;
              res[cur][diff].version = ver.version;
              res[cur][diff].installed_version = storeAppVer.getValue('version');
              if(diff == 'patch')
              {
                res[cur][diff].latest_patch_version = ver.sys_id;
                res[cur][diff].latest_minor_version = ver.sys_id;
                res[cur][diff].latest_major_version = ver.sys_id;
              }
              if(diff == 'minor')
              {
                res[cur][diff].latest_minor_version = ver.sys_id;
                res[cur][diff].latest_major_version = ver.sys_id;
              }
              if(diff == 'major')
              {
                res[cur][diff].latest_major_version = ver.sys_id;
              }

              res[cur]['patch'].latest_version_level = diff;
              res[cur]['minor'].latest_version_level = diff;
              res[cur]['major'].latest_version_level = diff;

              res[cur]['patch'][diff + '_count']++;
              res[cur]['minor'][diff + '_count']++;
              res[cur]['major'][diff + '_count']++;

              res[cur][diff].name = storeAppVer.getDisplayValue() + ' - ' + ver.version;          
          }
      }

      for(var app in res)
      {
        var lastLevel = res[app]['patch'];
        for(var l in versionLevelAscending)
        {
            var levelCheck = versionLevelAscending[l];
            if(l > 0)
            {
                if(gs.nil(res[app][levelCheck].application))
                {
                    //gs.addInfoMessage('checking - not set: ' + levelCheck);
                    res[app][levelCheck] = lastLevel;
                } else {
                    //gs.addInfoMessage('checking - set: ' + levelCheck);
                    lastLevel = res[app][levelCheck];
                }
            }
        }

        for(var lev in res[app])
        {
            res[app][lev].batch_level = lev;
            if(!gs.nil(res[app][lev].available_version))
            {
                v_table.addRow(res[app][lev]);
            }
        }
    }
  })(v_table, v_query);

 

 

 

 

Validate remote table

Navigate to your new remote table. In my example, mine is named x_snc_app_updater_st_available_updates.  This should look something like this:

AdamStout_4-1725645249916.png

 

 

Here, we can see every app available, the currently installed version, and the available version. In the example script, we chose the most recent patch, minor, and major release, so there is at most one of each type. If you need a specific patch and there are multiple available, you would want to use the standard Application Manager to pick that specific version.

 

Note: I created an inline UI action to view the app in the Store with one click.  This is not essential, but a nice to have.

Create a Subflow to do the Install

The key to the batch installer lies in utilizing the out-of-the-box Batch Install action within the Continuous Integration and Continuous Development spoke. This action should already be installed on your instance; however, if it's not, be sure to activate it before continuing.

 

Before you get started, be sure to configure a user with the sn_cicd.sys_ci_automation role, credentials, and credential alias so you can use the Batch Install action. You can find the latest documentation on how to do this here:  https://docs.servicenow.com/csh?topicname=cicd-api.html&version=latest

 

Navigate to Workflow Studio and create a new subflow.

 

Define the subflow inputs and outputs.

Inputs

Label

Name

Type

Description

Applications

apps

String

This is a list of sys_ids of apps to install

 

Outputs

Label

Name

Type

Description

Progress ID

progress_id

String

Progress ID of the worker that is doing that install

Status Message

status_message

String

Output message from the batch installer.

 

AdamStout_5-1725645249917.png

 

Flow Variables

Define a flow variable with the label Batch Manifest and name batch_manifest.  This is a string that will hold the JSON manifest of which apps to install for the Batch Installer action.

AdamStout_6-1725645249918.png

 

Actions

Lookup Application Versions

Add a lookup records action that will retrieve the application version records [sys_app_version] from the sys_ids we provided in the flow inputs.

 

AdamStout_7-1725645249920.png

 

 

Format Batch Manifest

Set the flow variable Batch Manifest to the required format. Use a script like this one:

 

 

var payload = '{"name": "Store App Batch Installer", "notes": "Installing available patches from the Store", "packages": [';
var loop = 0;
while(fd_data._1__look_up_records.records.next())	
{
    if(loop++ > 0)
    {
        payload += ',';
    }
    payload += '{"id": "' + fd_data._1__look_up_records.records.getValue('source_app_id') + '", "type": "application", ';
    payload += '"load_demo_data": false, "requested_version": "' + fd_data._1__look_up_records.records.getValue('version') + '", ';
    payload += '"notes": "' + fd_data._1__look_up_records.records.getDisplayValue() + '"}';
}
payload += ']}';
return payload;

 

 

AdamStout_8-1725645249921.png

 

Call Batch Install action

 

 

Add the Continuous Integration and Continuous Development -> Batch Install action.  The Batch plan will be the flow variable we set above.  The credentials will be what you have configured previously.  The instance URL is the instance you want to install the app on.  This is commonly the local instance you are running this on, but it could be a different instance if you have the proper credentials.

 

AdamStout_9-1725645249923.png

 

Assign outputs

Now that we have fired off the batch installer, we’ll output the progress worker id and status so we can display them to the user.

flow - output.png

Here is what the complete flow should look like:

flow - complete view.png

Save and publish this flow, and we can move on. This would be a good time to test the flow to make sure everything is correct. To install, you just need a sys_id or two from sys_app_version.

 

 

Create the UI to choose updates

Now comes the fun part.  In this example, I built the UI using Core UI but this could (and probably should) be written in Next Experience.

 

Create UI Pages

We are going to create two UI pages that will help us in the installation process. We will call them later via a UI action.

 

Updates to Install

Create a new UI page named updates_to_install. This will allow the user to review what they have selected to install. A confirmation screen is not essential, but it can prevent a simple clicking mistake, so I highly encourage adding it.

 

In the UI page, set the HTML to:

 

 

 

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
    <g:evaluate>
        var apps = new GlideAggregate("x_snc_app_updater_st_available_updates");
        apps.addQuery('available_version', 'IN', '${sysparm_app_versions}');
        apps.groupBy('name');
        apps.groupBy('level');
        apps.orderBy('level');
        apps.orderBy('name');
        apps.setGroup(true);
        apps.query();
    </g:evaluate>

    <h3>${gs.getMessage('The following applications will be upgraded:')}</h3>
    <ul>
    <j:while test="${apps.next()}">
        <li>${apps.getValue('name')} - ${apps.getValue('level')}</li>
    </j:while>
    </ul>
    <g:ui_form>
        <input type="hidden" name="app_versions" value="${sysparm_app_versions}" />
        <input type="hidden" name="app_count" value="${apps.getRowCount()}" />
        <div class="form-group pull-right" style="padding-right: 20px;">
            <g:dialog_buttons_ok_cancel ok="return actionOK()" cancel="return onCancel()" ok_text="${gs.getMessage('Install Updates')}" ok_id="ok_button" cancel_type="button" />
        </div>
    </g:ui_form>
</j:jelly>

 

 

 

 

And the processing script to:

 

 

 

UpBegin();
function UpBegin() {
        var progress_id = '-1';
        var status_message = '';

        try {
            var inputs = {};
            inputs['apps'] = app_versions; // String 
            var result = sn_fd.FlowAPI.getRunner().subflow('x_snc_app_updater.batch_install_updates4d5').inForeground().withInputs(inputs).run();
            var outputs = result.getOutputs();
            progress_id = outputs['progress_id'];
            status_message = outputs['status_message'];

        } catch (ex) {
            var message = ex.getMessage();
            status_message = message;
            gs.addErrorMessage(progress_id + ' - ' + message);
        }
    //var session = gs.getSession();
    //var URL = session.getUrlOnStack();
    var URL = 'x_snc_app_updater_installing_updates.do?';
    URL += "&sysparm_pworker_sysid=" + progress_id + "&sysparm_message=" + status_message;
    URL += "&sysparm_app_count=" + app_count;
    URL += "&sysparm_app_versions=" + app_versions;
    response.sendRedirect(URL);
}

 

 

 

 

Install Updates

In the UI page, set the HTML to:

 

 

 

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
    <g:evaluate var="jvar_reload_now" jelly="true">
        var progressWorker = new GlideRecord('sys_progress_worker');
        progressWorker.addQuery('sys_id', '=', jelly.sysparm_pworker_sysid);
        progressWorker.query();

        var reloadNow = false;

        if(progressWorker.getRowCount() == 0)
        {
            reloadNow = true;
        } else {
            progressWorker.next();
            if(progressWorker.getValue('state') == 'starting')
            {
                reloadNow = true;
            }
        }
        reloadNow;
    </g:evaluate>
    <g:evaluate>
        var remaining_msg = '';
        var remaining_apps = {};
        var remaining_app_count = 0;
        var hasApps = false;
        var apps = new GlideAggregate('sys_app_version'); // 
        apps.addQuery('sys_id', 'IN', '${sysparm_app_versions}');
        apps.addAggregate('COUNT');
        apps.query();
        if(apps.next())
        {
            remaining_app_count = parseInt(apps.getAggregate('COUNT'));
        }
        if(remaining_app_count)
        {
            remaining_msg = gs.getMessage('Installation of {0} apps has been scheduled, {1} remaining to install.', [ ${sysparm_app_count}, remaining_app_count.toFixed(0) ]);
        }
        var completed_msg = gs.getMessage('{0} apps have been installed.', [ ${sysparm_app_count} ]);
    </g:evaluate>
    <g:ui_form>
        <input type="hidden" name="reloadNow" id="reloadNow" value="${jvar_reload_now}" />
    </g:ui_form>
    <h3>
        <j:if test='${remaining_app_count > 0}'>
            ${remaining_msg}
        </j:if>

        <j:if test='${remaining_app_count == 0}'>
            ${completed_msg}
        </j:if>
    </h3>
    <j:if test='${sysparm_message}'>
        ${sysparm_message}<br /><br />
    </j:if>
    <g:ui_form>
        <table cellpadding="0" cellspacing="0" width="500px">
            <g:ui_progress_worker worker_id="${sysparm_pworker_sysid}" show_cancel="false" rows="grow"/>
        </table>
    </g:ui_form>
</j:jelly>

 

 

Client script:

 

 

if(gel('reloadNow').value == 'true') {
    location.reload();
}

 

 

Processing Script:

 

 

var gr = new GlideRecord('sys_progress_worker');
gr.addQuery('sys_id', '=', sysparm_pworker_sysid);
gr.query();

gr.next();

gs.addInfoMessage(gr.getValue('state'));
if (gr.getValue('state') == 'starting' || gr.getValue('state') == 'running') {

    var wait = 3;
    if(gr.getValue('state') == 'running')
    {
        wait = 15;
    }
    var pm = new GlideProgressMonitor(sysparm_pworker_sysid);
    pm.waitForCompletionOrTimeout(wait);
    var redirect = "x_snc_app_updater_installing_updates.do?sysparm_pworker_sysid=" + sysparm_pworker_sysid;
    response.sendRedirect(redirect);
}

 

 

 

 

 

Create UI Action

We are going to create a UI action to start the installation process.  Users will select which applications to update and then select the UI action to install them.

 

Set the following values:

  • Name - Install Updates
  • Table - Available Updates <or whatever the remote table is you created>
  • Select checkboxes
    • Active
    • Show update
    • Client
    • List v2 Compatible
    • List banner button
    • List choice
  • Onclick - openModalWithSelectedRecords()

Here is the script:

 

 

 

function openModalWithSelectedRecords() {
    var selectedRecords = g_list.getChecked();
    if (selectedRecords.length == 0) {
        gs.addErrorMessage('Please select at least one record.');
        return;
    }
    var sysIds = selectedRecords;
    var dialogClass = GlideModal ? GlideModal : GlideDialogWindow;
    var dialog = new dialogClass('x_snc_app_updater_updates_to_install');
    dialog.setTitle('Selected Apps');
    dialog.setSize(600, 400);
    dialog.setPreference('sysparm_app_versions', sysIds);
    dialog.render();
}

 

 

 

Add Modules

To make this easier to kick off the process, create 4 modules to quickly review the available updates.

Title

Application menu

Order

Link type

Table

Filter

Available Store Updates

Upgrade Center

2000

Separator

 

 

Major Updates

Upgrade Center

2010

List of Records

Available Updates (your remote table)

Batch Level = Major

Minor Updates

Upgrade Center

2020

List of Records

Available Updates (your remote table)

Batch Level = Minor

Patches

Upgrade Center

2030

List of Records

Available Updates (your remote table)

Batch Level = Patch

 

Try It Out

You have built out the app's basics now. Use the modules you built to review the updates available and use the UI action to start the batch update process.

What’s Next

This app shows the basics of how to build a batch installer, but it is certainly not the limit of what can be done.  Here are some ideas that have been discussed on how to improve it.

Build UI in Next Experience

This example uses Core UI, which works, but this would be better if we rebuilt it in Next Experience.

Add ACLs to allow others to review updates

In my example, I did not add any ACLs, but now that you have your table, you should add some read ACLs to decide who can read it other than admins. This is a great way to empower your business users to review what is available.

Create a catalog item to request updates

Now that you have a way for your business owners to review updates, allow them to request that they be installed. Delegating this responsibility will empower your users and ease your burden.

 

What else would you like to see an installer like this do?  Let me know in the comments.

 

48 Comments
josgarcia
Tera Expert

Both myself and my colleague worked on getting this tool created for about a 1 1/2 days and finally figured it out. I am going to provide from beginning to end what instructions we had to go through to get this working. Hopefully this article and instruction proves to be helpful to you all as it was a very daunting 1 1/2 days for us...

 

Note: The coloring and formatting of this article are a little messed up, apologies for the confusion that this brings.

 

Batch Installer KB Article


Prerequisites

Plugin Requirements

Install the ServiceNow Product from the ServiceNow Application Manager called Continuous Integration and Continuous Delivery (CICD) Spoke.


Account Requirements

There are two separate accounts needed to implement this custom application:

  1. ServiceNow admin account
  2. ServiceNow service account

Role Requirements

Both accounts need the following roles for custom application to work:

  1. Admin Account: admin
  2. Service Account: sn_cicd.sys_ci_automation

Prerequisite Instructions

Plugin Installation Instructions

  1. In the All menu, search for Application Manager and click on the found module.
  2. Click on the Sync now button to ensure the store is up to date with the latest plugins and updates available.
  3. Search for CICD and hit enter.
  4. Scroll down on the found options under the Available for you tab and look at the options under the Servicenow products section.
  5. Click on the product named Continuous Integration and Continuous Delivery (CICD) Spoke.
  6. On the Get Started section within the Summary widget, click on Install.

Admin Account Instructions

  1. This knowledge article is to only be followed by a ServiceNow admin. If someone else is following these instructions, please ensure to contact your ServiceNow administration team for creating this process.

Service Account Instructions

  1. Request or create an account per your companies requirement.

 

Instructions

Creating the Batch Installer

Before you begin, ensure the prerequisites have been completed

Useful Documentation

All the information provided in this Knowledge Article was obtained by following the ServiceNow Dev Community post here.


Create CICD Credential

  1. In the All menu, search for and click on the module named Connection & Credential Aliases.
  2. If the plugin has been installed properly, under the Name column there will be a pre-made Credential named CICD. Open up that record.
    • Note: Do not change the "type" of this record to Connection & Credential as a basic auth credential is required for this setup.
  3. Click on New under the related list named Credentials.
  4. Fill out the form as follows:
    1. Name: Give the basic auth credential record a proper name, such as CICD Credentials.
    2. User name: The user name of the service account created from the prerequisite instructions.
    3. Password: The password for the service account created from the prerequisite instructions.
    4. Credential alias: If not already filled out, search for an available alias and select the option sn_cicd_spoke.CICD.
      • Note: If any of the records that are installed by default from product Continuous Integration and Continuous Delivery (CICD) Spoke, navigate to the product plugin with the ID of com.sn_cicd_spoke from the Application Manager and select Repair to have the records installed and validated.

Create a Custom Application

Follow the instructions provided here. Provide the application a proper name such as, Batch Install.

Once the application is created, change your Update Set scoped application to the newly created custom application.

Create a Remote Table

  1. In the All menu in the top left corner, search for Remote Table.
  2. Under the Remote Tables module, select Tables.
  3. Click New.
    • Provide the table a proper name such as, Available Updates.
    • Create the following fields on the remote table:  
      Column LabelColumn NameTypeReferenceChoice Fields
      ApplicationapplicationReferenceStore Application [sys_store_app] 
      Avail. Majormajor_countInteger  
      Avail. Minorminor_countInteger  
      Avail. Patchespatch_countInteger  
      Available Versionavailable_versionReferenceApplication Version [sys_app_version] 
      Batch Levelbatch_levelChoice Major | major // Minor | minor // Patch | patch
      Installed Versioninstalled_versionString  
      Latest Major Versionlatest_major_versionReferenceApplication Version [sys_app_version] 
      Latest Minor Versionlatest_minor_versionReferenceApplication Version [sys_app_version] 
      Latest Patch versionlatest_patch_versionReferenceApplication Version [sys_app_version] 
      Latest Version Levellatest_version_levelChoice Major | major // Minor | minor // Patch | patch
      LevellevelChoice Major | major // Minor | minor // Patch | patch
      NamenameString  
  4. Save the Remote Table record.

Create CICD Credential

1. In the All menu, search for Connection & Credential Aliases and click on the Connection & Credential Aliases module.

Create a Remote Table Definition

  1. Navigate to the All menu and search for Remote Tables and click on the Definitions module.
  2. Click New and fill out the following fields as shown:
    • Name: Provide the record a proper name such as, Populate Available Updates.
    • Table: Reference to the Remote Table created from the previous step.
    • Active: Set to True.
    • Script:
      (function executeQuery(v_table, v_query) {
          var res = {};
      
          function getAvailableVersion(appSysId) {
              var vers = [];
              var v = new GlideRecord('sys_app_version');
              v.addQuery('source_app_id', '=', appSysId);
              v.orderByDesc('version');
              v.query();
              while (v.next()) {
                  vers.push({
                      'sys_id': v.getUniqueValue(),
                      'name': v.getDisplayValue(),
                      'version': v.getValue('version'),
                      'publish_date': v.getValue('publish_date')
                  });
              }
              return vers;
          }
      
          function compareVersion(currentVer, availableVer) {
              var current = currentVer.split('.').map(Number);
              var available = availableVer.split('.').map(Number);
              if (current[0] !== available[0]) return 'major';
              if (current[1] !== available[1]) return 'minor';
              if (current[2] !== available[2]) return 'patch';
              return 'no';
          }
      
          function isNewerVersion(v1, v2) {
              var a = v1.split('.').map(Number);
              var b = v2.split('.').map(Number);
              for (var i = 0; i < 3; i++) {
                  if (a[i] > b[i]) return true;
                  if (a[i] < b[i]) return false;
              }
              return false;
          }
      
          var storeAppVer = new GlideRecord('sys_store_app');
          storeAppVer.addQuery('active', '=', true);
          storeAppVer.addQuery('update_available', '=', true);
          storeAppVer.query();
      
          while (storeAppVer.next()) {
              var appId = storeAppVer.getUniqueValue();
              var installedVersion = storeAppVer.getValue('version');
              var availableVersions = getAvailableVersion(appId);
      
              if (!res[appId]) {
                  res[appId] = {};
              }
      
              for (var i = 0; i < availableVersions.length; i++) {
                  var ver = availableVersions[i];
                  var level = compareVersion(installedVersion, ver.version);
                  if (level === 'no') continue;
      
                  if (!res[appId][level] || isNewerVersion(ver.version, res[appId][level].version)) {
                      res[appId][level] = {
                          sys_id: ver.sys_id,
                          application: appId,
                          level: level,
                          available_version: ver.sys_id,
                          version: ver.version,
                          installed_version: installedVersion,
                          name: storeAppVer.getDisplayValue() + ' - ' + ver.version,
                          batch_level: level,
                          major_count: level === 'major' ? 1 : 0,
                          minor_count: level === 'minor' ? 1 : 0,
                          patch_count: level === 'patch' ? 1 : 0
                      };
                  }
              }
          }
      
          // Add all available levels (major, minor, patch) even if they point to same version
          for (var app in res) {
              var seenVersions = {};
              for (var level of ['major', 'minor', 'patch']) {
                  var entry = res[app][level];
                  if (entry) {
                      var uniqueKey = entry.version + '-' + level;
                      if (!seenVersions[uniqueKey]) {
                          v_table.addRow(entry);
                          seenVersions[uniqueKey] = true;
                      }
                  }
              }
          }
      })(v_table, v_query);
  3. Save the Remote Table Definition record.
  4. Navigate to the newly created Remote Table list and ensure the columns and fields are populating.

Create a Batch Installer Subflow

  1. In the All menu in the top left corner, search for and access the Flow Designer module.
  2. Create a new Subflow and fill out the following fields:
    • Name: Provide a proper name, such as Batch Install Updates.
    • Application: The name of the created application scope from the beginning of this KB Article.
    • Run As: Set to the User who initiates session.
  3. Provide it the following Inputs and Outputs:
    Inputs
    LabelNameTypeDescription
    ApplicationsapplicationsStringThis is a list of sys_ids of applications to install.

    Outputs
    LabelNameTypeDescription
    Progress IDprogress_idStringProgress ID of the worker that is doing that install.
    Status Messagestatus_messageStringOutput message from that batch installer.
  4. Create Flow Variables as follows:
    LabelNameTypeDescription
    Batch Manifestbatch_manifestJSONPayload from the Subflow Applications input.
    Instance URLinstance_urlStringDynamically pull the instance url per instance.
  5. Add four separate actions as follows:
    • Look Up Records
      • Table: Application Version [sys_app_version]
      • Conditions: Sys ID is one of Subflow Inputs > Applications
    • Set Flow Variables
      • Batch Manifest: Scripted field input
        var payload = '{"name": "Store App Batch Installer", "notes": "Installing available patches from the Store", "packages": [';
        var loop = 0;
        while(fd_data._1__look_up_records.records.next())	
        {
            if(loop++ > 0)
            {
                payload += ',';
            }
            payload += '{"id": "' + fd_data._1__look_up_records.records.getValue('source_app_id') + '", "type": "application", ';
            payload += '"load_demo_data": false, "requested_version": "' + fd_data._1__look_up_records.records.getValue('version') + '", ';
            payload += '"notes": "' + fd_data._1__look_up_records.records.getDisplayValue() + '"}';
        }
        payload += ']}';
        return payload;
      • Instance URL: Scripted field input
        var instanceURL = gs.getProperty('glide.servlet.uri');
        
        // Remove trailing slash if it exists
        if (instanceURL.endsWith("/")) {
            instanceURL = instanceURL.slice(0, -1);
        }
        
        return instanceURL;
    • Batch Install
      • Batch Plan: Flow Variables > Batch Manifest.
      • Credential Alias [Connection & Credentials Aliases]: sn_cicd_spoke.CICD.
      • Instance URL: Flow Variables > Instance URL.
      • API Version: Leave this field blank.
    • Assign Subflow Outputs
      • Progress ID: 3 - Batch Install > Progress ID.
      • Status Message3 - Batch Install > Status Message.
  6. Publish the subflow.

Create UI Pages

The following records will be UI pages that are utilized upon selecting a custom UI action that will be created at a later step. These pages will provide a visual progress status for the state of the batch install progress.

  1. In the All menu, search for and select UI Pages.
  2. For the first out of two UI Pages, click New and fill out the fields as follows:
    • Name: updates_to_install
    • HTML:
      <?xml version="1.0" encoding="utf-8" ?>
      <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
          <g:evaluate>
              var apps = new GlideAggregate("x_ourcompanyname2_batch_in_0_st_available_updates");
              apps.addQuery('available_version', 'IN', '${sysparm_app_versions}');
              apps.groupBy('name');
              apps.groupBy('level');
              apps.orderBy('level');
              apps.orderBy('name');
              apps.setGroup(true);
              apps.query();
          </g:evaluate>
      
          <h3>${gs.getMessage('The following applications will be upgraded:')}</h3>
          <ul>
          <j:while test="${apps.next()}">
              <li>${apps.getValue('name')} - ${apps.getValue('level')}</li>
          </j:while>
          </ul>
          <g:ui_form>
              <input type="hidden" name="app_versions" value="${sysparm_app_versions}" />
              <input type="hidden" name="app_count" value="${apps.getRowCount()}" />
              <div class="form-group pull-right" style="padding-right: 20px;">
                  <g:dialog_buttons_ok_cancel ok="return actionOK()" cancel="return onCancel()" ok_text="${gs.getMessage('Install Updates')}" ok_id="ok_button" cancel_type="button" />
              </div>
          </g:ui_form>
      </j:jelly>
    • Client Script: Leave this field blank.
    • Processing Script:
      UpBegin();
      function UpBegin() {
              var progress_id = '-1';
              var status_message = '';
      
              try {
                  var inputs = {};
                  inputs['applications'] = app_versions; // String 
                  var result = sn_fd.FlowAPI.getRunner().subflow('x_ourcompanyname2_batch_in_0.batch_install_updates').inForeground().withInputs(inputs).run();
                  var outputs = result.getOutputs();
                  progress_id = outputs['progress_id'];
                  status_message = outputs['status_message'];
      
              } catch (ex) {
                  var message = ex.getMessage();
                  status_message = message;
                  alert.addErrorMessage(progress_id + ' - ' + message);
              }
          //var session = gs.getSession();
          //var URL = session.getUrlOnStack();
        
          URL = 'x_ourcompanyname2_batch_in_0_installing_updates.do?'; 
          URL += "&sysparm_pworker_sysid=" + progress_id + "&sysparm_message=" + status_message;
          URL += "&sysparm_app_count=" + app_count;
          URL += "&sysparm_app_versions=" + app_versions;
          response.sendRedirect(URL);
      }
  3. Save the record.
  4. Return to the UI Pages module and create a new UI Page with the following fields:
    • Name: installing_updates
    • HTML:
      <?xml version="1.0" encoding="utf-8" ?>
      <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
          <g:evaluate var="jvar_reload_now" jelly="true">
              var progressWorker = new GlideRecord('sys_progress_worker');
              progressWorker.addQuery('sys_id', '=', jelly.sysparm_pworker_sysid);
              progressWorker.query();
      
              var reloadNow = false;
      
              if(progressWorker.getRowCount() == 0)
              {
                  reloadNow = true;
              } else {
                  progressWorker.next();
                  if(progressWorker.getValue('state') == 'starting')
                  {
                      reloadNow = true;
                  }
              }
              reloadNow;
          </g:evaluate>
          <g:evaluate>
              var remaining_msg = '';
              var remaining_apps = {};
              var remaining_app_count = 0;
              var hasApps = false;
              var apps = new GlideAggregate('sys_app_version'); // 
              apps.addQuery('sys_id', 'IN', '${sysparm_app_versions}');
              apps.addAggregate('COUNT');
              apps.query();
              if(apps.next())
              {
                  remaining_app_count = parseInt(apps.getAggregate('COUNT'));
              }
              if(remaining_app_count)
              {
                  remaining_msg = gs.getMessage('Installation of {0} apps has been scheduled, {1} remaining to install.', [ ${sysparm_app_count}, remaining_app_count.toFixed(0) ]);
              }
              var completed_msg = gs.getMessage('{0} apps have been installed.', [ ${sysparm_app_count} ]);
          </g:evaluate>
          <g:ui_form>
              <input type="hidden" name="reloadNow" id="reloadNow" value="${jvar_reload_now}" />
          </g:ui_form>
          <h3>
              <j:if test='${remaining_app_count > 0}'>
                  ${remaining_msg}
              </j:if>
      
              <j:if test='${remaining_app_count == 0}'>
                  ${completed_msg}
              </j:if>
          </h3>
          <j:if test='${sysparm_message}'>
              ${sysparm_message}<br /><br />
          </j:if>
          <g:ui_form>
              <table cellpadding="0" cellspacing="0" width="500px">
                  <g:ui_progress_worker worker_id="${sysparm_pworker_sysid}" show_cancel="false" rows="grow"/>
              </table>
          </g:ui_form>
      </j:jelly>
    • Client Script:
      if(gel('reloadNow').value == 'true') {
          location.reload();
      }
      
      function onCancel() {
         GlideDialogWindow.get().destroy();
      }
    • Processing Script:
      var gr = new GlideRecord('sys_progress_worker');
      gr.addQuery('sys_id', '=', sysparm_pworker_sysid);
      gr.query();
      
      gr.next();
      
      gs.addInfoMessage(gr.getValue('state'));
      if (gr.getValue('state') == 'starting' || gr.getValue('state') == 'running') {
      
          var wait = 3;
          if(gr.getValue('state') == 'running')
          {
              wait = 15;
          }
          var pm = new GlideProgressMonitor(sysparm_pworker_sysid);
          pm.waitForCompletionOrTimeout(wait);
          var redirect = "x_ourcompanyname2_batch_in_0_installing_updates.do?sysparm_pworker_sysid=" + sysparm_pworker_sysid;
          response.sendRedirect(redirect);
      }
  5. Save the record.

Create UI Action

  1. In the All menu, under System Definition, search and click on the UI Actions module.
  2. Fill out the fields as follows:
    • Name: Install Updates.
    • Table: Reference to the custom Remote Table created earlier in this KB Article.
    • Active: True.
    • Show update: True.
    • Client: True.
    • List v2 Compatible: True.
    • List banner button: True.
    • List choice: True.
    • Onclick: openModalWithSelectedRecords().
    • Script: 
      function openModalWithSelectedRecords() {
      	
          var selectedRecords = g_list.getChecked();
      	
          if (selectedRecords.length == 0) {
              alert('Please select at least one record.');
              return;
          }
          var sysIds = selectedRecords;
          var dialogClass = GlideModal ? GlideModal : GlideDialogWindow;
          var dialog = new dialogClass('x_ourcompanyname2_batch_in_0_updates_to_install');
          dialog.setTitle('Selected Apps');
          dialog.setSize(600, 400);
          dialog.setPreference('sysparm_app_versions', sysIds);
          dialog.render();
      }
  3. Save the record.

Test the Batch Installer

Now that all the steps have been followed, here is what to expect when running the installer:

  1. In the All menu, search for Available Updates (name of the custom Remote Table), and click on the module to view the list of available updates.
  2. Filter the listed options as desired and check a few plugins that need to be updated in a batch and click on Install Updates (name of the UI Action).
    • A good query to run for testing is available_versionISNOTEMPTY^nameNOT LIKE@^level=patch.
    • Before clicking Install Updates UI action, your screen should look similar to this:
      Screenshot 2025-08-18 at 5.31.49 PM.png

       

  3. If the first UI Page and UI Action were configured properly, you should see a pop up confirming your selection of updates as displayed in this screenshot:

    Screenshot 2025-08-18 at 5.32.50 PM.png


  4. When the Install Updates button is selected, it will process the second UI Page as shown in this screenshot:

    Screenshot 2025-08-18 at 5.33.25 PM.png


  5. The page will periodically refresh to check for the progress on the selected plugins. As the plugins are installed, the number of remaining to install updates will start to go down as displayed in this screenshot:
    Screenshot 2025-08-18 at 5.33.53 PM.png


  6. Once all of the listed plugins have been updated, the following screen will be displayed to show the final result:
    Screenshot 2025-08-18 at 5.34.25 PM.png

     

  7. Finally, confirm that the that were updated are no longer shown in your custom Remote Table to identify that they truly updated.

    Screenshot 2025-08-18 at 5.34.48 PM.png

 

 

VGunnam
Tera Explorer

Hi @josgarcia 

I really appreciate the detailed knowledge article, it was really helpful.


Thanks,

Vamshi

 

Markus9
Tera Contributor

Here are instructions on how to add the link to the store in the remote table.

 

The links have the structure "https://appstoreprod.service-now.com/sn_appstore_store.do#!/store/application/SYS_STORE_APP_SYSID/VE...".

 

Example for the "Access Analyzer" application:

 

1) Create new Field on the Remote Table

  • Type: UI Action List (Enter the value "glide_action_list" directly using SN Utils, as the type is only available for selection for maint)
  • Column label: Actions
  • Column name: actions
  • Default value: Will be filled later with the SysID of the new UI action

 

0_field.png

 

 

2) Create Script Include

  • Name: StoreLinkUtil
  • Client callable: true
  • Script:
var StoreLinkUtil = Class.create();
StoreLinkUtil.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    getStoreLink: function() {
        var target_sys_id = this.getParameter('sysparm_target_sys_id');
        var store_url = '';

        var gr = new GlideRecord('sys_app_version');
        if (gr.get(target_sys_id) && gr.getValue('source_app_id') && gr.getValue('version')) {
            store_url = "https://appstoreprod.service-now.com/sn_appstore_store.do#!/store/application/" + gr.getValue('source_app_id') + "/" + gr.getValue('version');
        }

        return store_url;
    },

    type: 'StoreLinkUtil'
});

 

1_script_include.png

 

 

3) Create UI Action

  • Name: Store Link
  • Client: true
  • Onclick: onClick()
  • Script:
function onClick() {
    var target_sys_id = rowSysId;
    var ga = new GlideAjax('StoreLinkUtil');
    ga.addParam('sysparm_name', 'getStoreLink');
    ga.addParam('sysparm_target_sys_id', target_sys_id);
    ga.getXMLAnswer(function(response) {
        var store_url = response;
        if (store_url) {
            g_navigation.openPopup(store_url);
        } else {
            alert('Could not retrieve the URL.');
        }
    });
}

 

2_ui_action.png

 

 

4) Update Default Value on new Field

  • Default Value: SysID of the UI Action "Store Link"

 

 

5) Configure List Layout

  •  Add the new field to the default List Layout

 

3_list_layout.png

Conan Lloyd1
Tera Contributor

Question.  Out IT department blocks adding extensions to our browsers so I cannot get SN Tools.  Is there another way to add the UI Action List type of field?

Markus9
Tera Contributor

@Conan Lloyd1 

I was also able to create the field using a background script. It is important that the script is executed in the global scope.

 

Example of a new field:

Table: Available Updates [x_xxxx_available_updates]
Column Label: Actions
Column Name: actions
Type: UI Action List [glide_action_list]

 

Script:

new GlideDBUtil.createElement(
    'x_xxxx_available_updates',
    'Actions',
    'actions',
    'glide_action_list',
    '',
    'x_xxxx_available_updates',
    true
);

 

Anusha Napa1
Tera Contributor

Hi,

 

I cannot see application version table in the subflow while creating look up records. Please suggest someways to see

 

 

Anusha Napa1
Tera Contributor

Hi,

 

When I click on the ui action it is not working. 

 

can someone help on this
This is my ui action 

 

 

Anusha Napa1
Tera Contributor

When I click on install update this page is showing can someone help on this when i refresh the page i see the same count in the remote table

 

@Adam Stout  @josgarcia