Skip to main content

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: ActivityQuery is not yet supported by InstanceQueryBuilder. Use the verbose new 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

ServiceMethodPurpose
IUserServiceGetUsers(search, roles, activeOnly, withRoles, skip, take)All platform users matching a role
IUserServiceGetUsersLight(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
IUserServiceGetUserByEmail(email)UserLook up a user by email address
IUserServiceGetUser()Get the full user details of current logged-in user
IUserServiceGetUser(instanceId)Get full user details by UserProfile instance ID
IRoleServiceGetRole(roleName) → Role with Users[]Role definition with its members
IInstanceServiceGetInstanceUsersAsync(instanceId, ...)Users with instance-level access (filter by role for targeted lookup)