Cascade Promote Pattern
Overview
The Cascade Promote pattern automatically promotes one or more related instances to a new lifecycle state when a state transition occurs on the parent or triggering instance. It is implemented in PromoteActionCommand or PostCreate programs by calling InstanceService.PromoteInstanceAsync.
When to Use
- When promoting a parent should propagate a state change to all children (e.g., cancelling a work order should cancel all its pending tasks).
- When creating an instance should immediately advance it to the next state (e.g., auto-promote a new record to its initial working state).
- When a workflow step logically triggers the start of related work items.
Applicable Program Types
| Program Type | Interface | Notes |
|---|---|---|
| PromoteActionCommand | IPromoteActionCommandProgramAsync | Promote related instances as part of the action |
| PostCreate Trigger | IPostCreateTriggerProgramAsync | Auto-promote newly created instance |
| Scheduler | ISchedulerProgramAsync | Promote instances that meet a time-based condition |
Pattern Structure
- Fetch the related instances to be promoted using
GetRelatedInstancesAsync(with optionalStateFilterto target only instances in the correct state). - Loop through each related instance.
- Optionally update an attribute on the instance before promoting (e.g., set a flag).
- Call
InstanceService.PromoteInstanceAsync(instanceId, actionName, comments).
Code Example — Cascade Promote on Action (PromoteActionCommand)
When a WorkOrder is cancelled, all related tasks in "Planned" state are also cancelled.
using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System;
using System.Threading.Tasks;
using Attribute = Prorigo.Protrak.API.Contracts.Attribute;
namespace YourNamespace.Programs
{
/// <summary>
/// Type: Promote Action Command
/// Configured for: Type WorkOrder
/// Trigger: On "Cancel" action
/// Summary: Cancels all related tasks in "Planned" state when the work order is cancelled.
/// </summary>
public class CancelAllPendingTasksOnCancel : IPromoteActionCommandProgramAsync
{
public IInstanceService InstanceService { get; set; }
public ILoggingService LoggingService { get; set; }
private readonly string RELATION_TASKS_TO_WORKORDER = "TaskToWorkOrder";
private readonly string TYPE_TASK = "Task";
private readonly string ATTRIBUTE_IS_CANCELLED = "IsCancelled";
private readonly string STATE_PLANNED = "Planned";
private readonly string ACTION_CANCEL = "Cancel";
public async Task<ProgramResult> RunAsync(Instance instance, string fromState, string toState, string actionName)
{
try
{
// 1. Find all related tasks in "Planned" state
var related = await InstanceService.GetRelatedInstancesAsync(instance.Id,
new RelatedInstanceQuery
{
RelationFilters = new[]
{
new RelationFilter
{
InstanceId = instance.Id,
RelationTypeName = RELATION_TASKS_TO_WORKORDER,
TypeName = TYPE_TASK,
RelationDirection = RelationDirection.From
}
},
StateFilter = new[] { STATE_PLANNED },
Skip = 0,
Take = int.MaxValue
});
if (related?.Items == null || related.TotalCount == 0)
return new ProgramResult { IsSuccess = true };
// 2. Flag and cancel each related task
foreach (var item in related.Items)
{
var update = new Instance { Id = item.RelatedInstanceId, InstanceTypeName = TYPE_TASK };
update.SetBooleanAttributeValue(ATTRIBUTE_IS_CANCELLED, true);
await InstanceService.UpdateInstanceAsync(update, null);
await InstanceService.PromoteInstanceAsync(item.RelatedInstanceId, ACTION_CANCEL, string.Empty);
}
return new ProgramResult { IsSuccess = true };
}
catch (Exception ex)
{
LoggingService.Error(ex.Message);
throw;
}
}
}
}
Code Example — Auto-Promote on Create (PostCreate)
After an instance is created, it is automatically promoted to its initial working state.
/// <summary>
/// Type: Post Create Trigger
/// Configured for: Type WorkOrder
/// Summary: Auto-promotes the newly created work order to the "Active" state.
/// </summary>
public class AutoPromoteToActiveOnCreate : IPostCreateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string STATE_ACTIVE = "Active";
public async Task RunAsync(Guid instanceId)
{
await InstanceService.PromoteInstanceAsync(
instanceId, STATE_ACTIVE, "System auto-promoted to Active state.");
}
}
Code Example — Scheduler Cascade Promote
A daily scheduler promotes contracts to "On Hold" if they have been in review states for more than one year.
using Prorigo.Protrak.API.Contracts.Builders;
/// <summary>
/// Type: Scheduler
/// Trigger: Daily CRON
/// Summary: Promotes contracts to "On Hold" if they have been in review for over a year.
/// </summary>
public class AutoHoldStaleContractsScheduler : ISchedulerProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string TYPE_CONTRACT = "Contract";
private readonly string ATTR_CREATED = "Created";
private readonly string ACTION_ON_HOLD = "On Hold";
private readonly string[] STATES = { "Draft", "Legal Review", "Partner Review" };
public async Task RunAsync()
{
var agreements = await InstanceService.GetInstancesAsync(
InstanceQueryBuilder.ForType(TYPE_CONTRACT)
.InStates(STATES)
.Select(ATTR_CREATED)
.TakeAll()
.Build());
if (agreements?.Items == null) return;
foreach (var contract in agreements.Items)
{
var created = contract.GetDateAttributeValue(ATTR_CREATED);
if (created != null && created.Value.Date.AddDays(364) <= DateTime.UtcNow.Date)
{
await InstanceService.PromoteInstanceAsync(
contract.Id, ACTION_ON_HOLD,
"System auto-promoted to On Hold after one year of inactivity.");
}
}
}
}
Code Example — Aggregate All Approvers (Scheduler)
Promotes an Application when all linked Reviewer instances have completed their review. Exceptions are swallowed per-instance so one failure does not block others.
using Prorigo.Protrak.API.Contracts.Builders;
/// <summary>
/// Type: Scheduler
/// Trigger: Periodic
/// Summary: For each Application in "Under Review", promotes it to the next state
/// only when ALL linked Reviewers have completed their review.
/// </summary>
public class PromoteApplicationWhenAllReviewsComplete : ISchedulerProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string TYPE_APPLICATION = "Application";
private readonly string TYPE_REVIEWER = "Reviewer";
private readonly string REL_REVIEWER_TO_APP = "ReviewerToApplication";
private readonly string STATE_UNDER_REVIEW = "Under Review";
private readonly string STATE_ACCEPTED = "Review Accepted";
private readonly string ACTION_SUBMIT = "Submit for Decision";
public async Task RunAsync()
{
var applications = await InstanceService.GetInstancesAsync(
InstanceQueryBuilder.ForType(TYPE_APPLICATION)
.InStates(STATE_UNDER_REVIEW)
.TakeAll()
.Build());
if (applications?.Items == null) return;
foreach (var application in applications.Items)
{
try
{
var reviewers = await InstanceService.GetRelatedInstancesAsync(application.Id,
new RelatedInstanceQuery
{
RelationFilters = new[]
{
new RelationFilter
{
RelationTypeName = REL_REVIEWER_TO_APP,
TypeName = TYPE_REVIEWER,
RelationDirection = RelationDirection.From
}
}
});
if (reviewers?.Items == null) continue;
// Only promote when NO reviewer is still pending
bool anyPending = reviewers.Items.Any(
r => r.State.Name == STATE_UNDER_REVIEW || r.State.Name == STATE_ACCEPTED);
if (!anyPending)
await InstanceService.PromoteInstanceAsync(application.Id, ACTION_SUBMIT, "");
}
catch
{
// Continue processing remaining applications even if one fails
}
}
}
}
Key Services
| Service | Method | Purpose |
|---|---|---|
IInstanceService | PromoteInstanceAsync(instanceId, actionName, comments) | Execute a lifecycle promote action on an instance |
IInstanceService | GetRelatedInstancesAsync | Retrieve related instances to cascade the promote to |
IInstanceService | UpdateInstanceAsync | Optionally update attributes before promoting |
Common Gotchas
actionNamemust exactly match the action name as configured in the schema — it is case-sensitive.- Use
StateFilterinRelatedInstanceQueryto restrict cascade promotes only to instances in the expected state, preventing unintended transitions. - In a
PromoteActionCommand, you can both promote the current instance (it happens automatically) and cascade to related instances. Throwing an exception or returningIsSuccess = falsewill block the parent's transition too. - In a scheduler that checks many instances, wrap each
PromoteInstanceAsynccall in a try/catch so a failure on one instance doesn't stop the entire batch.