Multi-select new list action in change request list

NMMZ10
Tera Contributor

Implement a multi-select List Action on the Change Request list to allow users to mass-cancel Change Requests that were automatically created from Standard Change templates.

It must be available only for change manager role.

The action must:

Allow selecting multiple Change Requests.

Prompt the user for a comment (mandatory).

Set the state to Cancelled and record the comment in the Work Notes or Closure Notes.

Be reusable for future similar needs.

This prevents abandoned Change Requests when the wrong Standard Change is opened.



The requirements are: 

When triggered:

  • User sees a prompt asking for a mandatory comment.
  • Selected Change Requests change to state Cancelled.
  • The comment is written into work notes or closure notes.

Only visible to appropriate roles (change managers / fulfiller roles).

No impact on existing Standard Change workflows.

What would be the ideal way to create this? How would you do it? 
Ui Action? Ui builder? 

5 REPLIES 5

Ankur Bawiskar
Tera Patron
Tera Patron

@NMMZ10 

where you want this? Native or Workspace?

what did you start with and where are you stuck?

💡 If my response helped, please mark it as correct and close the thread 🔒— this helps future readers find the solution faster! 🙏

Regards,
Ankur
Certified Technical Architect  ||  9x ServiceNow MVP  ||  ServiceNow Community Leader

Native. 
Ive created a UI action and a Script include.

Ui action 

function onClick() {
    // Get selected sys_ids from the list (works in List v2/v3)
    var listObj;
    try {
        listObj = (typeof g_list !== 'undefined') ? g_list : GlideList2.get(g_list.getListID());
    } catch (e) {
        listObj = GlideList2.get(g_list.getListID());
    }
    var sysIds = listObj.getChecked(); // comma-separated sys_ids

    if (!sysIds) {
        gsftInform('Please select one or more Change Requests to cancel.');
        return;
    }

    // Open a modal UI Page to capture the mandatory comment
    var dlg = new GlideModal('mass_cancel_comment_ui'); // UI Page name (create in step 2)
    dlg.setTitle('Cancel selected Standard Changes');
    dlg.setPreference('sys_ids', sysIds);
    dlg.setPreference('table', 'change_request');
    dlg.render();
}

 

