Approval Flow: Human-in-the-Loop Workflows
Approval Flow: Human-in-the-Loop Workflows
Section titled “Approval Flow: Human-in-the-Loop Workflows”The Problem: AI Shouldn’t Act Alone
Section titled “The Problem: AI Shouldn’t Act Alone”Your AI system generates a legal document, makes a financial decision, or prepares content for publication. Do you just let it act?
The dangerous approach: AI generates → auto-execute. Problems:
- No human verification of AI judgment
- Legal liability when AI makes mistakes
- No audit trail showing who approved what
- Impossible to catch errors before they cause harm
What you need: A workflow that:
- Lets AI do the heavy lifting (drafting, analysis)
- Pauses for human review at critical decision points
- Handles timeouts gracefully (humans forget)
- Supports rejection and re-work cycles
- Records who approved what, when, and why
This is the AwaitApproval pattern—human checkpoints in automated workflows.
Learning Objectives
Section titled “Learning Objectives”After this example, you will understand:
- Human approval gates that pause workflow execution
- Timeout handling for unresponsive approvers
- Escalation paths when approvals don’t arrive
- Rejection cycles that route back for re-work
- State persistence across hours or days of waiting
Conceptual Foundation
Section titled “Conceptual Foundation”The Human-AI Collaboration Spectrum
Section titled “The Human-AI Collaboration Spectrum”Where should humans be in the loop?
| Approach | Human Role | Risk Level | Speed |
|---|---|---|---|
| Human-only | Does everything | Lowest | Slowest |
| AI-assisted | Reviews AI output before action | Balanced | Moderate |
| AI-monitored | Notified after AI acts | Higher | Fast |
| AI-autonomous | No human involvement | Highest | Fastest |
Approval flows implement AI-assisted: AI does the work, humans make the final call.
Why Pause for Humans?
Section titled “Why Pause for Humans?”Human approval isn’t bureaucracy—it’s risk management:
| Concern | Why Pause? |
|---|---|
| Liability | Who’s responsible when AI makes mistakes? The approver. |
| Brand safety | AI might miss tone, context, or cultural sensitivities |
| Compliance | Regulations may require human review (financial, medical, legal) |
| Judgment | Some decisions require human values, not just optimization |
The Persistence Challenge
Section titled “The Persistence Challenge”Unlike instant operations, approvals can take hours or days:
10:00 AM - Workflow reaches approval step10:01 AM - Notification sent to approver [Workflow pauses, state persisted] ... [Approver at lunch, in meetings, on vacation...] ...3:15 PM - Approver reviews and approves3:15 PM - Workflow resumes exactly where it pausedKey insight: The workflow state must survive process restarts, deployments, and server crashes while waiting for human input.
Timeout: What If They Never Respond?
Section titled “Timeout: What If They Never Respond?”Humans are unreliable. They forget, go on vacation, or leave the company. Workflows need timeout strategies:
| Strategy | When | Outcome |
|---|---|---|
| Fail | Approval is critical | Workflow fails, manual intervention required |
| Escalate | Someone else can approve | Routes to backup approver |
| Auto-approve | Low-risk decisions | Proceeds automatically (with logging) |
| Remind | Approver needs nudging | Sends reminders before timeout |
Rejection: The Re-Work Cycle
Section titled “Rejection: The Re-Work Cycle”Approval isn’t always “yes.” Rejections route back for revision:
┌──────────────────────────────────────────────────────────────────┐│ ││ ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────┐ ││ │ Draft │───▶│ Review │───▶│ Await │───▶│ Publish │ ││ └─────────┘ └──────────┘ │ Approval │ └─────────┘ ││ └────────────┘ ││ │ ││ [Rejected] ││ │ ││ ▼ ││ ┌────────────┐ ││ │ Address │ ││ │ Concerns │──────────────────┘│ └────────────┘│ │└────────────────────────────────────────┘ (loop until approved)Design Decisions
Section titled “Design Decisions”| Decision | Why This Approach | Alternative | Trade-off |
|---|---|---|---|
| Explicit approval step | Clear checkpoint | Implicit approval via no-response | Slower, but explicit accountability |
| Timeout with escalation | Business continuity | Fail on timeout | May bypass original approver |
| Rejection loops back | Iterative improvement | Fail on rejection | May loop indefinitely |
| State persists during wait | Reliability | In-memory wait | Survives restarts |
When to Use This Pattern
Section titled “When to Use This Pattern”Good fit when:
- Actions have external consequences (publishing, payments, contracts)
- Regulatory compliance requires human review
- Decisions involve judgment, not just computation
- Audit trails are required
Poor fit when:
- Speed matters more than review (real-time systems)
- Decisions are fully deterministic (no judgment needed)
- No humans are available in the workflow
- Actions are easily reversible
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”| Anti-Pattern | Problem | Correct Approach |
|---|---|---|
| No timeout | Workflows wait forever | Always configure timeout with handling |
| Silent failure | No one knows workflow is stuck | Send notifications, log state |
| Approval without context | Approver can’t make informed decision | Include all relevant information |
| No rejection path | Rejection = workflow death | Route rejections to revision step |
| Mutable approval state | Can’t prove what was approved | Immutable approval records |
Building the Workflow
Section titled “Building the Workflow”The Shape First
Section titled “The Shape First”┌───────────────┐ ┌─────────────┐ ┌───────────────────────────┐│ DraftDocument │───▶│ LegalReview │───▶│ AwaitApproval ││ │ │ │ │ <LegalTeam> ││ AI generates │ │ AI analyzes │ │ ││ draft │ │ legal issues│ │ [Timeout: 2 days] ││ │ │ │ │ [OnTimeout: Escalate] ││ │ │ │ │ [OnRejection: AddressFix] │└───────────────┘ └─────────────┘ └───────────────────────────┘ │ ┌──────────────────────────────┼──────────────────────────┐ │ │ │ ▼ ▼ ▼ [Approved] [Rejected] [Timeout] │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ PublishDocument │ │ AddressConcerns │ │ EscalateToMgr │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ ▼ ▼ │ ┌─────────────────┐ (back to LegalReview) │ │NotifyStakeholder│ │ └─────────────────┘ │ │ ┌──────────────────────────┘ ▼ (continues to Publish)State: What We Track
Section titled “State: What We Track”[WorkflowState]public record DocumentState : IWorkflowState{ // Identity public Guid WorkflowId { get; init; }
// Input document public Document Document { get; init; } = null!;
// Draft content (AI-generated) public string? DraftContent { get; init; }
// Legal analysis (AI-generated) public LegalReviewResult? LegalReview { get; init; }
// Human approval decision public ApprovalDecision? Approval { get; init; }
// Workflow status flags public bool IsPublished { get; init; } public bool StakeholdersNotified { get; init; } public bool IsEscalated { get; init; }}Why this design?
LegalReview: AI’s analysis, shown to human approver for contextApproval: Human decision, captures who/when/whyIsEscalated: Flag for timeout handling path
The Supporting Records
Section titled “The Supporting Records”public record Document( string Title, string Author, DocumentType Type, string Content);
public record LegalReviewResult( bool HasIssues, // Did AI find problems? IReadOnlyList<string> Issues, // What problems? string ReviewerComments); // AI's analysis
public record ApprovalDecision( bool Approved, // Yes or no string ApproverId, // WHO approved DateTimeOffset DecisionTime, // WHEN they approved string? Comments); // WHY (for rejections)
public enum DocumentType { Contract, Policy, Procedure, Marketing }The Workflow Definition
Section titled “The Workflow Definition”var workflow = Workflow<DocumentState> .Create("document-approval") .StartWith<DraftDocument>() .Then<LegalReview>() .AwaitApproval<LegalTeam>(options => options .WithTimeout(TimeSpan.FromDays(2)) .OnTimeout(flow => flow.Then<EscalateToManager>()) .OnRejection(flow => flow .Then<AddressLegalConcerns>() .Then<LegalReview>())) .Then<PublishDocument>() .Finally<NotifyStakeholders>();Reading this definition:
- Draft the document (AI)
- Legal review (AI analysis)
- Await approval from LegalTeam
- If approved: continue to publish
- If timeout (2 days): escalate to manager, then publish
- If rejected: address concerns, re-review, request approval again
- Publish approved document
- Notify stakeholders
Approval Options
Section titled “Approval Options”Basic approval (wait indefinitely):
.AwaitApproval<LegalTeam>()With timeout (fail if no response):
.AwaitApproval<LegalTeam>(options => options .WithTimeout(TimeSpan.FromDays(2)))Timeout with escalation:
.AwaitApproval<LegalTeam>(options => options .WithTimeout(TimeSpan.FromDays(2)) .OnTimeout(flow => flow.Then<EscalateToManager>()))With rejection handling:
.AwaitApproval<LegalTeam>(options => options .OnRejection(flow => flow .Then<AddressLegalConcerns>() .Then<LegalReview>()))Multiple approvers:
.AwaitApproval<LegalTeam>(options => options .RequireAll() // All team members must approve .WithQuorum(2)) // OR: at least 2 must approveThe Approver Interface
Section titled “The Approver Interface”public class LegalTeam : IApprover<DocumentState>{ public string Role => "legal-team";
// Create the approval request with all context needed for decision public ApprovalRequest CreateRequest(DocumentState state) { return new ApprovalRequest { Title = $"Legal Approval Required: {state.Document.Title}", Description = "Please review the document and legal analysis.", Context = new Dictionary<string, object> { ["DocumentTitle"] = state.Document.Title, ["DocumentType"] = state.Document.Type.ToString(), ["LegalIssues"] = state.LegalReview?.Issues ?? [], ["ReviewerComments"] = state.LegalReview?.ReviewerComments ?? "" } }; }
// Apply the decision to state public DocumentState ApplyApproval(DocumentState state, ApprovalDecision decision) { return state.With(s => s.Approval, decision); }}The key insight: The approval request includes ALL context needed for the human to make an informed decision. Don’t make them dig for information.
The Escalation Step
Section titled “The Escalation Step”public class EscalateToManager : IWorkflowStep<DocumentState>{ private readonly IEscalationService _escalation;
public EscalateToManager(IEscalationService escalation) { _escalation = escalation; }
public async Task<StepResult<DocumentState>> ExecuteAsync( DocumentState state, StepContext context, CancellationToken ct) { await _escalation.EscalateAsync( $"Document approval timeout: {state.Document.Title}", state.WorkflowId, ct);
return state .With(s => s.IsEscalated, true) .AsResult(); }}Escalation records the fact that normal approval didn’t happen. This is important for audit.
The Rejection Handler
Section titled “The Rejection Handler”public class AddressLegalConcerns : IWorkflowStep<DocumentState>{ private readonly IDocumentReviser _reviser;
public AddressLegalConcerns(IDocumentReviser reviser) { _reviser = reviser; }
public async Task<StepResult<DocumentState>> ExecuteAsync( DocumentState state, StepContext context, CancellationToken ct) { // Use AI to address the rejection feedback var revisedContent = await _reviser.AddressIssuesAsync( state.DraftContent!, state.LegalReview!.Issues, state.Approval?.Comments, // Include rejection reason ct);
return state .With(s => s.DraftContent, revisedContent) .With(s => s.LegalReview, null) // Clear for re-review .With(s => s.Approval, null) // Clear previous decision .AsResult(); }}After addressing concerns, the workflow loops back to LegalReview, then requests approval again.
Submitting Approvals
Section titled “Submitting Approvals”Approvals come from an external system (UI, API, email, etc.):
public class ApprovalController : ControllerBase{ private readonly IApprovalService _approvals;
[HttpPost("{workflowId}/approve")] public async Task<IActionResult> Approve( Guid workflowId, [FromBody] ApprovalRequest request) { await _approvals.SubmitDecisionAsync(workflowId, new ApprovalDecision( Approved: true, ApproverId: User.Identity!.Name!, DecisionTime: DateTimeOffset.UtcNow, Comments: request.Comments));
return Ok(); }
[HttpPost("{workflowId}/reject")] public async Task<IActionResult> Reject( Guid workflowId, [FromBody] RejectionRequest request) { await _approvals.SubmitDecisionAsync(workflowId, new ApprovalDecision( Approved: false, ApproverId: User.Identity!.Name!, DecisionTime: DateTimeOffset.UtcNow, Comments: request.Reason)); // Reason is required for rejections
return Ok(); }}When the decision arrives, the workflow resumes exactly where it paused.
The “Aha Moment”
Section titled “The “Aha Moment””Trust in AI systems comes from transparency and control, not from the AI being perfect.
The approval step isn’t a bottleneck—it’s the difference between “AI error” and “approved decision with known risk.” When something goes wrong at 2 AM, you’ll be glad you can answer “Who approved this?” with a name, timestamp, and their reasoning.
The workflow persists its state across hours or days of waiting. Process restarts, deployments, even server crashes don’t lose the approval context. When the human finally responds, the workflow picks up exactly where it left off.
Querying Pending Approvals
Section titled “Querying Pending Approvals”Build dashboards showing what needs attention:
// Find all documents awaiting legal approvalvar pending = await session .Query<DocumentApprovalReadModel>() .Where(d => d.CurrentPhase == DocumentApprovalPhase.AwaitingApproval) .ToListAsync();
// Find approvals approaching timeoutvar urgent = await session .Query<DocumentApprovalReadModel>() .Where(d => d.CurrentPhase == DocumentApprovalPhase.AwaitingApproval && d.ApprovalRequestedAt < DateTimeOffset.UtcNow.AddHours(-36)) .ToListAsync();Extension Exercises
Section titled “Extension Exercises”Exercise 1: Add Reminder Notifications
Section titled “Exercise 1: Add Reminder Notifications”Send reminders before timeout:
- Configure reminder interval (e.g., every 8 hours)
- Create
SendReminderstep - Add to timeout path: remind → wait → remind → escalate
Exercise 2: Multi-Level Approval
Section titled “Exercise 2: Multi-Level Approval”Require multiple approvals in sequence:
- Add
AwaitApproval<LegalTeam>()followed byAwaitApproval<Finance>() - Each approval captures different perspective
- Handle rejection at any level
Exercise 3: Conditional Approval Requirements
Section titled “Exercise 3: Conditional Approval Requirements”Different documents need different approval levels:
- Add logic to check
Document.Type - Marketing → single approver
- Contract → legal + executive approval
- High-value → board approval
Key Takeaways
Section titled “Key Takeaways”- Approval steps persist workflow state and wait for external input
- Timeouts prevent workflows from waiting forever—always configure handling
- Escalation paths maintain business continuity when approvers don’t respond
- Rejection paths enable iterative improvement—not workflow death
- Context is critical—approvers need all information to make good decisions
- Audit trails capture who approved what, when, and why
Related
Section titled “Related”- ContentPipeline Sample - Working implementation with approval gates
- Iterative Refinement Pattern - Loops for quality improvement
- Branching Pattern - Conditional routing based on decisions