Combine members from two groups into a single array and trigger approvals

saint
Tera Expert

Hi everyone,

I have a requirement where I’m working with two groups in ServiceNow.

I’m able to:

  • Look up both group records
  • Retrieve the members from each group

What I’m trying to do next is:

  • Combine the users who are common to both groups (intersection of Group A and Group B) into a single array
  • Use that array to trigger an approval for those users

I’m currently stuck on:

  • How to properly use the forEach loop
  • How to set and populate a variable/array with the matching users
I’m getting confused about the best approach and the solution. 
 

Could someone please help with an example or guidance on how to:

  1. Compare members from two groups
  2. Store the common users in an array
  3. Trigger approvals for those users

Any help would be appreciated. Thanks in advance!

3 REPLIES 3

Pavan Srivastav
ServiceNow Employee

Hello Saint,

 

Here is a complete working example with explanation at each step:


Step 1 — Retrieve Members from Both Groups

function getGroupMembers(groupSysId) {
    var members = [];
    var gr = new GlideRecord('sys_user_grmember');
    gr.addQuery('group', groupSysId);
    gr.query();
    while (gr.next()) {
        members.push(gr.getValue('user')); // stores user sys_id
    }
    return members;
}

var groupASysId = 'YOUR_GROUP_A_SYS_ID';
var groupBSysId = 'YOUR_GROUP_B_SYS_ID';

var groupAMembers = getGroupMembers(groupASysId);
var groupBMembers = getGroupMembers(groupBSysId);

gs.info('Group A members: ' + groupAMembers);
gs.info('Group B members: ' + groupBMembers);

Step 2 — Find the Intersection (Common Users)

This is where your forEach confusion likely is. Here is the pattern explained clearly:

var commonUsers = [];

// Loop through every user in Group A
groupAMembers.forEach(function(userSysId) {
    
    // For each Group A user, check if they also exist in Group B
    if (groupBMembers.indexOf(userSysId) !== -1) {
        // indexOf returns -1 if NOT found, anything else means it IS found
        commonUsers.push(userSysId);
    }
});

gs.info('Common users: ' + commonUsers);
gs.info('Total common users: ' + commonUsers.length);

Why indexOf !== -1?

  • indexOf searches the array for a value
  • Returns the position (0, 1, 2...) if found
  • Returns -1 if not found
  • So !== -1 means "was found in the array"

Step 3 — Trigger Approvals for Common Users

function createApprovalForUser(userSysId, targetRecord) {
    var approval = new GlideRecord('sysapproval_approver');
    approval.setValue('approver', userSysId);
    approval.setValue('sysapproval', targetRecord); // sys_id of record needing approval
    approval.setValue('state', 'requested');
    approval.setValue('source_table', 'YOUR_TABLE_NAME'); // e.g. 'sc_request'
    var approvalSysId = approval.insert();
    gs.info('Approval created: ' + approvalSysId + ' for user: ' + userSysId);
    return approvalSysId;
}

// Loop through common users and create an approval for each
commonUsers.forEach(function(userSysId) {
    createApprovalForUser(userSysId, current.sys_id);
});

Complete Combined Script

// ------------------------------------------------
// 1. Helper — get all member sys_ids from a group
// ------------------------------------------------
function getGroupMembers(groupSysId) {
    var members = [];
    var gr = new GlideRecord('sys_user_grmember');
    gr.addQuery('group', groupSysId);
    gr.query();
    while (gr.next()) {
        members.push(gr.getValue('user'));
    }
    return members;
}

// ------------------------------------------------
// 2. Helper — create one approval record
// ------------------------------------------------
function createApproval(userSysId, recordSysId, tableName) {
    var approval = new GlideRecord('sysapproval_approver');
    approval.setValue('approver', userSysId);
    approval.setValue('sysapproval', recordSysId);
    approval.setValue('state', 'requested');
    approval.setValue('source_table', tableName);
    var newSysId = approval.insert();
    
    // Log the user's name alongside the sys_id for easier debugging
    var user = new GlideRecord('sys_user');
    user.get(userSysId);
    gs.info('Approval created for: ' + user.getDisplayValue() + ' (' + userSysId + ')');
    
    return newSysId;
}

// ------------------------------------------------
// 3. Main logic
// ------------------------------------------------
var GROUP_A_SYS_ID = 'YOUR_GROUP_A_SYS_ID'; // replace with real sys_ids
var GROUP_B_SYS_ID = 'YOUR_GROUP_B_SYS_ID';
var TARGET_TABLE   = 'YOUR_TABLE_NAME';      // table of the record being approved

var groupAMembers = getGroupMembers(GROUP_A_SYS_ID);
var groupBMembers = getGroupMembers(GROUP_B_SYS_ID);

// ------------------------------------------------
// 4. Find intersection
// ------------------------------------------------
var commonUsers = [];
groupAMembers.forEach(function(userSysId) {
    if (groupBMembers.indexOf(userSysId) !== -1) {
        commonUsers.push(userSysId);
    }
});

gs.info('Found ' + commonUsers.length + ' common user(s): ' + commonUsers.join(', '));

// ------------------------------------------------
// 5. Create approvals
// ------------------------------------------------
if (commonUsers.length === 0) {
    gs.warn('No common users found between the two groups. No approvals created.');
} else {
    commonUsers.forEach(function(userSysId) {
        createApproval(userSysId, current.sys_id, TARGET_TABLE);
    });
}

