Skip to main content

Synchronous vs Asynchronous Workflow Execution

Last Updated: January 9, 2026

Learn how to handle mixed synchronous/asynchronous execution within a single workflow - where some tasks must complete before responding to the client (e.g., mobile app), while other tasks continue in the background.


The Use Case

Real-World Scenario: Mobile Banking Transfer

A mobile app user initiates a transfer. The workflow has these tasks:

1. ✅ Validate PIN           → Must complete before response (SYNC)
2. ✅ Validate Account → Must complete before response (SYNC)
3. ✅ Debit Customer → Must complete before response (SYNC)
─────────────────────────────────────────────────────────────────
📱 RETURN RESPONSE TO MOBILE APP HERE
─────────────────────────────────────────────────────────────────
4. 🔄 Transfer to Other Bank → Can happen in background (ASYNC)
5. ⚠️ Reversal (if failed) → Background (ASYNC)
6. 📧 Send Notification → Background (ASYNC)
7. 📄 Generate Invoice → Background (ASYNC)
8. 📤 Send Invoice → Background (ASYNC)

Requirements:

  • ❌ If steps 1-3 fail → Return error immediately (real-time)
  • ✅ If steps 1-3 succeed → Return success immediately, steps 4-8 continue in background
  • 📱 Mobile app gets response in < 3 seconds (not 30+ seconds)

The BPMN Pattern: Wait State

Concept: Process Wait States

BPMN processes can have wait states where execution pauses until an external event occurs. The key insight:

A wait state allows the API to return a response while the process continues later.

BPMN Wait State Elements

ElementPurposeUse Case
Intermediate Message EventWait for external message/signalWait for callback from external system
Timer EventWait for time durationDelayed execution (retry after 5 min)
User TaskWait for human actionManual approval required
Receive TaskWait for messageWait for webhook response

Solution: Intermediate Message Event Pattern

BPMN Diagram

┌─────────────────────────────────────────────────────────────────┐
│ Mobile Banking Transfer Workflow │
└─────────────────────────────────────────────────────────────────┘

┌───────────────┐
│ Start Event │
└───────┬───────┘
│
â–¼
┌──────────────────────┐
│ Validate PIN │
│ (ScriptTask) │
│ │
│ If invalid → throw │
└──────────┬───────────┘
│
â–¼
┌──────────────────────┐
│ Validate Account │
│ (ScriptTask) │
│ │
│ If invalid → throw │
└──────────┬───────────┘
│
â–¼
┌──────────────────────┐
│ Debit Customer │
│ (ScriptTask) │
│ │
│ If fails → throw │
└──────────┬───────────┘
│
│ ✅ All critical tasks passed
│
â–¼
╔═════════════════════════════╗
║ INTERMEDIATE MESSAGE EVENT ║ ← WAIT STATE (Process pauses here)
║ "AwaitBankTransferResult" ║ ← API returns response here
╚═════════════┬═══════════════╝ ← Process continues when signaled
│
│ ⏸️ PROCESS PAUSED
│ 📱 API RETURNS TO MOBILE APP
│
│ (Background worker continues later)
│
â–¼
┌──────────────────────┐
│ Transfer to Bank │
│ (ScriptTask) │
│ │
│ Call external API │
└──────────┬───────────┘
│
â–¼
┌──────────────────────┐
│ Check Success? │
│ (Exclusive Gateway) │
└──────┬───────────────┘
│
┌───────┴──────┐
│ │
SUCCESS FAILED
│ │
â–¼ â–¼
┌─────────────┐ ┌──────────────┐
│ Send │ │ Do Reversal │
│ Notification│ │ │
└──────┬──────┘ └──────┬───────┘
│ │
â–¼ â–¼
┌─────────────┐ ┌──────────────┐
│ Generate │ │ Send Failure │
│ Invoice │ │ Notification │
└──────┬──────┘ └──────┬───────┘
│ │
▼ │
┌─────────────┐ │
│ Send │ │
│ Invoice │ │
└──────┬──────┘ │
│ │
└────────┬───────┘
│
â–¼
┌────────────────┐
│ End Event │
└────────────────┘

