Dynamic State Selection Pattern (PromoteAction)
Overview
IPromoteActionProgramAsync is a distinct interface from IPromoteActionCommandProgramAsync. Instead of executing side effects and returning a ProgramResult, a PromoteAction program returns the name of the target state to route to. This enables branching workflows where the destination state is determined at runtime based on instance data rather than being fixed in the schema.
When to Use
- The same promote action button must route the instance to different states depending on a picklist selection, a count, or any business condition.
- Multi-path approval: "Approve" goes to "Approved", "Reject" goes to "Rejected", determined by an attribute.
- All-approvers-done check: promote a parent when every related reviewer has completed their task.
PromoteAction vs PromoteActionCommand
IPromoteActionProgramAsync | IPromoteActionCommandProgramAsync | |
|---|---|---|
| Purpose | Choose destination state | Execute side effects |
| Returns | Task<string> (target state name) | Task<ProgramResult> |
| Call signature | RunAsync(Guid instanceId, string actionName) | RunAsync(Instance instance, string fromState, string toState, string actionName) |
| To block | Throw an exception | Return IsSuccess = false |
| Side effects | Avoid (use a Command for those) | Yes — updates, notifications, etc. |
Both types can be attached to the same promote action. The PromoteAction runs first to determine the target state; the PromoteActionCommand runs to execute side effects.
Code Example — Attribute-Driven Branching
Routes to different states depending on a picklist selection made by the applicant. Validates that a required document is attached before allowing one of the paths.
using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System;
using System.Threading.Tasks;
namespace YourNamespace.Programs
{
/// <summary>
/// Type: Promote Action Program (IPromoteActionProgramAsync)
/// Configured for: Type Application, Action "Applicant Response"
/// Summary: Routes to "Withdrawn" if the applicant chooses to withdraw,
/// or to "Under Committee Review" otherwise (requiring a response document).
/// </summary>
public class BranchOnApplicantDecision : IPromoteActionProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string ATTRIBUTE_DECISION = "ApplicantDecision";
private readonly string ATTRIBUTE_RESPONSE_DOC = "ResponseDocument";
private readonly string PICKLIST_WITHDRAW = "Withdraw my application";
private readonly string STATE_WITHDRAWN = "Withdrawn";
private readonly string STATE_COMMITTEE_REVIEW = "Under Committee Review";
public async Task<string> RunAsync(Guid instanceId, string actionName)
{
var instance = await InstanceService.GetInstanceAsync(instanceId,
new[] { ATTRIBUTE_DECISION, ATTRIBUTE_RESPONSE_DOC });
var decision = instance.GetPicklistAttributeValue(ATTRIBUTE_DECISION);
if (decision == null || decision.Length == 0)
throw new Exception("A decision is required before submitting.");
if (decision[0] == PICKLIST_WITHDRAW)
return STATE_WITHDRAWN;
// Proceeding — response document is mandatory
var responseDoc = instance.GetAttachmentAttributeValue(ATTRIBUTE_RESPONSE_DOC);
if (responseDoc == null)
throw new Exception("A response document is required before proceeding.");
return STATE_COMMITTEE_REVIEW;
}
}
}
Code Example — Boolean / Approval Flag Pattern
A generic two-state router based on an "Approve/Reject" picklist attribute:
/// <summary>
/// Type: Promote Action Program
/// Summary: Routes to "Approved" or "Rejected" based on the ApprovalDecision picklist.
/// </summary>
public class ApproveOrRejectPromoteAction : IPromoteActionProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string ATTRIBUTE_DECISION = "ApprovalDecision";
private readonly string STATE_APPROVED = "Approved";
private readonly string STATE_REJECTED = "Rejected";
public async Task<string> RunAsync(Guid instanceId, string actionName)
{
var instance = await InstanceService.GetInstanceAsync(instanceId, new[] { ATTRIBUTE_DECISION });
var decision = instance.GetPicklistAttributeValue(ATTRIBUTE_DECISION)?.FirstOrDefault()
?? throw new Exception("Approval Decision is required.");
return decision switch
{
"Approve" => STATE_APPROVED,
"Reject" => STATE_REJECTED,
_ => throw new Exception($"Unknown decision value: {decision}")
};
}
}
Rules
- Return the exact state name as configured in the Protrak schema (case-sensitive). If the returned name does not match a valid target state for this action, the promotion fails.
- Throw an exception to block the transition — do not return null, empty string, or a fake state name.
- No side effects in PromoteAction programs. Notifications, attribute updates, and other mutations belong in a PromoteActionCommand program attached to the same action.
instanceIdis passed (not the fullInstance) because the program is responsible for fetching only what it needs.
Workflow Configuration
In the schema, a promote action has two optional program slots:
- Promote Action Program → implements
IPromoteActionProgramAsync→ determines target state - Promote Action Command → implements
IPromoteActionCommandProgramAsync→ executes side effects
Both can be attached to the same action and both run synchronously in the promotion transaction.