Visual Breakdown of the forEach Logic

groupAMembers = ['user1', 'user2', 'user3', 'user4']
groupBMembers = ['user2', 'user4', 'user5']

forEach iteration:
  'user1' → indexOf in B = -1  → NOT added  ✗
  'user2' → indexOf in B =  0  → ADDED       ✓
  'user3' → indexOf in B = -1  → NOT added  ✗
  'user4' → indexOf in B =  1  → ADDED       ✓

commonUsers = ['user2', 'user4']

Key Things to Remember

Concept Detail
sys_user_grmember The table that links users to groups
indexOf !== -1 The standard way to check if a value exists in a JS array
forEach Loops every item — the inner function receives each item as its argument
sysapproval_approver The table where individual approval records live
Always guard commonUsers.length === 0 Avoid silently creating no approvals without a log entry

Hi @Pavan Srivastav  Thank you for your respose, I'm trying to build this in flow designer, and wondering how to set the flow variable values as array of unique user records, with common members of both the groups

Pavan Srivastav
ServiceNow Employee

Sure Saint.

 

Flow Designer handles this differently from script since it's a no-code/low-code environment, but for array intersection logic you will need a small Script step. Here is the complete approach:


Overall Flow Structure

 
 
Trigger (your trigger)
Look Up Records → Group A Members  (sys_user_grmember, group = Group A)
Look Up Records → Group B Members  (sys_user_grmember, group = Group B)
Script Step → Find Common Users + Build Array
For Each → Loop commonUsers array
    Ask For Approval (inside loop)

Step 1 — Create Flow Variables

Before building the steps, set up your flow variables first. Go to Flow Variables (the pill icon at the top of the flow canvas):

Variable Name Type Notes
group_a_sys_id String sys_id of Group A
group_b_sys_id String sys_id of Group B
common_users Array.Reference (sys_user) Will hold the intersection result

Step 2 — Look Up Group Members

Add two Look Up Records actions:

Look Up #1 — Group A Members

 
 
Table:      Group Members [sys_user_grmember]
Condition:  Group  is  <your Group A sys_id or trigger pill>

Look Up #2 — Group B Members

 
 
Table:      Group Members [sys_user_grmember]  
Condition:  Group  is  <your Group B sys_id or trigger pill>

These return record collections you will reference in the Script step.


Step 3 — Script Step (Core Logic)

Add an Action → Script step. This is where the intersection happens.

Script Step Inputs

Define these inputs inside the Script step so you can pass Flow data in:

Input Name Value (drag pill from)
groupAMembers Look Up #1 → Records
groupBMembers Look Up #2 → Records

Script Step Outputs

Define one output:

Output Name Type
commonUsers Array

The Script

 
 
javascript
(function execute(inputs, outputs) {

    // Build array of user sys_ids from Group A
    var groupAUserIds = [];
    var membersA = inputs.groupAMembers; 
    for (var i = 0; i < membersA.size(); i++) {
        var memberA = membersA.get(i);
        groupAUserIds.push(memberA.getValue('user'));
    }

    // Build array of user sys_ids from Group B
    var groupBUserIds = [];
    var membersB = inputs.groupBMembers;
    for (var j = 0; j < membersB.size(); j++) {
        var memberB = membersB.get(j);
        groupBUserIds.push(memberB.getValue('user'));
    }

    // Find intersection — users in BOTH groups
    var commonUserIds = [];
    groupAUserIds.forEach(function(userSysId) {
        if (groupBUserIds.indexOf(userSysId) !== -1) {
            // Deduplicate — only add if not already in array
            if (commonUserIds.indexOf(userSysId) === -1) {
                commonUserIds.push(userSysId);
            }
        }
    });

    gs.info('Flow: Common users found = ' + commonUserIds.length);

    // Set the output — array of user sys_ids
    outputs.commonUsers = commonUserIds;

})(inputs, outputs);

Step 4 — Set Flow Variable

After the Script step, add a Set Flow Variables action:

 
 
common_users  =  Script Step → commonUsers (output pill)

This writes the intersection result into your flow-level variable so it is available to subsequent steps.


Step 5 — For Each Loop → Ask For Approval

Add a For Each action:

 
 
Items:  Flow Variables → common_users

Inside the loop, add Ask For Approval:

 
 
Table:    your target table
Record:   your trigger record pill
Approve:  For Each Item → User  (the current loop item)

Full Flow Visualised

 
 
[Trigger]
    ├─► [Look Up Records] Group A members  ──► groupARecords
    ├─► [Look Up Records] Group B members  ──► groupBRecords
    ├─► [Script Step]
    │       inputs:  groupARecords, groupBRecords
    │       output:  commonUsers (array of sys_ids)
    ├─► [Set Flow Variables]
    │       common_users = Script output pill
    └─► [For Each]  items = common_users
            └─► [Ask For Approval]
                    approver = current loop item (user)

Important Gotchas

Gotcha 1 — Array type must match When you define common_users as a flow variable, set the type to Array.Reference pointing to sys_user — this ensures the For Each loop correctly resolves each item as a user record for the approval step.

Gotcha 2 — Script step input type for collections When passing Look Up Records output into a Script step input, set the input type to GlideRecord List not String — otherwise .size() and .get() won't work on it.

Gotcha 3 — Empty intersection guard Add an If condition after the Set Flow Variables step before the For Each:

 
 
Condition:  common_users  is not empty

This prevents the For Each from erroring on an empty array if the two groups share no members.