Flow Studio
OnBeforeExecuteAsync
The pre-execution hook — called after the engine resolves the executor and right before ExecuteInternalAsync is called. Ideal for audit start records, rate-limit pre-checks, and execution tracing.
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
| Data | Available? | Notes |
|---|---|---|
| NodeId, NodeType, ExecutionId | Yes | Always set |
| Context.ExecutionMemory | Yes (snapshot) | Upstream outputs are readable |
| Context.NodeConfig | Yes (snapshot) | Configured settings for this node |
| Result | No | Executor hasn't run yet |
| DurationMs | No | No 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-pattern | Why It's Wrong | Alternative |
|---|---|---|
Mutating args.Context | It's a snapshot — mutations are silently ignored | Use GuardRail engines if you need to enrich context |
| Long-running network calls | Adds latency to every node execution | Fire-and-forget to a background queue |
| Throwing to enforce business rules | Couples enforcement into subscribers; hard to test | Implement a GuardRail engine for policy enforcement |