Flow Studio
Standard Workflow Nodes
The default node pattern — stateless, short-lived, resolved fresh for every node execution by the DI container. The right choice for the vast majority of workflow steps.
Execution Model
When the workflow engine reaches a standard node, it resolves the executor from the DI container using a scoped lifetime, calls ExecuteAsync, waits for it to complete, stores the result in the execution context, then disposes the executor. There is no persistent state between invocations.
WorkflowEngine (per-node step)
│
├── Resolve IProcessElementExecution from DI (scoped)
│
├── await executor.ExecuteAsync(ctx, ct)
│ └── performs operation, returns NodeExecutionResult
│
├── Store result in NodeExecutionContext.OutputStore
│
└── Dispose executor (end of DI scope)
The Executor Interface
public interface IProcessElementExecution
{
Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext ctx,
CancellationToken ct);
}
// BaseNodeExecutor provides settings deserialization:
public abstract class BaseNodeExecutor<TSettings> : IProcessElementExecution
where TSettings : class, new()
{
public async Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext ctx,
CancellationToken ct)
{
var settings = ctx.Node.DeserializeSettings<TSettings>();
return await ExecuteAsync(ctx, settings, ct);
}
protected abstract Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext ctx,
TSettings settings,
CancellationToken ct);
}
Characteristics of Standard Nodes
| Characteristic | Detail |
|---|---|
| DI lifetime | Scoped — new instance per workflow node execution |
| State | None carried between executions — all state comes from NodeExecutionContext |
| Concurrency | Inherently safe — each execution is isolated |
| Startup cost | Near-zero — DI resolution plus constructor injection only |
| Testing | Simple unit testing — construct executor, call ExecuteAsync, assert result |
| Failure isolation | A crashed executor affects only that execution — the host process continues |
Example: A Typical Standard Node Executor
public class HttpCallExecutor : BaseNodeExecutor<HttpCallSettings>
{
private readonly IHttpClientFactory _http;
public HttpCallExecutor(IHttpClientFactory http) { _http = http; }
protected override async Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext ctx,
HttpCallSettings settings,
CancellationToken ct)
{
var client = _http.CreateClient();
var response = await client.GetAsync(settings.Url, ct);
var body = await response.Content.ReadAsStringAsync(ct);
return NodeExecutionResult.Success(new
{
statusCode = (int)response.StatusCode,
body = body,
headers = response.Headers.ToDictionary(h => h.Key, h => h.Value.First())
});
}
}
HttpClientFactory is scoped-safe:
IHttpClientFactory is singleton and manages connection pooling internally. Injecting it into a scoped executor is correct — you get a fresh HttpClient instance each time, but the underlying connections are pooled at the factory level.