Salesforce Queue Users Audit is a practical way to ensure every record routed to a queue finds a real, active owner. In complex orgs, queues can end up with no users or with members hidden inside groups and roles. In this article, I’ll share two Apex scripts that make this audit simple: one that lists queues with their users and another that shows queues without any active users — all ready to run in Execute Anonymous.

Why Perform a Salesforce Queue Users Audit?
Queues without users can hold cases or leads indefinitely, creating invisible bottlenecks in your service process. By running this audit, you can quickly detect queues that are empty or improperly configured — ensuring that all routed records are handled on time and by the right people.
- Prevent orphaned cases: Identify queues that no one is monitoring.
- Improve governance: Track who can access or manage each queue.
- Increase SLA accuracy: Ensure every assignment has an active owner.
Understanding Queue Membership in Salesforce
Each queue (Group.Type = 'Queue') can contain:
- Users — added directly.
- Public Groups — containing users, other groups, or roles.
- Roles / Roles and Subordinates — which indirectly include multiple users.
Because memberships can be nested, you can’t rely on a simple SOQL query. The scripts below use recursion to explore groups, roles, and users to produce a full audit report.
Script 1 — Queues With Users
The first script, QueueUsersReport, lists all queues and their respective active users — whether assigned directly or through nested groups or roles. The result is printed as a CSV in the debug log.
public class QueueUsersReport {
private String csvEsc(String s) {
if (s == null) return '';
String v = s.replace('"', '""');
if (v.contains(';') || v.contains('\n') || v.contains('\r') || v.contains('"'))
v = '"' + v + '"';
return v;
}
private void traverseGroup(
Id groupId,
Set<Id> visited,
Map<Id, List<Id>> groupToMembers,
Set<Id> activeUserIds,
Map<Id, User> activeUsersMap,
Set<Id> roleIds,
Map<Id, List<Id>> roleToUsers,
String queueName,
List<String> csvLines
) {
if (groupId == null || visited.contains(groupId)) return;
visited.add(groupId);
List<Id> members = groupToMembers.get(groupId);
if (members == null) return;
for (Id memberId : members) {
if (activeUserIds.contains(memberId)) {
User u = activeUsersMap.get(memberId);
csvLines.add(csvEsc(queueName) + ';' + csvEsc(u != null ? u.Name : String.valueOf(memberId)));
continue;
}
if (roleIds.contains(memberId)) {
List<Id> usersInRole = roleToUsers.get(memberId);
if (usersInRole != null) {
for (Id uid : usersInRole) {
User u = activeUsersMap.get(uid);
csvLines.add(csvEsc(queueName) + ';' + csvEsc(u != null ? u.Name : String.valueOf(uid)));
}
}
continue;
}
if (groupToMembers.containsKey(memberId)) {
traverseGroup(memberId, visited, groupToMembers, activeUserIds, activeUsersMap, roleIds, roleToUsers, queueName, csvLines);
}
}
}
public void run() {
List<Group> queues = [SELECT Id, Name FROM Group WHERE Type = 'Queue'];
List<GroupMember> allGroupMembers = [SELECT GroupId, UserOrGroupId FROM GroupMember];
Map<Id, List<Id>> groupToMembers = new Map<Id, List<Id>>();
Set<Id> allMemberIds = new Set<Id>();
for (GroupMember gm : allGroupMembers) {
if (!groupToMembers.containsKey(gm.GroupId)) {
groupToMembers.put(gm.GroupId, new List<Id>());
}
groupToMembers.get(gm.GroupId).add(gm.UserOrGroupId);
allMemberIds.add(gm.UserOrGroupId);
}
Map<Id, User> activeUsersMap = new Map<Id, User>([
SELECT Id, Name, UserRoleId
FROM User
WHERE IsActive = true
]);
Set<Id> activeUserIds = activeUsersMap.keySet();
Set<Id> roleIds = new Set<Id>();
if (!allMemberIds.isEmpty()) {
for (UserRole r : [SELECT Id FROM UserRole WHERE Id IN :allMemberIds]) {
roleIds.add(r.Id);
}
}
Map<Id, List<Id>> roleToUsers = new Map<Id, List<Id>>();
if (!roleIds.isEmpty()) {
for (User u : activeUsersMap.values()) {
if (u.UserRoleId != null && roleIds.contains(u.UserRoleId)) {
if (!roleToUsers.containsKey(u.UserRoleId)) {
roleToUsers.put(u.UserRoleId, new List<Id>());
}
roleToUsers.get(u.UserRoleId).add(u.Id);
}
}
}
List<String> output = new List<String>();
output.add('QUEUE;USER');
for (Group q : queues) {
List<Id> topMembers = groupToMembers.get(q.Id);
if (topMembers == null) continue;
Set<Id> visited = new Set<Id>();
for (Id memberId : topMembers) {
if (activeUserIds.contains(memberId)) {
User u = activeUsersMap.get(memberId);
output.add(csvEsc(q.Name) + ';' + csvEsc(u != null ? u.Name : String.valueOf(memberId)));
continue;
}
if (roleIds.contains(memberId)) {
List<Id> usersInRole = roleToUsers.get(memberId);
if (usersInRole != null) {
for (Id uid : usersInRole) {
User u = activeUsersMap.get(uid);
output.add(csvEsc(q.Name) + ';' + csvEsc(u != null ? u.Name : String.valueOf(uid)));
}
}
continue;
}
if (groupToMembers.containsKey(memberId)) {
traverseGroup(memberId, visited, groupToMembers, activeUserIds, activeUsersMap, roleIds, roleToUsers, q.Name, output);
}
}
}
String csv = String.join(output, '\n');
System.debug('=== CSV QUEUE WITH USERS ===\n' + csv);
System.debug('TOTAL QUEUE WITH USERS: ' + (output.size() - 1));
}
}
Run it with: new QueueUsersReport().run();

