Flow Studio
BaseNodeExecutor Pattern
BaseNodeExecutor is an abstract partial class that implements IProcessElementExecution using the template method pattern. You extend it and override only ExecuteInternalAsync — the base class orchestrates all other pipeline stages.
The Template Method
// BaseNodeExecutor.cs (simplified)
public abstract partial class BaseNodeExecutor : IProcessElementExecution
{
// Called by the engine — runs the full pipeline
public async Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext nodeExecutionContext,
CancellationToken cancellationToken = default)
{
// Validates context, runs all pipeline stages
return await Execute(nodeExecutionContext, cancellationToken);
}
// THE METHOD YOU IMPLEMENT
protected abstract Task<NodeExecutionResult> ExecuteInternalAsync(
NodeExecutionContext nodeExecutionContext,
CancellationToken cancellationToken);
}
Minimal Complete Executor
// MyCustomNodeExecutor.cs
using BizFirst.Ai.ProcessExecutor.Service; // BaseNodeExecutor
using BizFirst.Ai.ProcessEngine.Domain.Execution;
using BizFirst.Ai.ProcessEngine.Domain.Execution.Context;
public partial class MyCustomNodeExecutor : BaseNodeExecutor, IActionNodeExecution
{
public const string NodeTypeName = "my-custom-node";
public override string ProcessElementTypeCode => NodeTypeName;
private readonly IMyExternalService _myService;
public MyCustomNodeExecutor(
IMyExternalService myService,
ILogger<MyCustomNodeExecutor> logger,
INodeConfigurationParser? configParser = null)
: base(logger, configParser)
{
_myService = myService;
}
protected override async Task<NodeExecutionResult> ExecuteInternalAsync(
NodeExecutionContext ctx,
CancellationToken cancellationToken)
{
// 1. Get settings (populated by BaseNodeExecutor before this is called)
var s = (MyCustomNodeSettings)settings!;
// 2. Do the work
var result = await _myService.DoWorkAsync(s.InputValue, cancellationToken);
// 3. Return success result with output data
return new NodeExecutionResult
{
IsSuccess = true,
OutputPortKey = "main",
OutputData = new Dictionary<string, object>
{
{ "result", result },
{ "status", "ok" }
}
};
}
}
// MyCustomNodeExecutor.Config.cs
public partial class MyCustomNodeExecutor
{
private MyCustomNodeSettings? mySettings => settings as MyCustomNodeSettings;
public override BaseNodeExecutorSettings CreateExecutorSettings()
=> new MyCustomNodeSettings();
}
What BaseNodeExecutor Provides
| Capability | Partial Class File |
|---|---|
| Configuration loading and validation | BaseNodeExecutor.Config.cs |
| Pre/Post GuardRails execution | BaseNodeExecutor.PreGuardRails.cs / .PostGuardRails.cs |
| Error routing (error output port) | BaseNodeExecutor.ErrorHandler.cs |
| HIL suspension/resume | BaseNodeExecutor.Suspension.cs |
| Webhook capability | BaseNodeExecutor.Webhooks.cs |
| Form capability | BaseNodeExecutor.Forms.cs |
| SignalR event emission | BaseNodeExecutor.Event.cs |
| Performance metrics and tracing | BaseNodeExecutor.Activity.cs / .ProgressReport.cs |
| Item iteration support | BaseNodeExecutor.Iteration.cs |
Constructor Base Call
The base constructor requires a logger and optionally a config parser and form resolver. Always pass through all parameters received from DI:
public MyCustomNodeExecutor(
IMyService service,
ILogger<MyCustomNodeExecutor> logger,
INodeConfigurationParser? configParser = null,
NodeFormResolver? formResolver = null)
: base(logger, configParser, formResolver)
{
_service = service;
}
settings Is Populated Before ExecuteInternalAsync
The
settings field on BaseNodeExecutor is populated during the EntryValidate pipeline stage — before ExecuteInternalAsync is called. You can cast and use it directly in ExecuteInternalAsync without calling LoadConfigAsync yourself.