Skip to main content

Async Boundaries (Phase 2)

Async Boundaries enable background execution of process tasks, allowing the API to return immediately while the process continues asynchronously.

Overview

Async boundaries provide:

  • Immediate API response - Return to caller without waiting
  • Background execution - Process continues in background
  • Improved user experience - No long waits for users
  • Better scalability - Handle more concurrent requests
  • Mobile optimization - Ideal for mobile apps

How It Works

Without Async Boundary (Synchronous)

User Request → Process Starts → Task 1 → Task 2 → Task 3 → Response
↑ ↑
Wait... 5 seconds

Problem: User waits 5 seconds for entire process to complete.

With Async Boundary (Asynchronous)

User Request → Process Starts → [Async Boundary] → Response (immediate)

Background: Task 1 → Task 2 → Task 3

Benefit: User gets immediate response, process continues in background.

Configuration

Use camunda:asyncBefore="true" on any task to create an async boundary:

<bpmn:userTask id="ProcessApplication" 
name="Process Application"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="application-form"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

Common Patterns

Pattern 1: Mobile Loan Application

User submits application, gets immediate response, processing happens in background.

<bpmn:startEvent id="StartLoanApplication" name="Start"/>

<!-- Validate basic input (synchronous - fast) -->
<bpmn:scriptTask id="ValidateInput" name="Validate Input">
<bpmn:script>
var application = context.application;

// Quick validation only
if (!application.customerId || !application.amount) {
throw new BpmnError('VALIDATION_ERROR', 'Missing required fields');
}

logger.info('Basic validation passed for customer ' + application.customerId);

