Portal Community

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

ScenarioRecommendation
Publish before returning SuccessStandard — event reflects the node outcome
Publish conditionally based on outputFine — check result value before PublishAsync
Publish inside error handlingConsider an INodeEventSubscriber OnError instead — cleaner separation
Publish multiple events from one nodeAllowed — 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).