In enterprise software engineering, developers often take quick shortcuts by hardcoding approval transitions directly into domain entity columns (e.g. updating matter.Status = 2 inside controllers). While initially convenient, this pattern quickly breaks down as business operations scale:
- Branching storms and state deadlocks: As management introduces governance rules—such as requiring simultaneous approval from the Managing Partner and Chief Risk Officer for claims $\ge 50$ million, or mandatory conflict waivers for cross-border clients—the business codebase fills with brittle, nested
if/elseconditions. - Audit trail loss and compliance liability: Direct database column overwrites erase auditability regarding who approved which decision, when, and under what conditions, resulting in severe compliance findings during enterprise audits.
BitzOrcas.Modern includes a proprietary lightweight Directed Acyclic Graph (DAG) state machine workflow engine. Decoupling orchestration via the IWorkflowTransitionFlow transition port, the engine atomically maintains dual FlowState status fields alongside an immutable pipeline audit log.
This tutorial guides you through building a production-grade approval workflow for High-Value Civil Litigation (Claims $\ge 50$M) Partner Countersigning.
High-Stakes Approval Workflow DAG Topology
Core Transition Port: IWorkflowTransitionFlow
In BitzOrcas.Modern, workflow execution is decoupled from bloated monolithic services through the specialized IWorkflowTransitionFlow port:
| Core API Method | Operational Semantics | Governance Guarantee |
|---|---|---|
StartAsync(...) | Initiates a workflow instance | Validates definition version, generates InstanceId, and stages initial tasks |
CompleteTaskAsync(...) | Approver completes active task | Validates seat authorization, evaluates countersign quorum, and advances cursor |
RejectTaskAsync(...) | Approver rejects active task | Reverts to initiator or configured node with immutable audit comments |
WithdrawAsync(...) | Initiator withdraws active process | Safely cancels pending tasks if instance has not advanced past recall boundaries |
Step 1: Implement the Workflow Initiation Command (Start Workflow)
When an attorney submits a high-value litigation intake, trigger the workflow via IWorkflowTransitionFlow.StartAsync:
using BitzOrcas.Application.Abstractions.Tenancy;using BitzOrcas.Application.Security;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Domain;using BitzOrcas.Workflow.Abstractions.Services;using Mediator;
namespace BitzOrcas.Modules.Legal.Application.Commands.SubmitMatterForApproval;
public sealed record SubmitMatterForApprovalCommand( string MatterId, decimal ClaimAmount, bool HasPotentialConflict) : ICommand<Result<string>>;
public sealed class SubmitMatterForApprovalCommandHandler( IWorkflowTransitionFlow workflowTransition, ICurrentTenant currentTenant, ICurrentUser currentUser) : ICommandHandler<SubmitMatterForApprovalCommand, Result<string>>{ public async ValueTask<Result<string>> Handle( SubmitMatterForApprovalCommand request, CancellationToken cancellationToken) { var tenantId = currentTenant.Tenant.EffectiveTenantId; var starterId = currentUser.UserId;
// Assemble workflow variables used by DAG gateways to evaluate branching var workflowVariables = new Dictionary<string, object?> { ["MatterId"] = request.MatterId, ["ClaimAmount"] = request.ClaimAmount, ["HasPotentialConflict"] = request.HasPotentialConflict, ["IsMajorMatter"] = request.ClaimAmount >= 50_000_000.00m };
// Delegate to IWorkflowTransitionFlow to start process instance var startResult = await workflowTransition.StartAsync( definitionKey: "WF_LEGAL_MATTER_APPROVAL", businessKey: request.MatterId, businessType: "LegalMatterIntake", starterId: starterId, tenantId: tenantId, officeId: null, variables: workflowVariables, cancellationToken: cancellationToken);
if (startResult.IsFailure) { return Result<string>.Failure(startResult.Error); }
var instance = startResult.GetValueOrThrow(); return Result<string>.Success(instance.Id); }}Step 2: Processing Tasks (Approval and Rejection)
When the Managing Partner reviews the pending matter in the admin portal, invoke task transition APIs:
using BitzOrcas.Application.Security;using BitzOrcas.Domain.Results;using BitzOrcas.Workflow.Abstractions.Services;using Mediator;
namespace BitzOrcas.Modules.Legal.Application.Commands.ApproveMatterTask;
public sealed record ApproveMatterTaskCommand( string TaskId, string ApprovalComment) : ICommand<Result>;
public sealed class ApproveMatterTaskCommandHandler( IWorkflowTransitionFlow workflowTransition, ICurrentUser currentUser) : ICommandHandler<ApproveMatterTaskCommand, Result>{ public async ValueTask<Result> Handle( ApproveMatterTaskCommand request, CancellationToken cancellationToken) { var userId = currentUser.UserId;
// Advance task; the engine validates countersign quorum and transitions downstream upon completion var transitionResult = await workflowTransition.CompleteTaskAsync( taskId: request.TaskId, userId: userId, comment: request.ApprovalComment, variables: null, cancellationToken: cancellationToken);
return transitionResult; }}If a conflict of interest is identified, the risk officer executes a rejection:
// Revert back to lead attorney with structured rationalevar rejectResult = await workflowTransition.RejectTaskAsync( taskId: request.TaskId, userId: currentUser.UserId, comment: "Adversary identified as active retainer corporate client; execute formal conflict waiver.", targetNodeId: null, // null defaults to RollbackRule targeting initiator cancellationToken: cancellationToken);Step 3: End-to-End Workflow Integration Testing
Assert that major matters correctly branch into dual partner countersigning tasks:
using BitzOrcas.Modules.Legal.Application.Commands.SubmitMatterForApproval;using BitzOrcas.Workflow.Abstractions.Engine;using Shouldly;using Xunit;
public sealed class MatterWorkflowIntegrationTests : IClassFixture<CustomWebApplicationFactory>{ private readonly IWorkflowEngine _engine;
public MatterWorkflowIntegrationTests(CustomWebApplicationFactory factory) { _engine = factory.GetRequiredService<IWorkflowEngine>(); }
[Fact] public async Task MajorMatter_Over50Million_ShouldRequirePartnerCountersign() { // 1. Simulate submitting a 60M major litigation matter var transition = _engine.RuntimeTransition; var variables = new Dictionary<string, object?> { ["ClaimAmount"] = 60_000_000.00m, ["HasPotentialConflict"] = false };
var startResult = await transition.StartAsync( definitionKey: "WF_LEGAL_MATTER_APPROVAL", businessKey: "MAT-20260923-9999", businessType: "LegalMatterIntake", starterId: "lawyer_001", tenantId: "1000001", officeId: null, variables: variables, cancellationToken: CancellationToken.None);
startResult.IsSuccess.ShouldBeTrue(); var instanceId = startResult.GetValueOrThrow().Id;
// 2. Machine assertion: gateway correctly activates countersign tasks var tasksResult = await _engine.Tasks.GetActiveTasksByInstanceIdAsync(instanceId, CancellationToken.None); tasksResult.IsSuccess.ShouldBeTrue();
var activeTasks = tasksResult.GetValueOrThrow(); activeTasks.Count.ShouldBe(2); // Managing Partner task + Chief Risk Officer countersign task activeTasks.ShouldContain(t => t.TaskName == "Managing Partner Approval"); activeTasks.ShouldContain(t => t.TaskName == "Chief Risk Officer Countersign"); }}Architectural Review
- Complete Business-Workflow Decoupling: Business slices simply call
StartAsyncwith domain facts (claim amount, conflict status); routing rules are fully encapsulated within the engine. - Tamper-Resistant Auditing: Every approval timestamp, user ID, and comment is persisted within an ACID transaction, satisfying statutory legal compliance.
- High-Throughput State Machine: Narrow transition flows (
IWorkflowTransitionFlow) avoid global state locking, powering massive concurrent multi-tenant workflows.