Skip to content

Approval Workflows

Patterns for building approval and review workflows.

Simple Approval

Simple Approval Workflow

Single-level approval with approve/reject:

dart
final workflow = WorkflowBuilder(
  id: 'simple-approval',
  code: 'APPROVAL',
  name: 'Simple Approval',
)
    .start('begin')

    // Validate request
    .task('validate', execute: (ctx) async {
      // Use getInitial<T> for original workflow input
      final entityId = ctx.getInitial<String>('entityId')!;
      final submittedBy = ctx.getInitial<String>('submittedBy')!;

      final entity = await entityService.get(entityId);
      return {
        'entityId': entityId,
        'entityName': entity.name,
        'entityType': entity.type,
        'submittedBy': submittedBy,
      };
    })

    // Wait for approval
    .userTask('approvalDecision',
      schemaType: 'approval',
      title: 'Approve {{entityType}}: {{entityName}}',
      description: 'Please review and approve this request.',
      assignToRole: 'approvers',
      storeAs: 'approval',
    )

    // Route based on decision
    .oneOf('routeDecision', [
      Branch.when("approval.decision == 'approved'", then: 'processApproval'),
      Branch.otherwise(then: 'processRejection'),
    ])

    // Handle approved
    .task('processApproval', execute: (ctx) async {
      // Use getAny<T> for accumulated output from earlier nodes
      final entityId = ctx.getAny<String>('entityId')!;
      await entityService.approve(entityId);
      await notifyService.notifyApproval(ctx.input);
      return {'approvedAt': DateTime.now().toIso8601String()};
    })

    // Handle rejected
    .task('processRejection', execute: (ctx) async {
      // Use getAny<T> for accumulated output from earlier nodes
      final entityId = ctx.getAny<String>('entityId')!;
      await entityService.reject(entityId);
      await notifyService.notifyRejection(ctx.input);
      return {'rejectedAt': DateTime.now().toIso8601String()};
    })

    .end('approved')
    .end('rejected')

    .connect('begin', 'validate')
    .connect('validate', 'approvalDecision')
    .connect('approvalDecision', 'routeDecision')
    .connect('processApproval', 'approved')
    .connect('processRejection', 'rejected')

    .build();

Approval with Revision

Revision Loop Pattern

Allow submitter to revise and resubmit:

dart
builder
    .userTask('approvalDecision', storeAs: 'approval', ...)

    .oneOf('routeDecision', [
      Branch.whenFn((o) => o['approval']?['decision'] == 'approved', then: 'complete'),
      Branch.whenFn((o) => o['approval']?['decision'] == 'revision', then: 'requestRevision'),
      Branch.otherwise(then: 'rejected'),
    ])

    // Request revision from submitter
    .userTask('revisionTask',
      schemaType: 'revision',
      title: 'Revision Required: {{entityName}}',
      assignToUser: '{{submittedBy}}',  // Dynamic assignment
      storeAs: 'revision',
    )

    // After revision, go back to approval
    .connect('revisionTask', 'approvalDecision')  // Loop back

    .end('complete')
    .end('rejected');

Multi-Level Approval

N-Level Sequential Approval

Sequential approval chain:

dart
builder
    // Build approval chain based on business rules
    .task('buildApprovalChain', execute: (ctx) async {
      // Use getInitial<T> for original workflow input
      final amount = ctx.getInitial<double>('amount')!;
      final type = ctx.getInitial<String>('type')!;

      List<Map<String, dynamic>> chain = [];

      // Supervisor always
      chain.add({'level': 1, 'role': 'supervisor', 'name': 'Supervisor'});

      // Manager for > $5,000
      if (amount > 5000) {
        chain.add({'level': 2, 'role': 'manager', 'name': 'Manager'});
      }

      // Director for > $25,000
      if (amount > 25000) {
        chain.add({'level': 3, 'role': 'director', 'name': 'Director'});
      }

      // VP for > $100,000
      if (amount > 100000) {
        chain.add({'level': 4, 'role': 'vp', 'name': 'VP'});
      }

      return {
        'approvalChain': chain,
        'currentLevel': 0,
        'totalLevels': chain.length,
        'hasMoreLevels': chain.isNotEmpty,
      };
    })

    // Loop through levels (see Loop Patterns)
    .oneOf('checkHasMoreLevels', ...)
    .userTask('levelApproval', ...)
    .oneOf('routeLevelDecision', ...)
    .task('incrementLevel', ...);

