Flow Studio
Defining Custom Events
Extend WorkflowEvent with domain-specific payload fields. Publish from any node executor. Subscribe with IWorkflowEventHandler<YourEvent> — the type system handles dispatch automatically.
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
| Guideline | Detail |
|---|---|
| Name as past-tense domain fact | OrderApproved, InvoiceProcessed — not OrderApprovalEvent |
| Include all correlation fields | Always copy ExecutionId, ProcessId, TenantId from ctx |
| Keep payload flat | Avoid deep object graphs — flat fields are easier to serialise and log |
| One class per event type | Don't reuse event classes with a Type discriminator field |
| Add timestamp fields | Include domain-specific timestamps (ApprovedAt, not just OccurredAt) |