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:
- Task1, Task2 execute synchronously
- API returns after Task2
- Task3, Task4 execute in background
- 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>
Related Documentation
- Mobile Patterns - Mobile-optimized workflows
- User Tasks - User task async behavior
- Error Handling - Error handling in async tasks
- Callbacks - External callbacks with async
Features Used:
- Phase 2: Async Boundaries
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026