Portal Community

Base Class

namespace BizFirst.Ai.ProcessEngine.EventBus;

/// <summary>
/// Base class for all workflow events — both built-in and custom.
/// Provides correlation, tenant, and event metadata.
/// </summary>
public abstract class WorkflowEvent
{
    // Set by the engine at publish time
    public string          EventId     { get; init; } = Guid.NewGuid().ToString();
    public string          EventType   { get; init; } = string.Empty; // auto-set by dispatcher
    public DateTimeOffset  OccurredAt  { get; init; } = DateTimeOffset.UtcNow;

    // Correlation context — always required
    public string ExecutionId { get; init; }
    public string ProcessId   { get; init; }
    public string TenantId    { get; init; }
}

Base Fields Reference

FieldTypeDescription
EventIdstring (GUID)Unique identifier for this event instance — use for deduplication in idempotent handlers
EventTypestringAuto-set by dispatcher to the class name — e.g., "WorkflowCompleted"
OccurredAtDateTimeOffsetUTC timestamp when PublishAsync was called
ExecutionIdstringThe execution instance this event relates to
ProcessIdstringThe workflow definition (process) this execution is running
TenantIdstringTenant that owns the workflow — always include in handler DB writes for isolation

Idempotent Handler Pattern

Use EventId to prevent double-processing in handlers that write to external systems:

public async Task HandleAsync(WorkflowCompleted @event, CancellationToken ct)
{
    // Skip if already processed (e.g., after a retry)
    if (await _processedEvents.ExistsAsync(@event.EventId, ct))
        return;

    await _billingService.ChargeExecutionAsync(@event.TenantId, @event.ExecutionId, ct);
    await _processedEvents.MarkAsync(@event.EventId, ct);
}

Serialisation for External Delivery

When pushing events to an external message broker from a handler, serialise the full event object to preserve all base fields:

public async Task HandleAsync(WorkflowCompleted @event, CancellationToken ct)
{
    var message = new ServiceBusMessage(JsonSerializer.Serialize(@event))
    {
        MessageId      = @event.EventId,       // dedup key
        CorrelationId  = @event.ExecutionId,
        Subject        = @event.EventType
    };

    await _sender.SendMessageAsync(message, ct);
}