How to Run Post-Approval Actions as System User in ServiceNow (sn_si.admin role Limitation)
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
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.
Any guidance or confirmed working approach would be greatly appreciated.
Thank you.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
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:
- Begin →
- Approval – User Script (your existing routing script) →
- If Approved → Run Script (fulfillment logic above) → Set Values (close RITM) → End
- 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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
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
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
a week ago
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