return {
customerId: application.customerId,
amount: application.amount,
applicationId: context.applicationId
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC BOUNDARY: Process continues in background after this point -->
<bpmn:userTask id="ProcessApplication"
name="Process Application"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="application-review-form"/>
<custom:property name="ResponsibleRole" value="LOAN_OFFICER"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

<!-- Credit check (runs in background) -->
<bpmn:serviceTask id="CreditCheck" name="Credit Check">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CreditCheckCommand"/>
<custom:property name="customerId" value="${context.customerId}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Risk assessment (runs in background) -->
<bpmn:serviceTask id="RiskAssessment" name="Risk Assessment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="RiskAssessmentCommand"/>
<custom:property name="customerId" value="${context.customerId}"/>
<custom:property name="amount" value="${context.amount}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Notify customer (runs in background) -->
<bpmn:sendTask id="NotifyCustomer" name="Notify Customer">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="sms"/>
<custom:property name="to" value="${context.customerPhone}"/>
<custom:property name="message" value="Your loan application is being processed. You'll be notified of the decision."/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="EndLoanApplication" name="End"/>

API Response (immediate):

{
"processInstanceId": "abc-123",
"applicationId": "APP-001",
"status": "SUBMITTED",
"message": "Your application has been submitted and is being processed."
}

Timeline:

  • t=0ms: User submits application
  • t=100ms: Basic validation completes
  • t=150ms: API returns response to user
  • t=200ms: User task created in background
  • t=300ms: Credit check starts (background)
  • t=1500ms: Risk assessment completes (background)
  • t=1600ms: Customer notified via SMS (background)

Benefits:

  • ✅ User gets immediate feedback (150ms vs 1600ms)
  • ✅ Mobile app doesn't timeout
  • ✅ Better user experience
  • ✅ Server can handle more concurrent requests

Pattern 2: Document Upload Processing

User uploads documents, gets immediate confirmation, OCR and validation happen in background.

<bpmn:startEvent id="StartDocumentUpload" name="Start"/>

<!-- Save document metadata (fast) -->
<bpmn:scriptTask id="SaveMetadata" name="Save Metadata">
<bpmn:script>
var document = context.document;

// Save metadata only (fast operation)
var metadata = BankLingo.ExecuteCommand('SaveDocumentMetadata', {
customerId: document.customerId,
documentType: document.type,
fileName: document.fileName,
fileSize: document.fileSize,
uploadedAt: new Date().toISOString()
});

context.documentId = metadata.result.documentId;

logger.info('Document metadata saved: ' + context.documentId);

return {
documentId: context.documentId
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC BOUNDARY: Heavy processing in background -->
<bpmn:serviceTask id="ExtractText"
name="Extract Text (OCR)"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="OCRExtractionCommand"/>
<custom:property name="documentId" value="${context.documentId}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Validate extracted data -->
<bpmn:serviceTask id="ValidateData" name="Validate Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="DataValidationCommand"/>
<custom:property name="documentId" value="${context.documentId}"/>
<custom:property name="extractedText" value="${context.extractedText}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Gateway: Validation passed? -->
<bpmn:exclusiveGateway id="ValidationPassed" name="Valid?"/>

<!-- Success: Update status -->
<bpmn:sequenceFlow sourceRef="ValidationPassed" targetRef="UpdateStatusValid">
<bpmn:conditionExpression>${context.validationPassed === true}</bpmn:conditionExpression>
</bpmn:sequenceFlow>

<!-- Failure: Manual review -->
<bpmn:sequenceFlow sourceRef="ValidationPassed" targetRef="CreateReviewTask">
<bpmn:conditionExpression>${context.validationPassed === false}</bpmn:conditionExpression>
</bpmn:sequenceFlow>

<bpmn:scriptTask id="UpdateStatusValid" name="Update Status">
<bpmn:script>
BankLingo.ExecuteCommand('UpdateDocumentStatus', {
documentId: context.documentId,
status: 'VERIFIED',
validatedAt: new Date().toISOString()
});

logger.info('Document verified: ' + context.documentId);
</bpmn:script>
</bpmn:scriptTask>

<bpmn:userTask id="CreateReviewTask" name="Manual Review">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="document-review-form"/>
<custom:property name="ResponsibleRole" value="DOCUMENT_REVIEWER"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

<bpmn:endEvent id="End" name="End"/>

API Response (immediate):

{
"documentId": "DOC-123",
"status": "PROCESSING",
"message": "Document uploaded successfully. Processing will complete shortly."
}

Use Cases:

  • Document uploads
  • Image processing
  • OCR extraction
  • Data validation

Pattern 3: Account Opening Workflow

Customer opens account online, gets immediate confirmation, setup happens in background.

<bpmn:startEvent id="StartAccountOpening" name="Start"/>

<!-- Create account record (fast) -->
<bpmn:serviceTask id="CreateAccountRecord" name="Create Account">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CreateAccountCommand"/>
<custom:property name="customerId" value="${context.customerId}"/>
<custom:property name="accountType" value="${context.accountType}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<bpmn:scriptTask id="GenerateAccountNumber" name="Generate Account Number">
<bpmn:script>
// Generate account number (fast)
var accountNumber = BankLingo.ExecuteCommand('GenerateAccountNumber', {
accountType: context.accountType,
branchCode: context.branchCode
});

context.accountNumber = accountNumber.result.accountNumber;

logger.info('Account number generated: ' + context.accountNumber);

return {
accountNumber: context.accountNumber
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC BOUNDARY: Complex setup in background -->
<bpmn:serviceTask id="SetupOnlineBanking"
name="Setup Online Banking"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="SetupOnlineBankingCommand"/>
<custom:property name="accountId" value="${context.accountId}"/>
<custom:property name="customerId" value="${context.customerId}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Order debit card -->
<bpmn:serviceTask id="OrderDebitCard" name="Order Debit Card">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="OrderDebitCardCommand"/>
<custom:property name="accountId" value="${context.accountId}"/>
<custom:property name="customerId" value="${context.customerId}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Setup direct deposit -->
<bpmn:serviceTask id="SetupDirectDeposit" name="Setup Direct Deposit">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="SetupDirectDepositCommand"/>
<custom:property name="accountNumber" value="${context.accountNumber}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Send welcome package -->
<bpmn:sendTask id="SendWelcomePackage" name="Send Welcome Package">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="${context.customerEmail}"/>
<custom:property name="template" value="welcome-package"/>
<custom:property name="accountNumber" value="${context.accountNumber}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="End" name="End"/>

API Response (immediate):

{
"accountId": "ACC-123",
"accountNumber": "1234567890",
"status": "ACTIVE",
"message": "Your account has been created! Setup is in progress."
}

Background Processing:

  • Online banking credentials created
  • Debit card ordered and shipped
  • Direct deposit routing configured
  • Welcome email sent with account details

Pattern 4: Bulk Transaction Import

User uploads transaction file, gets immediate confirmation, import happens in background.

<bpmn:startEvent id="StartBulkImport" name="Start"/>

<!-- Validate file format (quick) -->
<bpmn:scriptTask id="ValidateFileFormat" name="Validate Format">
<bpmn:script>
var file = context.uploadedFile;

// Quick format check
var isValid = file.fileType === 'csv' || file.fileType === 'xlsx';
var maxSize = 10 * 1024 * 1024; // 10 MB

if (!isValid) {
throw new BpmnError('INVALID_FORMAT', 'Only CSV and Excel files are supported');
}

if (file.fileSize > maxSize) {
throw new BpmnError('FILE_TOO_LARGE', 'Maximum file size is 10 MB');
}

// Save file reference
context.importFileId = file.fileId;
context.importFileName = file.fileName;
context.importStartTime = new Date().toISOString();

logger.info('File validation passed: ' + file.fileName);

return {
fileId: file.fileId,
fileName: file.fileName
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC BOUNDARY: File processing in background -->
<bpmn:serviceTask id="ParseFile"
name="Parse File"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ParseTransactionFileCommand"/>
<custom:property name="fileId" value="${context.importFileId}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Validate transactions -->
<bpmn:scriptTask id="ValidateTransactions" name="Validate Transactions">
<bpmn:script>
var transactions = context.parsedTransactions;

var validTransactions = [];
var invalidTransactions = [];

transactions.forEach(function(txn) {
if (txn.amount > 0 && txn.accountNumber && txn.date) {
validTransactions.push(txn);
} else {
invalidTransactions.push({
transaction: txn,
errors: validateTransaction(txn)
});
}
});

context.validTransactions = validTransactions;
context.invalidTransactions = invalidTransactions;
context.validCount = validTransactions.length;
context.invalidCount = invalidTransactions.length;

logger.info('Validation: ' + validTransactions.length + ' valid, ' +
invalidTransactions.length + ' invalid');

return {
validCount: validTransactions.length,
invalidCount: invalidTransactions.length
};

function validateTransaction(txn) {
var errors = [];
if (!txn.amount || txn.amount <= 0) errors.push('Invalid amount');
if (!txn.accountNumber) errors.push('Missing account number');
if (!txn.date) errors.push('Missing date');
return errors;
}
</bpmn:script>
</bpmn:scriptTask>

<!-- Process valid transactions (multi-instance) -->
<bpmn:callActivity id="ProcessTransactions"
name="Process Transactions"
calledElement="ProcessSingleTransaction">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.validTransactions"/>
<custom:property name="elementVariable" value="currentTransaction"/>
<custom:property name="aggregationVariable" value="processingResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>

<!-- Generate import report -->
<bpmn:scriptTask id="GenerateImportReport" name="Generate Report">
<bpmn:script>
var results = context.processingResults;

var successful = results.filter(r => r.success).length;
var failed = results.filter(r => !r.success).length;

var report = BankLingo.ExecuteCommand('GenerateImportReport', {
fileId: context.importFileId,
fileName: context.importFileName,
totalCount: context.validCount + context.invalidCount,
validCount: context.validCount,
invalidCount: context.invalidCount,
successfulCount: successful,
failedCount: failed,
startTime: context.importStartTime,
endTime: new Date().toISOString()
});

context.reportId = report.result.reportId;

logger.info('Import complete: ' + successful + ' successful, ' +
failed + ' failed');

return {
reportId: context.reportId
};
</bpmn:script>
</bpmn:scriptTask>

<!-- Send report to user -->
<bpmn:sendTask id="SendReport" name="Send Report">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="${context.userEmail}"/>
<custom:property name="subject" value="Import Complete - ${context.importFileName}"/>
<custom:property name="body" value="Your transaction import is complete. Successful: ${context.successfulCount}, Failed: ${context.failedCount}"/>
<custom:property name="attachmentId" value="${context.reportId}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="End" name="End"/>

API Response (immediate):

{
"importId": "IMP-123",
"fileName": "transactions.csv",
"status": "PROCESSING",
"message": "File uploaded successfully. You'll receive an email when processing is complete."
}

Multiple Async Boundaries

You can have multiple async boundaries in a single process:

<bpmn:startEvent id="Start"/>

<!-- Synchronous initial tasks -->
<bpmn:scriptTask id="Task1" name="Task 1"/>
<bpmn:scriptTask id="Task2" name="Task 2"/>

<!-- First async boundary -->
<bpmn:serviceTask id="Task3" name="Task 3" camunda:asyncBefore="true"/>
<bpmn:serviceTask id="Task4" name="Task 4"/>

<!-- Second async boundary -->
<bpmn:serviceTask id="Task5" name="Task 5" camunda:asyncBefore="true"/>
<bpmn:serviceTask id="Task6" name="Task 6"/>

<bpmn:endEvent id="End"/>

Execution:

  1. Task1, Task2 execute synchronously
  2. API returns after Task2
  3. Task3, Task4 execute in background
  4. Task5, Task6 execute in background (second async segment)

When to Use Async Boundaries

✅ Use Async Boundaries For

  • Long-running operations:

    <bpmn:serviceTask id="GeneratePDF" camunda:asyncBefore="true"/>
  • External API calls:

    <bpmn:serviceTask id="CallPartnerAPI" camunda:asyncBefore="true"/>
  • User tasks (always async for mobile):

    <bpmn:userTask id="ManagerApproval" camunda:asyncBefore="true"/>
  • Batch processing:

    <bpmn:callActivity id="ProcessBatch" calledElement="ProcessItem" 
    camunda:asyncBefore="true">
    <bpmn:multiInstanceLoopCharacteristics isSequential="false"/>
    </bpmn:callActivity>
  • Document processing:

    <bpmn:serviceTask id="OCRExtraction" camunda:asyncBefore="true"/>

❌ Don't Use Async Boundaries For

  • Fast operations (< 100ms):

    <!-- ❌ Not needed for quick validations -->
    <bpmn:scriptTask id="ValidateInput" camunda:asyncBefore="true"/>
  • Database reads (usually fast):

    <!-- ❌ Database queries are typically fast -->
    <bpmn:serviceTask id="GetCustomer" camunda:asyncBefore="true"/>
  • Simple calculations:

    <!-- ❌ Mathematical operations are instant -->
    <bpmn:scriptTask id="CalculateTotal" camunda:asyncBefore="true"/>

Performance Considerations

Response Time Impact

Without Async:
Request → [Validation: 50ms] → [Processing: 2000ms] → Response: 2050ms

With Async:
Request → [Validation: 50ms] → Response: 50ms
Background → [Processing: 2000ms]

Improvement: 40x faster response time (2050ms → 50ms)

Throughput Impact

Synchronous:
- 1 request takes 2 seconds
- Server can handle 30 req/min (with 1 worker)

Asynchronous:
- 1 request takes 50ms (response)
- Server can handle 1200 req/min (with 1 worker)

Improvement: 40x more requests per minute

Best Practices

✅ Do This

<!-- ✅ Use async for user tasks -->
<bpmn:userTask id="Approval" camunda:asyncBefore="true"/>

<!-- ✅ Use async for long operations -->
<bpmn:serviceTask id="GenerateReport" camunda:asyncBefore="true"/>

<!-- ✅ Keep fast operations synchronous -->
<bpmn:scriptTask id="ValidateInput" name="Validate"/>
<!-- Then async boundary after validation -->
<bpmn:userTask id="Process" camunda:asyncBefore="true"/>

<!-- ✅ Return meaningful response -->
return {
processInstanceId: processId,
status: 'PROCESSING',
message: 'Your request is being processed'
};

❌ Don't Do This

<!-- ❌ Don't make first task async if validation needed -->
<bpmn:startEvent id="Start"/>
<bpmn:scriptTask id="Validate" camunda:asyncBefore="true"/>
<!-- User gets response before validation! -->

<!-- ❌ Don't use async for everything -->
<bpmn:scriptTask id="Step1" camunda:asyncBefore="true"/>
<bpmn:scriptTask id="Step2" camunda:asyncBefore="true"/>
<bpmn:scriptTask id="Step3" camunda:asyncBefore="true"/>
<!-- Unnecessary overhead -->

<!-- ❌ Don't forget error handling -->
<bpmn:serviceTask id="Task" camunda:asyncBefore="true"/>
<!-- Add error boundaries! -->

Error Handling with Async Boundaries

Errors in async tasks don't reach the API caller:

<bpmn:serviceTask id="ProcessAsync" 
name="Process"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Catch errors in async segment -->
<bpmn:boundaryEvent id="ProcessError"
attachedToRef="ProcessAsync"
cancelActivity="true">
<bpmn:errorEventDefinition/>
</bpmn:boundaryEvent>

<!-- Notify user of error -->
<bpmn:sendTask id="NotifyError" name="Notify Error">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="${context.userEmail}"/>
<custom:property name="subject" value="Processing Failed"/>
<custom:property name="body" value="An error occurred: ${context._lastError.message}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

Features Used:

  • Phase 2: Async Boundaries

Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026