Implementation

Step 1: Define BPMN Process with Wait State

Process Definition XML (key parts):

<bpmn:process id="MobileBankingTransfer" name="Mobile Banking Transfer">

<!-- Start -->
<bpmn:startEvent id="StartEvent_1" name="Start Transfer" />

<!-- Critical tasks (synchronous) -->
<bpmn:scriptTask id="ValidatePIN" name="Validate PIN">
<bpmn:script>
var result = doCmd('ValidatePIN', {
customerId: context.customerId,
pin: context.pin
});

if (!result.isValid) {
throw new Error('Invalid PIN');
}
</bpmn:script>
</bpmn:scriptTask>

<bpmn:scriptTask id="ValidateAccount" name="Validate Account">
<bpmn:script>
var result = doCmd('ValidateAccount', {
accountNumber: context.debitAccountNumber
});

if (!result.isValid) {
throw new Error('Invalid account: ' + result.message);
}

if (result.balance < context.amount) {
throw new Error('Insufficient balance');
}
</bpmn:script>
</bpmn:scriptTask>

<bpmn:scriptTask id="DebitCustomer" name="Debit Customer">
<bpmn:script>
var result = doCmd('DebitAccount', {
accountNumber: context.debitAccountNumber,
amount: context.amount,
reference: context.transactionRef,
narration: 'Transfer to ' + context.beneficiaryBank
});

if (!result.isSuccessful) {
throw new Error('Debit failed: ' + result.message);
}

return {
debitTransactionId: result.transactionId
};
</bpmn:script>
</bpmn:scriptTask>

<!-- WAIT STATE: Process pauses here -->
<bpmn:intermediateCatchEvent id="AwaitBankTransfer" name="Await Bank Transfer">
<bpmn:messageEventDefinition messageRef="BankTransferContinue" />
</bpmn:intermediateCatchEvent>

<!-- Asynchronous tasks (continue in background) -->
<bpmn:scriptTask id="TransferToBank" name="Transfer to Other Bank">
<bpmn:script>
var result = doCmd('InterBankTransfer', {
beneficiaryBank: context.beneficiaryBank,
beneficiaryAccount: context.beneficiaryAccount,
amount: context.amount,
reference: context.transactionRef,
narration: context.narration
});

return {
transferResult: result,
isSuccessful: result.isSuccessful
};
</bpmn:script>
</bpmn:scriptTask>

<!-- Rest of process... -->

</bpmn:process>

Step 2: API Controller - Start Process and Return Immediately

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Step 3: BPMN Engine - Handle Wait States

Your BPMN execution engine needs to support wait states. Here's how:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Step 4: Signal Process to Continue

When you want to resume the process from the wait state:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Alternative: Async Task Pattern (Without Wait State)

If your BPMN engine doesn't support wait states yet, use this pattern:

Pattern: Fire-and-Forget Async Task

┌─────────────────────────────────────────────────────────────┐
│ Mobile Transfer (Without Wait State) │
└─────────────────────────────────────────────────────────────┘

┌───────────────┐
│ Start Event │
└───────┬───────┘
│
â–¼
┌──────────────────────┐
│ Validate PIN │
│ (ScriptTask) │
└──────────┬───────────┘
│
â–¼
┌──────────────────────┐
│ Validate Account │
│ (ScriptTask) │
└──────────┬───────────┘
│
â–¼
┌──────────────────────┐
│ Debit Customer │
│ (ScriptTask) │
└──────────┬───────────┘
│
â–¼
┌──────────────────────────────────┐
│ Queue Async Tasks │
│ (ScriptTask) │
│ │
│ // Start separate workflow │
│ doCmd('StartProcess', { │
│ processDefinitionId: │
│ 'TransferCompletion', │
│ variables: {...} │
│ }); │
│ │
│ return { status: 'queued' }; │
└──────────┬───────────────────────┘
│
â–¼
┌───────────────┐
│ End Event │
└───────────────┘

