Built something you're proud of? Tell the story. A quick G2 review of App Engine or Build Agent helps other developers see what's possible on ServiceNow. Share your experience.

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.



4 REPLIES 4

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 Approved → Run Script (fulfillment logic above) → Set Values (close RITM) → End
  4. If Rejected → Set 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.

Hi Naveen,

Thank you for sharing the code and the flow. However, after implementing the script as provided, the issue still persists — users are still not being added or removed from the group as expected.

Could you please look into this and suggest an alternate approach if possible? Any guidance would be greatly appreciated.

Thanks,
Mustakim

 

Naveen20
ServiceNow Employee

The global Script Include bridge still executes within the session's security context, so SIR's ACL enforcement on sys_user_grmember silently blocks the insert/delete even from global scope. The record operation simply returns null or false with no visible error.

Here are two alternate approaches that actually work post-SIR.


Approach 1: Event + Script Action (Most Reliable)

This works because Script Actions execute in the system context with elevated privileges, completely bypassing SIR's session-level ACL enforcement on group membership tables.

Step 1 — Register a Custom Event

Navigate to System Policy > Events > Registry and create:

Field Value
Event Name custom.manage_group_member
Table sc_req_item
Fired by System

Step 2 — Fire the Event from Your Workflow Run Script

Replace your current fulfillment Run Script with this — instead of directly manipulating sys_user_grmember, it fires the event:

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

    if (!selectedUsers || !groupID) {
        gs.log('ManageGroupMembers: Missing group or users', 'Fulfillment');
        return;
    }

    // Build a param string: action|groupSysId|user1,user2,user3
    var eventParam = action + '|' + groupID + '|' + selectedUsers;

    // Fire event — the Script Action will handle the actual membership change
    gs.eventQueue('custom.manage_group_member', current, eventParam, '');

    gs.log('ManageGroupMembers: Event fired with param: ' + eventParam, 'Fulfillment');
})();

Step 3 — Create the Script Action

Navigate to System Policy > Events > Script Actions and create:

Field Value
Name Manage Group Member Fulfillment
Event Name custom.manage_group_member
Active true
Order 100

Script:

(function() {
    // event.parm1 format: action|groupSysId|user1,user2,user3
    var params = event.parm1.toString().split('|');

    if (params.length < 3) {
        gs.log('ManageGroupMembers ScriptAction: Invalid params', 'Fulfillment');
        return;
    }

    var action = params[0];
    var groupID = params[1];
    var userList = params[2].split(',');

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

        if (action == 'add') {
            _addMember(groupID, userSysId);
        } else if (action == 'remove') {
            _removeMember(groupID, userSysId);
        }
    }

    function _addMember(groupSysId, userSysId) {
        var grCheck = new GlideRecord('sys_user_grmember');
        grCheck.addQuery('group', groupSysId);
        grCheck.addQuery('user', userSysId);
        grCheck.query();
        if (grCheck.hasNext()) {
            gs.log('ManageGroupMembers: User ' + userSysId +
                ' already in group ' + groupSysId, 'Fulfillment');
            return;
        }

        var gr = new GlideRecord('sys_user_grmember');
        gr.initialize();
        gr.setValue('group', groupSysId);
        gr.setValue('user', userSysId);
        var sysId = gr.insert();

        if (sysId) {
            gs.log('ManageGroupMembers: Added user ' + userSysId +
                ' to group ' + groupSysId, 'Fulfillment');
        } else {
            gs.error('ManageGroupMembers: FAILED to add user ' + userSysId +
                ' to group ' + groupSysId, 'Fulfillment');
        }
    }

    function _removeMember(groupSysId, userSysId) {
        var gr = new GlideRecord('sys_user_grmember');
        gr.addQuery('group', groupSysId);
        gr.addQuery('user', userSysId);
        gr.query();
        if (gr.next()) {
            gr.deleteRecord();
            gs.log('ManageGroupMembers: Removed user ' + userSysId +
                ' from group ' + groupSysId, 'Fulfillment');
        } else {
            gs.log('ManageGroupMembers: User ' + userSysId +
                ' not found in group ' + groupSysId, 'Fulfillment');
        }
    }
})();

Why this works: The event queue is processed by the system scheduler, which runs with full system privileges. SIR's session-based ACL enforcement doesn't apply because there's no user session — it's the system acting on a queued event.


Approach 2: Flow Designer with OOTB Spoke Actions

If you're open to migrating away from legacy Workflow to Flow Designer, this is the cleanest long-term approach.

Step 1 — Create a Flow triggered by RITM update where Approval = Approved and State = Closed Complete (or use a subflow called from your existing workflow).

Step 2 — Use the OOTB "Add Members to Group" and "Remove Members from Group" Flow Designer actions from the Group spoke (sn_group). These actions are already SIR-aware and execute with the correct internal privilege escalation.

Step 3 — Map your RITM variables (group, selected_users, action) into the action inputs using Flow Designer data pills.

This approach avoids all custom scripting for the membership change itself.


Debugging Tip

To confirm that SIR is indeed what's blocking you, temporarily check the system logs after the script runs. Look for entries like:

Security restricted: Insert on sys_user_grmember denied

You can also test by navigating to System Diagnostics > Session Debug > Debug Security and replaying the scenario. If SIR is the blocker, you'll see explicit ACL denial entries in the node log.


I'd recommend Approach 1 since it plugs directly into your existing workflow with minimal changes — just swap the Run Script content and add the event + script action. The slight downside is that event processing is asynchronous (usually within a few seconds), so the membership change won't be instantaneous upon approval, but it's effectively immediate for practical purposes.

Hi @Naveen20,

Thank you for the solution provided.

However, we are still facing issues while adding/removing users to the group. Could you please let us know if there is any alternative approach to resolve this without role limitations?

Your guidance on this would be highly appreciated.

Thanks,
Mustakim