Role-Based User Lookup Pattern
Overview
Programs often need to find users by their Protrak role rather than by a specific User attribute on an instance. There are three complementary approaches: UserService.GetUsers, RoleService.GetRole, and InstanceService.GetInstanceUsersAsync filtered by role. The right choice depends on whether you want all users with a role in the system, users of a role who also have access to a specific instance, or the members of a role from the schema.
When to Use
- Send notifications to all users with a specific role (e.g., notify all "Legal" users when a contract reaches a review state).
- Grant access to an instance for all active users of a role.
- Build dynamic sign-off lists populated with the current members of a role.
- In a scheduler: identify recipients per-state using role-to-state mappings.
Applicable Program Types
All program types can use these lookups, but most common in:
- Scheduler (batch notifications to role-based recipients)
- PostCreate / PostConnect (grant access, notify role members)
- PromoteActionCommand (notify relevant role on state transition)
Approach 1: UserService.GetUsers — All users with a role
Returns all platform users matching a role (and optional search term), regardless of instance access.
// Signature:
// PagedData<User> GetUsers(string searchTerm, string[] roles, bool activeOnly, bool withRoles, int skip, int take)
var users = UserService.GetUsers("", new[] { "SystemAdmin" }, true, true, 0, 100);
var recipients = users.Items.Select(u => new UserValue
{
UserId = u.Id,
UserName = u.UserName,
UserEmail = u.UserEmail
}).ToArray();
User properties available: Id, UserName, UserEmail, IsActive, Roles[], TypeInstanceId (the UserProfile instance ID).
Approach 2: RoleService.GetRole — Role with its member list
Returns the role definition including all users currently assigned to it.
var role = RoleService.GetRole("ReviewTeam");
if (role != null && role.Users != null)
{
foreach (var user in role.Users)
{
// user.UserName, user.UserEmail, user.IsActive
if (user.IsActive)
{
// Look up the UserProfile instance to read custom attributes
var userByEmail = UserService.GetUserByEmail(user.UserEmail);
var profileInstance = await InstanceService.GetInstanceAsync(
userByEmail.TypeInstanceId, new[] { "JobTitle" });
var jobTitle = profileInstance.GetPicklistAttributeValue("JobTitle")?.FirstOrDefault();
}
}
}
Approach 3: InstanceService.GetInstanceUsersAsync — Users on a specific instance, filtered by role
Returns users who have explicit access to a specific instance. Filter by role to target only role-specific users.
// Signature:
// Task<PagedData<InstanceUser>> GetInstanceUsersAsync(Guid instanceId, int skip, int take, string search, bool withRoles)
var instanceUsers = await InstanceService.GetInstanceUsersAsync(contract.Id, 0, int.MaxValue, "", true);
// Filter to users with the target role
string[] targetRoles = { "Legal", "Compliance" };
var matched = instanceUsers.Items
.Where(iu => iu.User.Roles.Any(r => targetRoles.Contains(r)))
.Select(iu => new UserValue
{
UserId = iu.User.Id,
UserName = iu.User.UserName
});
Full Code Example — State-Based Reminder with Role Lookup
Sends reminder notifications every 2 days to the role responsible for each state, with a SystemAdmin role on BCC.
using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace YourNamespace.Programs
{
/// <summary>
/// Type: Scheduler
/// Trigger: Daily
/// Summary: Sends a reminder every 2 days of inactivity to the role responsible for
/// each contract's current state. BCC goes to SystemAdmin.
/// </summary>
public class ReminderForInactiveContractsScheduler : ISchedulerProgramAsync
{
public IInstanceService InstanceService { get; set; }
public INotificationService NotificationService { get; set; }
public IUserService UserService { get; set; }
private readonly string TYPE_CONTRACT = "Contract";
private readonly string APP_URL = "https://yourapp.com";
private readonly string ROLE_SYSTEM_ADMIN = "SystemAdmin";
// Map: state name → roles responsible in that state
private static readonly Dictionary<string, string[]> StateToRoles = new()
{
["Legal Review"] = new[] { "Legal" },
["Compliance Review"] = new[] { "Compliance" },
["Partner Review"] = new[] { "Partner" },
["Final Approval"] = new[] { "Approver" },
};
public async Task RunAsync()
{
// 1. Fetch SystemAdmin users (global BCC list)
var admins = UserService.GetUsers("", new[] { ROLE_SYSTEM_ADMIN }, true, true, 0, 100);
var bccGlobal = admins.Items.Select(u => new UserValue { UserId = u.Id, UserName = u.UserName }).ToList();
// 2. Fetch contracts in active review states, including activity history
// ⚠ Builder gap: ActivityQuery not supported by InstanceQueryBuilder — use verbose form
var query = new InstanceQuery
{
InstanceTypeName = TYPE_CONTRACT,
StateFilter = StateToRoles.Keys.ToArray(),
Attributes = new[] { "ReferenceNumber" },
ActivityQuery = new ActivityQuery
{
Type = ActivityType.ActionPromoted,
Skip = 0,
Take = 100
},
Skip = 0,
Take = int.MaxValue
};
var contracts = await InstanceService.GetInstancesAsync(query);
foreach (var contract in contracts.Items)
{
// 3. Calculate days in current state using activity history
var lastTransition = contract.Activities?
.Where(a => a.ToState?.Name == contract.State.Name)
.OrderByDescending(a => a.StartTime)
.FirstOrDefault();
if (lastTransition == null) continue;
var daysInState = (DateTime.UtcNow - lastTransition.StartTime.Date).Days;
if (daysInState % 2 != 0) continue; // Send only on even-day intervals
// 4. Look up responsible role users for this state
if (!StateToRoles.TryGetValue(contract.State.Name, out var roles)) continue;
var instanceUsers = await InstanceService.GetInstanceUsersAsync(contract.Id, 0, int.MaxValue, "", true);
var recipients = instanceUsers.Items
.Where(iu => iu.User.Roles.Any(r => roles.Contains(r)))
.Select(iu => new UserValue { UserId = iu.User.Id, UserName = iu.User.UserName })
.Distinct()
.ToArray();
if (recipients.Length == 0 || bccGlobal.Count == 0) continue;
// 5. Send reminder
var refNum = contract.GetTextAttributeValue("ReferenceNumber");
NotificationService.SendNotification(new Notification
{
InstanceId = contract.Id,
Subject = $"Reminder: Contract '{refNum}' awaiting action ({daysInState} days)",
Body = $"Contract <a href='{APP_URL}/#view_{contract.Id}'>{refNum}</a> " +
$"has been in '{contract.State.Name}' for {daysInState} days. Please take action.",
Recipients = recipients,
BccRecipients = bccGlobal.ToArray()
});
}
}
}
}
BCC Recipients
Notification.BccRecipients sends a blind carbon copy — recipients cannot see who else was BCC'd. Useful for notifying admin/monitoring roles without cluttering the primary recipient's view.
var notification = new Notification
{
InstanceId = instanceId,
Subject = "...",
Body = "...",
Recipients = primaryRecipients,
CcRecipients = ccRecipients, // visible CC
BccRecipients = bccRecipients // hidden BCC
};
NotificationService.SendNotification(notification);
Activity History in Queries
Include ActivityQuery in InstanceQuery to fetch state transition history alongside instances. Use instance.Activities to calculate time-in-state:
Note:
ActivityQueryis not yet supported byInstanceQueryBuilder. Use the verbosenew InstanceQuery { ... }form when you need to include activity history.
// ⚠ Builder gap: ActivityQuery not supported — use verbose form
var query = new InstanceQuery
{
InstanceTypeName = "Contract",
ActivityQuery = new ActivityQuery
{
Type = ActivityType.ActionPromoted, // only state transitions
Skip = 0,
Take = 100
}
};
var result = await InstanceService.GetInstancesAsync(query);
// Calculate how long since entering current state
var lastEntry = result.Items.First().Activities
.Where(a => a.ToState?.Name == instance.State.Name)
.OrderByDescending(a => a.StartTime)
.FirstOrDefault();
var daysInState = (DateTime.UtcNow - lastEntry.StartTime.Date).Days;
Key Services
| Service | Method | Purpose |
|---|---|---|
IUserService | GetUsers(search, roles, activeOnly, withRoles, skip, take) | All platform users matching a role |
IUserService | GetUsersLight(int skip, int take, string sortBy, string order, string searchString, bool includeInactiveUsers = false, bool includeAdmins = false, string[] roles = null) | All lightweight representation of platform users matching a role |
IUserService | GetUserByEmail(email) → User | Look up a user by email address |
IUserService | GetUser() | Get the full user details of current logged-in user |
IUserService | GetUser(instanceId) | Get full user details by UserProfile instance ID |
IRoleService | GetRole(roleName) → Role with Users[] | Role definition with its members |
IInstanceService | GetInstanceUsersAsync(instanceId, ...) | Users with instance-level access (filter by role for targeted lookup) |