Portal Community

Step 1: Define the Event Class

// Place in your domain library or ExecutionNodes project
public class OrderApprovedEvent : WorkflowEvent
{
    public string OrderId        { get; init; }
    public string ApprovedBy     { get; init; }
    public decimal OrderAmount   { get; init; }
    public string Currency       { get; init; }
    public DateTimeOffset ApprovedAt { get; init; }
}

public class OrderRejectedEvent : WorkflowEvent
{
    public string OrderId        { get; init; }
    public string RejectedBy     { get; init; }
    public string RejectionReason{ get; init; }
    public DateTimeOffset RejectedAt { get; init; }
}

Step 2: Publish from an Executor

protected override async Task<NodeExecutionResult> ExecuteInternalAsync(
    NodeExecutionContext ctx, CancellationToken ct)
{
    var input = ctx.GetInput<OrderApprovalInput>();

    if (input.Decision == ApprovalDecision.Approved)
    {
        await _eventBus.PublishAsync(new OrderApprovedEvent
        {
            ExecutionId  = ctx.ExecutionId,
            ProcessId    = ctx.ProcessId,
            TenantId     = ctx.TenantId,
            OrderId      = input.OrderId,
            ApprovedBy   = input.ActorId,
            OrderAmount  = input.OrderAmount,
            Currency     = input.Currency,
            ApprovedAt   = DateTimeOffset.UtcNow
        }, ct);
    }
    else
    {
        await _eventBus.PublishAsync(new OrderRejectedEvent
        {
            ExecutionId      = ctx.ExecutionId,
            ProcessId        = ctx.ProcessId,
            TenantId         = ctx.TenantId,
            OrderId          = input.OrderId,
            RejectedBy       = input.ActorId,
            RejectionReason  = input.Comment,
            RejectedAt       = DateTimeOffset.UtcNow
        }, ct);
    }

    return NodeExecutionResult.Success(new ApprovalOutput { Decision = input.Decision });
}

Step 3: Subscribe with a Handler

public class OrderNotificationHandler : IWorkflowEventHandler<OrderApprovedEvent>
{
    private readonly IEmailService _email;

    public OrderNotificationHandler(IEmailService email) => _email = email;

    public async Task HandleAsync(OrderApprovedEvent @event, CancellationToken ct)
    {
        await _email.SendAsync(new Email
        {
            To      = $"buyer@example.com",  // resolve from orderId
            Subject = $"Order {@event.OrderId} Approved",
            Body    = $"Your order for {@event.Currency} {@event.OrderAmount} has been approved."
        }, ct);
    }
}

// Register in Program.cs
services.AddTransient<
    IWorkflowEventHandler<OrderApprovedEvent>,
    OrderNotificationHandler>();

Custom Event Design Guidelines

GuidelineDetail
Name as past-tense domain factOrderApproved, InvoiceProcessed — not OrderApprovalEvent
Include all correlation fieldsAlways copy ExecutionId, ProcessId, TenantId from ctx
Keep payload flatAvoid deep object graphs — flat fields are easier to serialise and log
One class per event typeDon't reuse event classes with a Type discriminator field
Add timestamp fieldsInclude domain-specific timestamps (ApprovedAt, not just OccurredAt)