Script 2 — Queues Without Users
The second script, QueueWithoutUsersReport, lists only queues that have no active users, whether directly or indirectly assigned. This helps you find potential SLA risks early.
public class QueueWithoutUsersReport {
private String csvEsc(String s) {
if (s == null) return '';
String v = s.replace('"', '""');
if (v.contains(';') || v.contains('\n') || v.contains('\r') || v.contains('"'))
v = '"' + v + '"';
return v;
}
private void traverseGroup(
Id groupId,
Set<Id> visited,
Map<Id, List<Id>> groupToMembers,
Set<Id> activeUserIds,
Set<Id> roleIds,
Map<Id, List<Id>> roleToUsers,
Set<Id> foundUsers
) {
if (groupId == null || visited.contains(groupId)) return;
visited.add(groupId);
List<Id> members = groupToMembers.get(groupId);
if (members == null) return;
for (Id memberId : members) {
if (activeUserIds.contains(memberId)) {
foundUsers.add(memberId);
continue;
}
if (roleIds.contains(memberId)) {
List<Id> usersInRole = roleToUsers.get(memberId);
if (usersInRole != null) foundUsers.addAll(usersInRole);
continue;
}
if (groupToMembers.containsKey(memberId)) {
traverseGroup(memberId, visited, groupToMembers, activeUserIds, roleIds, roleToUsers, foundUsers);
}
}
}
public void run() {
List<Group> queues = [SELECT Id, Name FROM Group WHERE Type = 'Queue'];
List<GroupMember> allGroupMembers = [SELECT GroupId, UserOrGroupId FROM GroupMember];
Map<Id, List<Id>> groupToMembers = new Map<Id, List<Id>>();
Set<Id> allMemberIds = new Set<Id>();
for (GroupMember gm : allGroupMembers) {
if (!groupToMembers.containsKey(gm.GroupId))
groupToMembers.put(gm.GroupId, new List<Id>());
groupToMembers.get(gm.GroupId).add(gm.UserOrGroupId);
allMemberIds.add(gm.UserOrGroupId);
}
Map<Id, User> activeUsersMap = new Map<Id, User>([
SELECT Id FROM User WHERE IsActive = true
]);
Set<Id> activeUserIds = activeUsersMap.keySet();
Set<Id> roleIds = new Set<Id>();
if (!allMemberIds.isEmpty()) {
for (UserRole r : [SELECT Id FROM UserRole WHERE Id IN :allMemberIds]) {
roleIds.add(r.Id);
}
}
Map<Id, List<Id>> roleToUsers = new Map<Id, List<Id>>();
if (!roleIds.isEmpty()) {
for (User u : [SELECT Id, UserRoleId FROM User WHERE IsActive = true AND UserRoleId IN :roleIds]) {
if (!roleToUsers.containsKey(u.UserRoleId))
roleToUsers.put(u.UserRoleId, new List<Id>());
roleToUsers.get(u.UserRoleId).add(u.Id);
}
}
List<String> output = new List<String>();
output.add('QUEUE;USER');
for (Group q : queues) {
Set<Id> foundUsers = new Set<Id>();
Set<Id> visited = new Set<Id>();
List<Id> topMembers = groupToMembers.get(q.Id);
if (topMembers == null) continue;
for (Id memberId : topMembers) {
if (activeUserIds.contains(memberId)) {
foundUsers.add(memberId);
continue;
}
if (roleIds.contains(memberId)) {
List<Id> usersInRole = roleToUsers.get(memberId);
if (usersInRole != null) foundUsers.addAll(usersInRole);
continue;
}
if (groupToMembers.containsKey(memberId)) {
traverseGroup(memberId, visited, groupToMembers, activeUserIds, roleIds, roleToUsers, foundUsers);
}
}
if (foundUsers.isEmpty()) {
output.add(csvEsc(q.Name) + ';' + 'Not User');
}
}
String csv = String.join(output, '\n');
System.debug('=== CSV QUEUE WITHOUT USERS ===\n' + csv);
System.debug('TOTAL QUEUE WITHOUT USERS: ' + (output.size() - 1));
}
}
Run it with: new QueueWithoutUsersReport().run();
How to Use and Automate
- Open Developer Console → Execute Anonymous.
- Paste one class at a time and execute
new QueueUsersReport().run();ornew QueueWithoutUsersReport().run();. - Copy the CSV from
Debug Logsand paste it into Excel/Sheets. - Optionally, move logic into a Scheduled Apex class for weekly queue audits.
- Send reports to admins and clean up empty queues.
Conclusion
These two Apex scripts give you full visibility of queue health in your Salesforce org. With QueueUsersReport you can audit who’s assigned, and with QueueWithoutUsersReport you can identify risky empty queues before they cause issues. This audit improves SLA compliance, routing accuracy, and governance across Service Cloud teams.
🔗 Related reads:
- How to Resolve Cases Created Outside Configured Settings in Email-to-Case
- Salesforce Help: Manage Queues and Queue Members
- Trailhead: Service Cloud Basics
Have you ever found queues with no active users in your org? Leave a comment below and share your solution — it might help other admins too!

Leave a Reply