System Event Pattern
Overview
The System Event pattern is used to dispatch complex or long-running operations asynchronously by creating a special SystemEvent instance. A PostCreate trigger on the SystemEvent type then reads the event's payload and invokes the appropriate common program to execute the actual logic.
This pattern is useful when a synchronous trigger (like a PromoteActionCommand) needs to trigger heavyweight processing without blocking the user-facing operation.
When to Use
- A PromoteActionCommand or PostCreate trigger needs to trigger a complex batch operation (e.g., recalculate all related records) but cannot block the user-facing request.
- Business logic must be triggered indirectly — the "what to do" is encoded in data (the SystemEvent instance) rather than hardcoded in the calling program.
- A router pattern is needed: one PostCreate trigger dispatches different common programs based on a type discriminator attribute.
How It Works
User Action
↓
PromoteActionCommand / PostCreate (fast)
→ Creates a SystemEvent instance with:
- Name = "operation name"
- InstanceType = "CommonProgramClassNameToRun"
- InstanceId = "target instance ID"
- ProgramData = "optional JSON payload"
↓
PostCreate on SystemEvent type (async)
→ Reads InstanceType attribute
→ Calls ProgramService.ExecuteCommonProgram(instanceType, args)
→ If success: promotes SystemEvent to "Completed"
→ If error: populates Error attribute, promotes SystemEvent to "Failed"
Applicable Program Types
| Role | Program Type | Interface |
|---|---|---|
| Dispatcher (creates the event) | PostCreate, PromoteActionCommand | Any |
| Router (handles SystemEvent) | PostCreate on SystemEvent type | IPostCreateTriggerProgramAsync |
| Worker (does the actual work) | Common Program | ICommonProgramAsync |
Code Example — Dispatcher (creates a SystemEvent)
When a log entry is created, dispatches an async status recalculation on the linked project.
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: Post Create Trigger
/// Configured for: Type ActivityLog
/// Trigger: After an activity log entry is created
/// Summary: Creates a SystemEvent to trigger async status recalculation on the linked project.
/// </summary>
public class TriggerStatusRecalculationOnLogCreate : IPostCreateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string TYPE_SYSTEM_EVENT = "SystemEvent";
private readonly string COMMON_RECALCULATE_STATUS = "RecalculateProjectStatus";
private readonly string ATTRIBUTE_PROJECT_LOG_REF = "ProjectToLogRef";
private readonly string ATTRIBUTE_NAME = "Name";
private readonly string ATTRIBUTE_INSTANCE_ID = "InstanceId";
private readonly string ATTRIBUTE_INSTANCE_TYPE = "InstanceType";
public async Task RunAsync(Guid instanceId)
{
var logEntry = await InstanceService.GetInstanceAsync(instanceId, new[] { ATTRIBUTE_PROJECT_LOG_REF });
var projectRef = logEntry.GetReferenceAttributeValue(ATTRIBUTE_PROJECT_LOG_REF);
if (projectRef == null || projectRef.Length == 0) return;
var projectId = projectRef.First().Id;
// Create a SystemEvent to dispatch the work asynchronously
var systemEvent = new Instance
{
InstanceTypeName = TYPE_SYSTEM_EVENT,
Attributes = new[]
{
new Attribute { Name = ATTRIBUTE_NAME, Type = AttributeType.Text, TextValue = "Recalculate Project Status" },
new Attribute { Name = ATTRIBUTE_INSTANCE_ID, Type = AttributeType.Text, TextValue = projectId.ToString() },
new Attribute { Name = ATTRIBUTE_INSTANCE_TYPE, Type = AttributeType.Text, TextValue = COMMON_RECALCULATE_STATUS }
}
};
await InstanceService.CreateInstanceAsync(systemEvent);
}
}
}
Code Example — Router (PostCreate on SystemEvent)
Reads the InstanceType discriminator, routes to the matching common program, and promotes the event to Completed or Failed.
/// <summary>
/// Type: Post Create Trigger
/// Configured for: Type SystemEvent
/// Trigger: After a SystemEvent instance is created
/// Summary: Reads InstanceType, calls the matching common program, and promotes to Completed or Failed.
/// </summary>
public class OperationsOnCreateOfSystemEvent : IPostCreateTriggerProgramAsync
{
public IProgramService ProgramService { get; set; }
public IInstanceService InstanceService { get; set; }
private readonly string ATTRIBUTE_INSTANCE_TYPE = "InstanceType";
private readonly string ATTRIBUTE_INSTANCE_ID = "InstanceId";
private readonly string ATTRIBUTE_PROGRAM_DATA = "ProgramData";
private readonly string ATTRIBUTE_ERROR = "Error";
private readonly string STATE_CREATED = "Created";
private readonly string ACTION_COMPLETED = "MarkAsCompleted";
private readonly string ACTION_FAILED = "MarkAsFailed";
// Map InstanceType values to common program class names
private static string GetProgramName(string instanceType) => instanceType switch
{
"RecalculateProjectStatus" => "RecalculateProjectStatus",
"BatchUpdateStatus" => "BatchUpdateStatus",
_ => null
};
public async Task RunAsync(Guid instanceId)
{
var systemEvent = await InstanceService.GetInstanceAsync(instanceId,
new[] { ATTRIBUTE_INSTANCE_TYPE, ATTRIBUTE_INSTANCE_ID, ATTRIBUTE_PROGRAM_DATA, ATTRIBUTE_ERROR });
if (systemEvent.State.Name != STATE_CREATED) return;
var instanceType = systemEvent.GetTextAttributeValue(ATTRIBUTE_INSTANCE_TYPE);
var targetId = systemEvent.GetTextAttributeValue(ATTRIBUTE_INSTANCE_ID);
var programData = systemEvent.GetTextAttributeValue(ATTRIBUTE_PROGRAM_DATA);
var programName = GetProgramName(instanceType);
if (programName == null) return;
try
{
await ProgramService.ExecuteCommonProgramAsync(programName,
new object[] { targetId, programData });
await InstanceService.PromoteInstanceAsync(instanceId, ACTION_COMPLETED, "Done");
}
catch (Exception ex)
{
systemEvent.SetTextAttributeValue(ATTRIBUTE_ERROR, ex.Message);
await InstanceService.UpdateInstanceAsync(systemEvent, null);
await InstanceService.PromoteInstanceAsync(instanceId, ACTION_FAILED, "Error during execution");
}
}
}
SystemEvent Schema Convention
The SystemEvent type typically has these attributes:
| Attribute | Type | Description |
|---|---|---|
Name | Text | Human-readable label for the event |
InstanceType | Text | Maps to the common program class name to invoke |
InstanceId | Text | The target instance ID (usually a Guid as string) |
ProgramData | Text | Optional JSON payload passed to the common program |
Error | Text | Error message populated if the common program fails |
The lifecycle usually has states: Created → Completed / Failed.
When to Use vs. Direct Async Trigger
| Approach | Use when |
|---|---|
| Direct PostCreate/PostUpdate trigger | Logic is specific to one event; no need to reuse or route |
| System Event pattern | Logic is long-running or must be decoupled; same logic is triggered from multiple places; you need visibility into success/failure state via the SystemEvent instance |