Skip to main content

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 TypeInterfaceNotes
PromoteActionCommandIPromoteActionCommandProgramAsyncPromote related instances as part of the action
PostCreate TriggerIPostCreateTriggerProgramAsyncAuto-promote newly created instance
SchedulerISchedulerProgramAsyncPromote instances that meet a time-based condition

Pattern Structure

  1. Fetch the related instances to be promoted using GetRelatedInstancesAsync (with optional StateFilter to target only instances in the correct state).
  2. Loop through each related instance.
  3. Optionally update an attribute on the instance before promoting (e.g., set a flag).
  4. 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

ServiceMethodPurpose
IInstanceServicePromoteInstanceAsync(instanceId, actionName, comments)Execute a lifecycle promote action on an instance
IInstanceServiceGetRelatedInstancesAsyncRetrieve related instances to cascade the promote to
IInstanceServiceUpdateInstanceAsyncOptionally update attributes before promoting

Common Gotchas

  • actionName must exactly match the action name as configured in the schema — it is case-sensitive.
  • Use StateFilter in RelatedInstanceQuery to 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 returning IsSuccess = false will block the parent's transition too.
  • In a scheduler that checks many instances, wrap each PromoteInstanceAsync call in a try/catch so a failure on one instance doesn't stop the entire batch.