How to Run Post-Approval Actions as System User in ServiceNow (sn_si.admin role Limitation)

mustakimkhan111
Tera Contributor

Hi Community,

I am working on a Service Catalog item "Manage Group Members" where users can
request to add or remove members from a group. The approval is routed to the
group manager via a workflow, and once approved, the system should automatically
add or remove the selected users from the group.

PROBLEM

After the group manager approves the request and the RITM reaches
State = Closed Complete and Approval = Approved, the selected users
are NOT being added or removed from the group.

Has anyone solved this specific scenario — group member management
post-SIR deployment — without granting sn_si.admin to approvers?

Please find the workflow item script which is written for approval of group.


answer = [];
 
var currentUser = gs.getUserID();
var groupID = current.variables.group;
var groupManager = current.variables.group.manager;
var currentUserManager = current.variables.OnBehalfOf.manager;
 
if (currentUser == groupManager) {
    answer = [];
} else if (currentUser != groupManager && groupManager.active.getDisplayValue() == 'false' && currentUserManager != '') {
    answer.push(currentUserManager);
} else if (currentUser != groupManager && groupManager.active.getDisplayValue() == 'true') {
    answer.push(groupManager);
} else if (currentUser != groupManager && groupManager.active.getDisplayValue() == 'false' && currentUserManager == '') {
    gs.addErrorMessage('You are not able to place this request as there is no one eligible to approve the request. Approvals are sent to the groups manager first. If the group does not have a manager or the manager is an inactive user, an approval is sent to your manager. In the case that you do not have a manager, the request will be cancelled');
    current.approval = 'rejected';
    current.state = '4';
    current.active = 'false';
    current.update();
}

 

Any guidance or confirmed working approach would be greatly appreciated.

Thank you.



1 REPLY 1

Naveen20
ServiceNow Employee

Script you shared is only the approval routing logic — it determines who approves the request. It doesn't contain any fulfillment logic to actually add or remove users from the group after approval. That's why nothing happens post-approval.

You need a separate fulfillment activity in your workflow (or a business rule on the RITM) that fires after the approval is granted. Here's a confirmed working approach, including the SIR-related scoping challenge.

 

After SIR (Security Improvement Release) deployments (Washington DC+), direct GlideRecord insert/delete operations on sys_user_grmember from a scoped app are blocked unless the executing context has elevated privileges. This is why many people hit a wall here — the workflow runs, approval completes, but the membership change silently fails with no error.

Try this

Since you've dealt with cross-scope challenges before, the cleanest pattern is a global-scope Script Include that your scoped workflow can invoke.

Step 1 — Global Script Include (ManageGroupMemberUtils)

Create this in the Global scope:

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

    addMember: function(groupSysId, userSysId) {
        // Check if already a member
        var grCheck = new GlideRecord('sys_user_grmember');
        grCheck.addQuery('group', groupSysId);
        grCheck.addQuery('user', userSysId);
        grCheck.query();
        if (grCheck.hasNext()) {
            return 'already_member';
        }

        var gr = new GlideRecord('sys_user_grmember');
        gr.initialize();
        gr.setValue('group', groupSysId);
        gr.setValue('user', userSysId);
        var result = gr.insert();
        return result ? 'added' : 'failed';
    },

    removeMember: function(groupSysId, userSysId) {
        var gr = new GlideRecord('sys_user_grmember');
        gr.addQuery('group', groupSysId);
        gr.addQuery('user', userSysId);
        gr.query();
        if (gr.next()) {
            gr.deleteRecord();
            return 'removed';
        }
        return 'not_found';
    },

    type: 'ManageGroupMemberUtils'
};

Mark it Client callable: false, Accessible from: All application scopes.

Step 2 — Workflow "Run Script" Activity (after Approval)

In your workflow, add a Run Script activity that executes after the Approval step completes successfully. This is the fulfillment logic:

(function() {
    var ritm = current; // sc_req_item
    var groupID = ritm.variables.group.toString();
    var action = ritm.variables.action.toString(); // 'add' or 'remove'

    // Handle multi-select users (assuming a List Collector variable)
    var selectedUsers = ritm.variables.selected_users.toString();
    if (!selectedUsers || !groupID) {
        gs.log('ManageGroupMembers: Missing group or users', 'Fulfillment');
        return;
    }

    var util = new global.ManageGroupMemberUtils();
    var userList = selectedUsers.split(',');

    for (var i = 0; i < userList.length; i++) {
        var userSysId = userList[i].trim();
        if (!userSysId) continue;

        var result;
        if (action == 'add') {
            result = util.addMember(groupID, userSysId);
        } else if (action == 'remove') {
            result = util.removeMember(groupID, userSysId);
        }

        gs.log('ManageGroupMembers: User ' + userSysId + ' -> ' + result, 'Fulfillment');
    }
})();

Step 3 — Cross-Scope Access Record

If your catalog item lives in a scoped app, you'll need a sys_scope_privilege record granting your scoped app access to invoke ManageGroupMemberUtils. Navigate to System Definition > Cross-Scope Privileges and create a record allowing your app to call the global Script Include. Alternatively, if your workflow itself runs in global scope (which is typical for legacy workflows), this may not be needed.

Workflow Structure Summary

Your workflow should look like this:

  1. Begin
  2. Approval – User Script (your existing routing script) →
  3. If ApprovedRun Script (fulfillment logic above) → Set Values (close RITM) → End
  4. If RejectedSet Values (reject RITM) → End

Quick Note on Your Approval Script

One thing to watch in your existing approval routing — on line 71, you're checking groupManager.active.getDisplayValue() == 'false' via a dot-walked reference field. This works but can be fragile. Consider using gr.getValue() style checks or a direct GlideRecord lookup for the manager's active state, especially if you hit intermittent issues where approvals route incorrectly.