Flow Studio
Publishing from a Node
Inject IWorkflowEventBus into your executor and call PublishAsync inside ExecuteInternalAsync. This is how domain events flow outward from workflow execution to the rest of your system.
Constructor Injection Pattern
public class InvoiceProcessingExecutor
: BaseNodeExecutor<InvoiceInput, InvoiceOutput>
{
private readonly IWorkflowEventBus _eventBus;
private readonly IInvoiceService _invoices;
public InvoiceProcessingExecutor(
IWorkflowEventBus eventBus,
IInvoiceService invoices)
{
_eventBus = eventBus;
_invoices = invoices;
}
protected override async Task<NodeExecutionResult> ExecuteInternalAsync(
NodeExecutionContext ctx, CancellationToken ct)
{
var result = await _invoices.ProcessAsync(ctx.Input.InvoiceId, ct);
// Publish domain event — all subscribed handlers will receive this
await _eventBus.PublishAsync(new InvoiceProcessedEvent
{
ExecutionId = ctx.ExecutionId,
ProcessId = ctx.ProcessId,
TenantId = ctx.TenantId,
InvoiceId = ctx.Input.InvoiceId,
Amount = result.TotalAmount,
Currency = result.Currency,
ProcessedAt = DateTimeOffset.UtcNow
}, ct);
return NodeExecutionResult.Success(new InvoiceOutput
{
InvoiceId = ctx.Input.InvoiceId,
Status = result.Status
});
}
}
Timing: When to Publish
| Scenario | Recommendation |
|---|---|
| Publish before returning Success | Standard — event reflects the node outcome |
| Publish conditionally based on output | Fine — check result value before PublishAsync |
| Publish inside error handling | Consider an INodeEventSubscriber OnError instead — cleaner separation |
| Publish multiple events from one node | Allowed — call PublishAsync multiple times |
Publishing Multiple Events
// Two domain events from one executor — both fully supported
await _eventBus.PublishAsync(new CustomerUpdatedEvent { ... }, ct);
await _eventBus.PublishAsync(new AuditTrailEntryEvent { ... }, ct);
Context Fields — Always Populate Them
Always copy ExecutionId, ProcessId, and TenantId from ctx into your event. These are required for correlating events back to executions and for tenant isolation in handlers.
Do not hardcode tenant IDs: Always use
ctx.TenantId. If you hardcode or omit TenantId, handlers that write to multi-tenant stores will corrupt or leak data.
PublishAsync Is Awaited
Despite the name "fire-and-forget" in common documentation, PublishAsync does await all handlers before returning. The "fire-and-forget" refers to the fact that handler results do not affect node output — but execution is still synchronous. If you want true background delivery, your handler must do the fire-and-forget internally (e.g., enqueue to a background channel).