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