(First workflow ends here - API returns)


═══════════════════════════════════════════════════════
SEPARATE WORKFLOW (Runs in Background)
═══════════════════════════════════════════════════════

┌───────────────┐
│ Start Event │
│ (Process: │
│ Transfer │
│ Completion) │
└───────┬───────┘
│
â–¼
┌──────────────────────┐
│ Transfer to Bank │
│ (ScriptTask) │
└──────────┬───────────┘
│
â–¼
┌──────────────────────┐
│ Check Success? │
│ (Gateway) │
└──────┬───────────────┘
│
┌───┴───┐
│ │
SUCCESS FAILED
│ │
â–¼ â–¼
(etc...)

Implementation: Queue Async Tasks

// In "Queue Async Tasks" script task
// This is the last task before returning to mobile app

// Start a SEPARATE workflow for the async tasks
var asyncResult = doCmd('StartProcess', {
processDefinitionId: 'TransferCompletion',
variables: {
transactionRef: context.transactionRef,
debitTransactionId: context.debitTransactionId,
beneficiaryBank: context.beneficiaryBank,
beneficiaryAccount: context.beneficiaryAccount,
amount: context.amount,
customerId: context.customerId,
narration: context.narration
}
});

// Log the async workflow ID
console.log('Started async workflow: ' + asyncResult.processInstanceId);

return {
status: 'processing',
asyncWorkflowId: asyncResult.processInstanceId
};

// Main workflow ends here
// API returns success to mobile app
// Async workflow continues independently

Execution Timeline

With Wait State Pattern

Time    Mobile App          API Server              BPMN Engine
─────────────────────────────────────────────────────────────────
T+0s POST /transfer → Validate request → Start process

T+0.5s → Execute: Validate PIN
T+1s → Execute: Validate Account
T+1.5s → Execute: Debit Customer
T+2s → Reach wait state (PAUSE)
Status: Waiting

T+2s ← Success (200) ← Return response ← (Process paused)
{
success: true,
status: "processing"
}

T+2.1s Signal to continue → Resume from wait state

T+3s → Execute: Transfer to Bank
T+10s → Execute: Send Notification
T+11s → Execute: Generate Invoice
T+12s → Execute: Send Invoice
Status: Completed

T+20s GET /status → Query status → (Process complete)
← Success (200) ← Return result ←
{
status: "completed",
transferSuccessful: true
}

With Separate Workflow Pattern

Time    Mobile App          API Server              BPMN Engine
─────────────────────────────────────────────────────────────────
T+0s POST /transfer → Validate request → Start Workflow A

T+0.5s → Execute: Validate PIN
T+1s → Execute: Validate Account
T+1.5s → Execute: Debit Customer
T+2s → Execute: Queue Async Tasks
↓
Start Workflow B (async)

T+2.1s → Workflow A: End
Status: Completed

T+2.1s ← Success (200) ← Return response ←
{
success: true,
status: "processing"
}

T+3s → Workflow B: Transfer to Bank
T+10s → Workflow B: Send Notification
T+11s → Workflow B: Generate Invoice
T+12s → Workflow B: Send Invoice
Workflow B: End
Status: Completed

Comparison: Wait State vs Separate Workflows

AspectWait State PatternSeparate Workflows
BPMN ComplexitySingle workflowTwo workflows
ImplementationRequires wait state supportWorks with current engine
TrackingOne process instanceTwo process instances
VariablesShared naturallyMust pass explicitly
DebuggingSingle execution traceTwo separate traces
Recommended✅ If engine supports it✅ Quick implementation

Error Handling

Critical Task Failure (Return Immediately)

