Portal Community

What Is Available

DataNotes
args.ResultThe full NodeExecutionResult — includes Output, Status, PinnedData
args.Result.OutputThe executor's output object (deserialise to specific type)
args.DurationMsTotal executor time in milliseconds (measured by the engine)
args.NodeTypeTypeCode string for filtering subscribers by node type
args.ContextSnapshot — includes full execution memory at time of completion

Metrics Collection Pattern

public async Task OnAfterExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    // Record execution duration per node type
    await _metrics.RecordAsync(new MetricPoint
    {
        Name   = "node.execution.duration_ms",
        Value  = args.DurationMs ?? 0,
        Labels = new Dictionary<string, string>
        {
            ["node_type"]    = args.NodeType,
            ["process_id"]   = args.ProcessId,
            ["tenant_id"]    = args.TenantId,
            ["result_status"]= args.Result!.Status.ToString()
        }
    }, ct);

    // Increment success counter
    _metrics.IncrementCounter("node.execution.success",
        ("node_type", args.NodeType),
        ("tenant_id", args.TenantId));
}

SLA Tracking Pattern

public async Task OnAfterExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    var sla = await _slaRepo.GetSlaForNodeTypeAsync(args.NodeType, ct);
    if (sla == null) return;

    if (args.DurationMs > sla.MaxDurationMs)
    {
        await _alertService.SendSlaBreachAsync(new SlaBreachAlert
        {
            NodeId       = args.NodeId,
            NodeType     = args.NodeType,
            ExecutionId  = args.ExecutionId,
            ActualMs     = args.DurationMs!.Value,
            SlaMs        = sla.MaxDurationMs,
            BreachPercent = (double)args.DurationMs!.Value / sla.MaxDurationMs * 100
        }, ct);
    }
}

Accessing Output Data

The Result.Output property is typed as object. Cast or deserialise to access specific fields:

public async Task OnAfterExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    if (args.NodeType != "HttpRequest") return;

    // Cast to the known output type for this executor
    if (args.Result!.Output is HttpRequestOutput httpOutput)
    {
        if (httpOutput.StatusCode >= 400)
        {
            await _alertService.SendAsync($"HTTP node returned {httpOutput.StatusCode}", ct);
        }
    }
}

Notification Pattern

public async Task OnAfterExecuteAsync(NodeExecutionEventArgs args, CancellationToken ct)
{
    // Notify integration layer that a specific node completed
    if (args.NodeType == "Approval" && args.Result!.Status == NodeResultStatus.Success)
    {
        await _eventBus.PublishAsync(new ApprovalNodeCompletedEvent
        {
            ExecutionId = args.ExecutionId,
            NodeId      = args.NodeId,
            Decision    = ((ApprovalOutput)args.Result.Output!).Decision
        }, ct);
    }
}
OnAfter only fires on Success: If the executor throws, OnAfterExecuteAsync is NOT called — OnErrorAsync fires instead. The two events are mutually exclusive.