Portal Community

When It Fires

The engine follows this sequence:

1

Router selects node

The execution router determines which node fires next based on completed edges.

2

GuardRail check

All registered GuardRail engines run. If any blocks, the node fails without reaching the executor or subscribers.

3

OnBeforeExecuteAsync fires

All subscribers' OnBefore methods are called in registration order.

4

ExecuteInternalAsync

The actual executor runs.

What Is Available

DataAvailable?Notes
NodeId, NodeType, ExecutionIdYesAlways set
Context.ExecutionMemoryYes (snapshot)Upstream outputs are readable
Context.NodeConfigYes (snapshot)Configured settings for this node
ResultNoExecutor hasn't run yet
DurationMsNoNo timing yet

Audit Start Record Pattern

public async Task OnBeforeExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    // Record that execution started — pair this with OnAfter to compute duration
    await _auditRepo.InsertAsync(new NodeExecutionAudit
    {
        Id          = Guid.NewGuid(),
        ExecutionId = args.ExecutionId,
        NodeId      = args.NodeId,
        NodeType    = args.NodeType,
        TenantId    = args.TenantId,
        StartedAt   = args.StartedAt,
        Status      = AuditStatus.InProgress
    }, ct);
}

Timing Pattern — Store Start for Duration Calculation

Since DurationMs is not set in OnBefore, store your own start timestamp if you need precise per-subscriber timing (the engine's DurationMs in OnAfter covers total executor time):

// In a field or concurrent dictionary keyed by executionId+nodeId
private readonly ConcurrentDictionary<string, long> _startTimes = new();

public Task OnBeforeExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    var key = $"{args.ExecutionId}:{args.NodeId}";
    _startTimes[key] = Stopwatch.GetTimestamp();
    return Task.CompletedTask;
}

public async Task OnAfterExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    var key = $"{args.ExecutionId}:{args.NodeId}";
    if (_startTimes.TryRemove(key, out var start))
    {
        var elapsed = Stopwatch.GetElapsedTime(start);
        await _metrics.RecordDurationAsync("node.execution.duration", elapsed, args.NodeType);
    }
}
Throwing in OnBefore: If your OnBefore method throws, the engine treats it as a node failure. The exception propagates as if the executor itself had thrown. This is the only subscriber method with this behaviour — use it with extreme care.

What Not to Do

Anti-patternWhy It's WrongAlternative
Mutating args.ContextIt's a snapshot — mutations are silently ignoredUse GuardRail engines if you need to enrich context
Long-running network callsAdds latency to every node executionFire-and-forget to a background queue
Throwing to enforce business rulesCouples enforcement into subscribers; hard to testImplement a GuardRail engine for policy enforcement