Script include

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

    // === Mass cancel for Change Requests ===
    cancelChanges: function() {
        var result = {
            updated: 0,
            skipped: [],
            errors: []
        };

        // Role guard: only change managers
        if (!gs.hasRole('change_manager')) {
            result.errors.push('Insufficient privileges: change_manager role required.');
            return JSON.stringify(result);
        }

        var sysIdsStr = (this.getParameter('sysparm_sys_ids') || '').trim();
        var comment = (this.getParameter('sysparm_comment') || '').trim();
        var requireStandard = (this.getParameter('sysparm_require_standard') || 'false') === 'true';

        if (!sysIdsStr) {
            result.errors.push('No records selected.');
            return JSON.stringify(result);
        }
        if (!comment) {
            result.errors.push('Comment is mandatory.');
            return JSON.stringify(result);
        }

        // Resolve "Canceled" internal value for change_request.state, by label
        var canceledValue = this._getChoiceValue('change_request', 'state', 'Canceled');
        if (!canceledValue) {
            result.errors.push('Could not resolve the "Canceled" state choice on change_request.state.');
            return JSON.stringify(result);
        }

        // Batch query for performance
        var sysIds = sysIdsStr.split(',').map(function(x) {
            return x.trim();
        }).filter(Boolean);
        var gr = new GlideRecord('change_request');
        gr.addQuery('sys_id', 'IN', sysIds);

        if (requireStandard) {
            // Default filter: type == Standard
            // Change this if your instance uses a different field to mark "Standard template origin"
            gr.addQuery('type', 'standard');
        }

        gr.query();

        while (gr.next()) {
            try {
                // Skip if already canceled
                if (gr.getValue('state') == canceledValue) {
                    result.skipped.push(gr.getUniqueValue());
                    continue;
                }

                // Journal entry + closure notes
                gr.work_notes = 'Mass-canceled by ' + gs.getUserDisplayName() + ' — ' + comment;
                try {
                    gr.setValue('close_notes', comment);
                } catch (e) {}

                // Set state
                gr.setValue('state', canceledValue);
                gr.update();

                result.updated++;
            } catch (ex) {
                result.errors.push('Error on ' + gr.getDisplayValue() + ': ' + ex.message);
            }
        }

        // Identify selected records that were filtered out by requireStandard
        if (requireStandard) {
            var touched = result.updated + result.skipped.length;
            var selectedCount = sysIds.length;
            var notFoundCount = selectedCount - touched;
            if (notFoundCount > 0) {
                result.errors.push(notFoundCount + ' selected records were not Standard Changes (and were ignored).');
            }
        }

        return JSON.stringify(result);
    },

    // === Generic mass state changer (reusable for other tables) ===
    genericMassStateChange: function() {
        var result = {
            updated: 0,
            skipped: [],
            errors: []
        };

        if (!gs.hasRole('change_manager')) {
            result.errors.push('Insufficient privileges: change_manager role required.');
            return JSON.stringify(result);
        }

        var tableName = (this.getParameter('sysparm_table') || '').trim();
        var sysIdsStr = (this.getParameter('sysparm_sys_ids') || '').trim();
        var comment = (this.getParameter('sysparm_comment') || '').trim();
        var targetLabel = (this.getParameter('sysparm_target_state_label') || '').trim();
        var extraQuery = (this.getParameter('sysparm_extra_query') || '').trim(); // encoded query, optional

        if (!tableName || !sysIdsStr || !targetLabel) {
            result.errors.push('Missing required parameters (table, sys_ids, target_state_label).');
            return JSON.stringify(result);
        }
        if (!comment) {
            result.errors.push('Comment is mandatory.');
            return JSON.stringify(result);
        }

        var targetValue = this._getChoiceValue(tableName, 'state', targetLabel);
        if (!targetValue) {
            result.errors.push('Could not resolve target state "' + targetLabel + '" for ' + tableName + '.');
            return JSON.stringify(result);
        }

        var sysIds = sysIdsStr.split(',').map(function(x) {
            return x.trim();
        }).filter(Boolean);
        var gr = new GlideRecord(tableName);
        gr.addQuery('sys_id', 'IN', sysIds);
        if (extraQuery) gr.addEncodedQuery(extraQuery);
        gr.query();

        while (gr.next()) {
            try {
                if (gr.getValue('state') == targetValue) {
                    result.skipped.push(gr.getUniqueValue());
                    continue;
                }
                // Journal + closure notes (if field exists)
                try {
                    gr.work_notes = '[Mass change] ' + comment;
                } catch (e) {}
                try {
                    gr.setValue('close_notes', comment);
                } catch (e) {}

                gr.setValue('state', targetValue);
                gr.update();
                result.updated++;
            } catch (ex) {
                result.errors.push('Error on ' + gr.getDisplayValue() + ': ' + ex.message);
            }
        }
        return JSON.stringify(result);
    },

    // Helper: fetch internal choice value by label
    _getChoiceValue: function(table, element, label) {
        var ch = new GlideRecord('sys_choice');
        ch.addQuery('name', table);
        ch.addQuery('element', element);
        ch.addQuery('label', label);
        ch.addQuery('language', gs.getSession().getLanguage());
        ch.setLimit(1);
        ch.query();
        if (ch.next()) return String(ch.getValue('value'));

        // Fallback any language
        var ch2 = new GlideRecord('sys_choice');
        ch2.addQuery('name', table);
        ch2.addQuery('element', element);
        ch2.addQuery('label', label);
        ch2.setLimit(1);
        ch2.query();
        if (ch2.next()) return String(ch2.getValue('value'));

        return null;
    },

    type: 'MassChangeActions'
};

@NMMZ10 

so what's not working?

what debugging did you do?

Regards,
Ankur
Certified Technical Architect  ||  9x ServiceNow MVP  ||  ServiceNow Community Leader

Dr Atul G- LNG
Tera Patron
Tera Patron

Hi @NMMZ10 

We faced something similar in a previous implementation for Incident Management.

Before you start, there are a few things you need to consider:

  • A UI Action or List Action would be the best option to implement this.

  • You need to build a clear process for what should happen when a change is moved to the Canceled state.

  • If there are multiple changes in a list view, how will the user select the required change?

  • Should this apply to all change records, or only specific ones?

*************************************************************************************************************
If my response proves useful, please indicate its helpfulness by selecting " Accept as Solution" and " Helpful." This action benefits both the community and me.

Regards
Dr. Atul G. - Learn N Grow Together
ServiceNow Techno - Functional Trainer
LinkedIn: https://www.linkedin.com/in/dratulgrover
YouTube: https://www.youtube.com/@LearnNGrowTogetherwithAtulG
Topmate: https://topmate.io/dratulgrover [ Connect for 1-1 Session]

****************************************************************************************************************