Parallel Approval

Parallel Approval

Multiple approvers review simultaneously:

dart
builder
    // Fork to multiple reviewers
    .allOf('startReviews', ['technicalReview', 'legalReview', 'financeReview'])

    // Each review is a user task
    .userTask('technicalReview',
      schemaType: 'review',
      title: 'Technical Review',
      assignToRole: 'technical_reviewers',
      storeAs: 'technicalReviewResult',
    )
    .userTask('legalReview',
      schemaType: 'review',
      title: 'Legal Review',
      assignToRole: 'legal_reviewers',
      storeAs: 'legalReviewResult',
    )
    .userTask('financeReview',
      schemaType: 'review',
      title: 'Finance Review',
      assignToRole: 'finance_reviewers',
      storeAs: 'financeReviewResult',
    )

    // Wait for all reviews
    .allOf('waitForAllReviews', ['evaluateReviews'])
    .connect('technicalReview', 'waitForAllReviews')
    .connect('legalReview', 'waitForAllReviews')
    .connect('financeReview', 'waitForAllReviews')

    // Evaluate combined results
    .task('evaluateReviews', execute: (ctx) async {
      // Use getAny<T> for accumulated output from parallel branches
      final tech = ctx.getAny<bool>('technicalReviewResult.approved') ?? false;
      final legal = ctx.getAny<bool>('legalReviewResult.approved') ?? false;
      final finance = ctx.getAny<bool>('financeReviewResult.approved') ?? false;

      return {
        'allApproved': tech && legal && finance,
        'rejectionReasons': [
          if (!tech) 'Technical',
          if (!legal) 'Legal',
          if (!finance) 'Finance',
        ],
      };
    })

    .oneOf('finalDecision', [
      Branch.whenFn((o) => o['allApproved'] == true, then: 'approved'),
      Branch.otherwise(then: 'rejected'),
    ]);

Escalation

Auto-escalate if not actioned in time:

dart
builder
    .userTask('approvalWithDeadline',
      schemaType: 'approval',
      dueDate: DateTime.now().add(Duration(hours: 24)),
      storeAs: 'approval',
    )

    // Timer sends signal if deadline passes
    .signalWait('waitForDeadline', signal: 'deadline_passed', storeAs: 'deadlineResult')

    // Check if timed out or completed
    .oneOf('checkTimeout', [
      Branch.whenFn((o) => o['approval'] != null, then: 'routeDecision'),
      Branch.whenFn((o) => o['deadlineResult']?['expired'] == true, then: 'escalate'),
    ])

    // Escalate to manager
    .task('escalate', execute: (ctx) async {
      // Use getAny<T> for accumulated output
      final assignedTo = ctx.getAny<String>('assignedTo')!;
      final managerId = await getManager(assignedTo);
      return {'escalatedTo': managerId};
    })
    .userTask('escalatedApproval',
      schemaType: 'approval',
      title: 'ESCALATED: {{entityName}}',
      assignToUser: '{{escalatedTo}}',
    );

Delegation

Allow assignee to delegate to someone else:

dart
builder
    .userTask('approval', storeAs: 'decision', ...)

    .oneOf('routeDecision', [
      Branch.whenFn((o) => o['decision']?['action'] == 'delegate', then: 'handleDelegation'),
      Branch.whenFn((o) => o['decision']?['decision'] == 'approved', then: 'approved'),
      Branch.otherwise(then: 'rejected'),
    ])

    // Create new task for delegate
    .task('handleDelegation', execute: (ctx) async {
      // Use get<T> with dot-notation for previous node output
      return {
        'delegatedTo': ctx.get<String>('decision.delegateTo'),
        'delegatedBy': ctx.get<String>('decision.completedBy'),
      };
    })
    .userTask('delegatedApproval',
      schemaType: 'approval',
      assignToUser: '{{delegatedTo}}',
      storeAs: 'decision',
    )
    .connect('delegatedApproval', 'routeDecision');  // Loop back

Best Practices

  1. Define clear approval rules - Who approves what and when
  2. Set appropriate deadlines - Enable escalation
  3. Track approval history - Store each decision
  4. Notify all parties - On submission, approval, rejection
  5. Allow revision - Not just approve/reject
  6. Handle delegation - Real organizations delegate

Next Steps