Skip to content
bitzorcas
中EN

Tutorial

Configuring Your First Approval Workflow: High-Stakes Matter Countersigning Hands-on

Leverage BitzOrcas.Modern's proprietary lightweight DAG state machine workflow engine to configure an approval workflow for major legal matters (claims exceeding 50M) with conditional gateways, partner countersigning, and immutable audit trails.

Last updated

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/else conditions.
  • 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

Potential AdversaryConflictNo ConflictYes (Major Matter = 50M)No (Standard Litigation)ApprovedApprovedRejectReject

1. Submit Matter Intake (SubmitMatterIntake)

2. Conflict of Interest Gateway (Automated Screen)

3. Execute Conflict Waiver Task (Conflict Waiver)

4. Claim Amount Gateway (ClaimAmount >= 50,000,000?)

5. Managing Partner & Chief Risk Officer Countersign (Countersign)

6. Practice Group Lead Approval (Single-Sign)

7. Archive & Record: Generate Case Ledger and Emit CAP Events

8. Reject: Revert to Lead Attorney and Freeze Draft


Core Transition Port: IWorkflowTransitionFlow

In BitzOrcas.Modern, workflow execution is decoupled from bloated monolithic services through the specialized IWorkflowTransitionFlow port:

Core API MethodOperational SemanticsGovernance Guarantee
StartAsync(...)Initiates a workflow instanceValidates definition version, generates InstanceId, and stages initial tasks
CompleteTaskAsync(...)Approver completes active taskValidates seat authorization, evaluates countersign quorum, and advances cursor
RejectTaskAsync(...)Approver rejects active taskReverts to initiator or configured node with immutable audit comments
WithdrawAsync(...)Initiator withdraws active processSafely 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:

src/Modules/Business/Legal/Application/Commands/SubmitMatterForApproval/SubmitMatterForApprovalCommandHandler.cs
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:

src/Modules/Business/Legal/Application/Commands/ApproveMatterTask/ApproveMatterTaskCommandHandler.cs
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:

Task rejection sample
// Revert back to lead attorney with structured rationale
var 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:

tests/BitzOrcas.Integration.Tests/Legal/MatterWorkflowIntegrationTests.cs
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

  1. Complete Business-Workflow Decoupling: Business slices simply call StartAsync with domain facts (claim amount, conflict status); routing rules are fully encapsulated within the engine.
  2. Tamper-Resistant Auditing: Every approval timestamp, user ID, and comment is persisted within an ACID transaction, satisfying statutory legal compliance.
  3. High-Throughput State Machine: Narrow transition flows (IWorkflowTransitionFlow) avoid global state locking, powering massive concurrent multi-tenant workflows.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%