// In Validate PIN task
try {
var result = doCmd('ValidatePIN', {
customerId: context.customerId,
pin: context.pin
});

if (!result.isValid) {
// This throws error, process fails immediately
// API returns error to mobile app
throw new Error('INVALID_PIN: Please enter correct PIN');
}
} catch (error) {
// Error propagates up
// Process status: Failed
// Mobile app gets error response immediately
throw error;
}

Async Task Failure (Handle in Background)

// In Transfer to Bank task (async)
try {
var result = doCmd('InterBankTransfer', {
beneficiaryBank: context.beneficiaryBank,
beneficiaryAccount: context.beneficiaryAccount,
amount: context.amount
});

if (!result.isSuccessful) {
// Don't throw! Handle gracefully in background
return {
transferResult: result,
isSuccessful: false,
needsReversal: true,
errorMessage: result.message
};
}

return {
transferResult: result,
isSuccessful: true
};
} catch (error) {
// Catch and handle (don't let it kill process)
return {
isSuccessful: false,
needsReversal: true,
errorMessage: error.message
};
}

Best Practices

✅ DO

  • Return immediately after critical tasks complete
  • Use wait states for sync/async boundary
  • Handle errors differently - throw for sync tasks, return error for async tasks
  • Provide status endpoint so mobile app can check completion
  • Send push notifications when async tasks complete
  • Log extensively in async tasks (user can't see them)
  • Set timeouts for async tasks to prevent hanging
  • Implement idempotency - async tasks may retry

❌ DON'T

  • Block mobile app waiting for email/invoice generation
  • Throw errors in async tasks (handle gracefully)
  • Forget to send completion notifications
  • Skip status tracking for async portion
  • Assume async tasks always succeed

Mobile App Integration

Swift (iOS) Example

func initiateTransfer(request: TransferRequest) async throws -> TransferResponse {
// Call API
let response = try await apiClient.post("/api/mobile/transfer", body: request)

if response.success {
// Show success immediately
showAlert("Transfer Initiated", "Your transfer is being processed")

// Poll for completion in background
Task {
await pollTransferStatus(transactionRef: response.transactionRef)
}

return response
} else {
// Critical task failed - show error
throw TransferError(message: response.message)
}
}

func pollTransferStatus(transactionRef: String) async {
var attempts = 0
let maxAttempts = 20 // Poll for 1 minute (3s intervals)

while attempts < maxAttempts {
try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds

if let status = try? await apiClient.get("/api/mobile/transfer/status/\(transactionRef)") {
if status.isComplete {
// Show push notification
if status.transferSuccessful {
LocalNotificationService.show(
title: "Transfer Completed",
body: "Your transfer of \(amount) was successful"
)
} else {
LocalNotificationService.show(
title: "Transfer Failed",
body: "Your transfer failed. Amount has been reversed."
)
}
break
}
}

attempts += 1
}
}

Testing

Test Critical Path (Synchronous)

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Test Async Path

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Summary

Key Takeaways

  1. ✅ Use wait states (Intermediate Message Events) to create sync/async boundary
  2. ✅ Critical tasks (PIN, Account, Debit) execute synchronously before API response
  3. ✅ Async tasks (Transfer, Notification, Invoice) continue in background after response
  4. ✅ Mobile app gets response in < 3 seconds, not 30+ seconds
  5. ✅ Status endpoint allows mobile app to check completion
  6. ✅ Push notifications inform user when async tasks complete

Implementation Options

OptionComplexityTime to ImplementRecommended
Wait State PatternMedium4-6 hours✅ Best approach
Separate WorkflowsLow1-2 hours✅ Quick solution
Fully AsyncHigh8+ hours❌ Overkill

Next Steps

  1. Choose pattern (Wait State or Separate Workflows)
  2. Implement BPMN process with sync/async boundary
  3. Update API controller to return immediately
  4. Add status polling endpoint
  5. Test with real mobile app
  6. Add push notifications for completion

Your mobile banking workflow will now provide instant feedback for critical operations while handling time-consuming tasks in the background! 🚀📱