BPM Engine Enhancements: Complete Implementation Guide
Last Updated: January 10, 2026
This document outlines FIVE major enhancements to make the BPM engine production-ready:
- Multi-Instance Subprocesses - Loop through collections executing multiple tasks per item
- Async Boundaries - Return to API while continuing in background
- Subprocess Call Activities - Process-to-process communication with callbacks
- Timer Events - Wait for duration or specific time
- Message Events - Send/receive messages to/from external systems
Enhancement 1: Multi-Instance Subprocess (CORRECTED)
Your Requirement - UNDERSTOOD NOW! ✅
You want to wrap MULTIPLE TASKS in a loop, not just one task!
Process Flow:
1. Fetch all transactions (Task - executes once)
2. FOR EACH transaction, execute these 3 tasks:
┌─────────────────────────────────────â”
│ 2a. Validate recipient account │ ↠Task 1 in loop
│ 2b. Debit customer │ ↠Task 2 in loop
│ 2c. Transfer to other bank │ ↠Task 3 in loop
└─────────────────────────────────────┘
(This entire block executes 2000 times!)
3. Generate report (Task - executes once)
4. Send email (Task - executes once)
BPMN Solution: Multi-Instance Subprocess
In BPMN, you use a subprocess (not a single task) with multi-instance characteristics:
<bpmn:process id="BulkDisbursement" name="Bulk Disbursement">
<!-- Task 1: Fetch transactions (executes once) -->
<bpmn:scriptTask id="FetchTransactions" name="Fetch Pending Transactions">
<bpmn:script>
var transactions = doCmd('GetPendingDisbursements', {
status: 'Pending',
maxRecords: 2000
});
return {
transactions: transactions.items,
totalCount: transactions.items.length
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow sourceRef="FetchTransactions" targetRef="ProcessTransactionSubprocess" />
<!-- MULTI-INSTANCE SUBPROCESS: This entire subprocess executes 2000 times! -->
<bpmn:subProcess id="ProcessTransactionSubprocess" name="Process Each Transaction">
<!-- Multi-instance configuration -->
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="collectionVariable" value="transactions" />
<camunda:property name="elementVariable" value="transaction" />
<camunda:property name="indexVariable" value="currentIndex" />
<camunda:property name="resultCollectionVariable" value="processingResults" />
<camunda:property name="continueOnError" value="true" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:multiInstanceLoopCharacteristics>
<!-- Tasks inside subprocess (these execute together for each transaction) -->
<!-- Task 2a: Validate recipient account -->
<bpmn:startEvent id="SubprocessStart" name="Start" />
<bpmn:scriptTask id="ValidateAccount" name="Validate Recipient Account">
<bpmn:script>
// 'transaction' variable contains current item from collection
console.log('Processing transaction ' + (currentIndex + 1) + '/' + nrOfInstances);
var validateResult = doCmd('ValidateAccount', {
accountNumber: transaction.recipientAccount
});
if (!validateResult.isValid) {
throw new Error('Invalid recipient account: ' + transaction.recipientAccount);
}
return {
accountValid: true,
accountName: validateResult.accountName
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Task 2b: Debit customer -->
<bpmn:scriptTask id="DebitCustomer" name="Debit Customer">
<bpmn:script>
var debitResult = doCmd('DebitAccount', {
accountNumber: transaction.sourceAccount,
amount: transaction.amount,
reference: transaction.reference,
narration: 'Transfer to ' + context.accountName
});
if (!debitResult.isSuccessful) {
throw new Error('Debit failed: ' + debitResult.message);
}
return {
debitTransactionId: debitResult.transactionId,
debitTime: debitResult.timestamp
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Task 2c: Transfer to other bank -->
<bpmn:scriptTask id="TransferToBank" name="Transfer to Other Bank">
<bpmn:script>
var transferResult = doCmd('InterBankTransfer', {
beneficiaryBank: transaction.beneficiaryBank,
beneficiaryAccount: transaction.recipientAccount,
amount: transaction.amount,
reference: transaction.reference,
sourceTransactionId: context.debitTransactionId
});
if (!transferResult.isSuccessful) {
// REVERSAL: Transfer failed, credit back
var reversalResult = doCmd('CreditAccount', {
accountNumber: transaction.sourceAccount,
amount: transaction.amount,
reference: 'REVERSAL-' + transaction.reference,
narration: 'Transfer failed - reversal'
});
throw new Error('Transfer failed: ' + transferResult.message);
}
return {
transferReference: transferResult.reference,
transferStatus: 'SUCCESS',
transferTime: transferResult.timestamp
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="SubprocessEnd" name="End" />
<!-- Sequence flows inside subprocess -->
<bpmn:sequenceFlow sourceRef="SubprocessStart" targetRef="ValidateAccount" />
<bpmn:sequenceFlow sourceRef="ValidateAccount" targetRef="DebitCustomer" />
<bpmn:sequenceFlow sourceRef="DebitCustomer" targetRef="TransferToBank" />
<bpmn:sequenceFlow sourceRef="TransferToBank" targetRef="SubprocessEnd" />
</bpmn:subProcess>
<!-- After all transactions processed, continue to next task -->
<bpmn:sequenceFlow sourceRef="ProcessTransactionSubprocess" targetRef="GenerateReport" />
<!-- Task 3: Generate report (executes once after all loops complete) -->
<bpmn:scriptTask id="GenerateReport" name="Generate Report">
<bpmn:script>
// processingResults contains array of results from all 2000 iterations
var successCount = processingResults.filter(function(r) { return r.success; }).length;
var failureCount = processingResults.filter(function(r) { return !r.success; }).length;
var report = {
processedAt: new Date().toISOString(),
totalTransactions: processingResults.length,
successful: successCount,
failed: failureCount,
successRate: (successCount / processingResults.length * 100).toFixed(2) + '%',
details: processingResults
};
return { report: report };
</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow sourceRef="GenerateReport" targetRef="SendEmail" />
<!-- Task 4: Send email (executes once) -->
<bpmn:scriptTask id="SendEmail" name="Send Report Email">
<bpmn:script>
doCmd('SendMail', {
to: ['operations@bank.com'],
subject: 'Bulk Disbursement Report - ' + report.processedAt,
body: 'Total: ' + report.totalTransactions +
', Success: ' + report.successful +
', Failed: ' + report.failed +
', Success Rate: ' + report.successRate,
attachments: [{
filename: 'disbursement-report.json',
content: JSON.stringify(report, null, 2)
}]
});
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="End" name="End" />
<bpmn:sequenceFlow sourceRef="SendEmail" targetRef="End" />
</bpmn:process>
Implementation: BpmnSubProcess Class
Implementation details removed for security.
Contact support for implementation guidance.
Implementation in BpmnExecutionEngine
Implementation details removed for security.
Contact support for implementation guidance.
Enhancement 2: Async Boundaries
(Same as before - no changes)
Implementation details removed for security.
Contact support for implementation guidance.
Enhancement 3: Subprocess Call Activities
(Same as before - no changes)
Enhancement 4: Timer Events â°
Use Cases
-
Timer Intermediate Catch Event - Wait for duration
Send Reminder → Wait 24 Hours → Send Follow-up -
Timer Boundary Event - Timeout/escalation
Approval Task → If no action in 48 hours → Escalate to Manager -
Timer Start Event - Scheduled process
Every day at 2 AM → Generate EOD Report
BPMN Timer Event Class
Implementation details removed for security.
Contact support for implementation guidance.
XML Configuration Examples
Example 1: Wait for Duration
<bpmn:process id="ReminderProcess">
<bpmn:scriptTask id="SendReminder" name="Send Reminder">
<bpmn:script>
doCmd('SendSMS', {
customerId: context.customerId,
message: 'Your loan payment is due in 24 hours'
});
</bpmn:script>
</bpmn:scriptTask>
<!-- â° TIMER: Wait 24 hours -->
<bpmn:intermediateCatchEvent id="Wait24Hours" name="Wait 24 Hours">
<bpmn:timerEventDefinition>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="timerType" value="duration" />
<camunda:property name="duration" value="P1D" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:scriptTask id="SendFollowUp" name="Send Follow-up">
<bpmn:script>
doCmd('SendSMS', {
customerId: context.customerId,
message: 'Final reminder: Your loan payment is due today'
});
</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
Example 2: Timer Boundary Event (Escalation)
<bpmn:process id="ApprovalWithEscalation">
<!-- User task with timer boundary -->
<bpmn:userTask id="ManagerApproval" name="Manager Approval">
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="form" value="LoanApprovalForm" />
<camunda:property name="responsibleTeam" value="LoanManagers" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- â° BOUNDARY TIMER: If no action in 48 hours, escalate -->
<bpmn:boundaryEvent id="EscalationTimer"
name="48 Hour Timeout"
attachedToRef="ManagerApproval"
cancelActivity="false">
<bpmn:timerEventDefinition>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="timerType" value="duration" />
<camunda:property name="duration" value="P2D" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Escalation path -->
<bpmn:scriptTask id="EscalateToDirector" name="Escalate to Director">
<bpmn:script>
doCmd('SendMail', {
to: ['director@bank.com'],
subject: 'Loan Approval Pending - Requires Attention',
body: 'Loan application ' + context.applicationId + ' has been pending for 48 hours'
});
// Reassign to director
doCmd('ReassignTask', {
taskId: context.currentTaskId,
newResponsible: 'Director'
});
</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
Implementation in BpmnExecutionEngine
Implementation details removed for security.
Contact support for implementation guidance.
ProcessTimer Table
CREATE TABLE ProcessTimers (
Id INT IDENTITY PRIMARY KEY,
ProcessInstanceId NVARCHAR(50) NOT NULL,
TimerEventId NVARCHAR(50) NOT NULL,
TriggerTime DATETIME2 NOT NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Pending', -- Pending, Triggered, Cancelled
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
TriggeredAt DATETIME2 NULL
);
CREATE INDEX IX_ProcessTimers_TriggerTime ON ProcessTimers(TriggerTime);
CREATE INDEX IX_ProcessTimers_ProcessInstanceId ON ProcessTimers(ProcessInstanceId);
CREATE INDEX IX_ProcessTimers_Status ON ProcessTimers(Status);
Background Timer Service
Implementation details removed for security.
Contact support for implementation guidance.
Enhancement 5: Message Events 📨
Use Cases
-
Message Throw Event - Send message to external system
Complete Order → Send Order Confirmation Message → External System receives -
Message Catch Event - Wait for message from external system
Send Payment Request → Wait for Payment Confirmation Message → Continue -
Message Boundary Event - React to messages during task
Processing Order → If "Cancel Order" message received → Cancel Process
BPMN Message Event Class
Implementation details removed for security.
Contact support for implementation guidance.
XML Configuration Examples
Example 1: Message Throw Event (Send Message)
<bpmn:process id="OrderProcess">
<bpmn:scriptTask id="CompleteOrder" name="Complete Order">
<bpmn:script>
var order = doCmd('FinalizeOrder', {
orderId: context.orderId
});
return {
orderNumber: order.orderNumber,
totalAmount: order.totalAmount,
customerEmail: order.customerEmail
};
</bpmn:script>
</bpmn:scriptTask>
<!-- 📨 MESSAGE THROW: Send order confirmation -->
<bpmn:intermediateThrowEvent id="SendConfirmation" name="Send Order Confirmation">
<bpmn:messageEventDefinition>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="messageName" value="OrderConfirmation" />
<camunda:property name="endpoint" value="https://notification-service.com/api/send" />
<camunda:property name="httpMethod" value="POST" />
<camunda:property name="messagePayload" value='{
"type": "order_confirmation",
"orderId": "${context.orderId}",
"orderNumber": "${context.orderNumber}",
"customerEmail": "${context.customerEmail}",
"amount": ${context.totalAmount}
}' />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:messageEventDefinition>
</bpmn:intermediateThrowEvent>
<bpmn:scriptTask id="UpdateStatus" name="Update Status">
<bpmn:script>
doCmd('UpdateOrderStatus', {
orderId: context.orderId,
status: 'Confirmed',
confirmedAt: new Date().toISOString()
});
</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
Example 2: Message Catch Event (Wait for Message)
<bpmn:process id="PaymentProcess">
<bpmn:scriptTask id="SendPaymentRequest" name="Send Payment Request">
<bpmn:script>
var correlationId = generateUUID();
var result = doCmd('SendWebhook', {
url: 'https://payment-gateway.com/api/pay',
method: 'POST',
body: {
amount: context.amount,
reference: correlationId,
callbackUrl: 'https://your-api.com/webhooks/payment'
}
});
return {
paymentCorrelationId: correlationId
};
</bpmn:script>
</bpmn:scriptTask>
<!-- 📨 MESSAGE CATCH: Wait for payment confirmation -->
<bpmn:intermediateCatchEvent id="WaitForPayment" name="Wait for Payment Confirmation">
<bpmn:messageEventDefinition>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="messageName" value="PaymentConfirmation" />
<camunda:property name="correlationKey" value="${paymentCorrelationId}" />
<camunda:property name="timeoutMinutes" value="15" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:messageEventDefinition>
</bpmn:intermediateCatchEvent>
<!-- Process pauses here until webhook message arrives -->
<bpmn:scriptTask id="ProcessPayment" name="Process Payment Result">
<bpmn:script>
// Message data available in context
if (context.paymentStatus === 'success') {
console.log('Payment successful: ' + context.transactionId);
return { paymentSuccess: true };
} else {
throw new Error('Payment failed: ' + context.failureReason);
}
</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
Implementation in BpmnExecutionEngine
Implementation details removed for security.
Contact support for implementation guidance.
MessageSubscription Table
CREATE TABLE MessageSubscriptions (
Id INT IDENTITY PRIMARY KEY,
ProcessInstanceId NVARCHAR(50) NOT NULL,
MessageEventId NVARCHAR(50) NOT NULL,
MessageName NVARCHAR(100) NOT NULL,
CorrelationKey NVARCHAR(200) NULL,
MessageData NVARCHAR(MAX) NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Waiting', -- Waiting, Received, Expired
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
ReceivedAt DATETIME2 NULL,
ExpiresAt DATETIME2 NULL
);
CREATE INDEX IX_MessageSubscriptions_MessageName ON MessageSubscriptions(MessageName);
CREATE INDEX IX_MessageSubscriptions_CorrelationKey ON MessageSubscriptions(CorrelationKey);
CREATE INDEX IX_MessageSubscriptions_Status ON MessageSubscriptions(Status);
Webhook Controller for Message Catch Events
Implementation details removed for security.
Contact support for implementation guidance.
Summary: All Five Enhancements
✅ Enhancement 1: Multi-Instance Subprocess
- Purpose: Loop through collections executing multiple tasks per item
- Key Concept: Subprocess contains tasks 2a, 2b, 2c - entire subprocess executes N times
- Properties:
IsMultiInstance,CollectionVariable,ElementVariable,MultiInstanceMode
✅ Enhancement 2: Async Boundaries
- Purpose: Return API response immediately, continue in background
- Property:
AsyncBoundary=trueon task
✅ Enhancement 3: Subprocess Call Activities
- Purpose: Process-to-process communication with callbacks
- Properties:
ServiceType="Subprocess Call",SubprocessId,WaitForCompletion
✅ Enhancement 4: Timer Events
- Purpose: Wait for duration, specific time, or recurring cycles
- Types: Duration (PT30S), Date (2026-01-15T14:30:00Z), Cycle (cron)
- Use Cases: Delays, timeouts, scheduled tasks, escalations
✅ Enhancement 5: Message Events
- Purpose: Send/receive messages to/from external systems
- Types: Throw (send message), Catch (wait for message)
- Use Cases: Webhooks, external integrations, event-driven workflows
Complete Example: All Enhancements Together
<bpmn:process id="ComplexBulkProcess">
<!-- 1. Fetch data -->
<bpmn:scriptTask id="FetchData" name="Fetch Transactions">
<bpmn:script>
var transactions = doCmd('GetPendingTransactions', {});
return { transactions: transactions.items };
</bpmn:script>
</bpmn:scriptTask>
<!-- 2. Multi-instance subprocess: Process each transaction -->
<bpmn:subProcess id="ProcessTransactions" name="Process Each Transaction">
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="collectionVariable" value="transactions" />
<camunda:property name="elementVariable" value="transaction" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:multiInstanceLoopCharacteristics>
<!-- Tasks inside subprocess -->
<bpmn:scriptTask id="ValidateAccount" name="Validate Account">
<bpmn:script>
var result = doCmd('ValidateAccount', {
accountNumber: transaction.accountNumber
});
</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="DebitAccount" name="Debit Account">
<!-- Async boundary: Return to API after this -->
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="asyncBoundary" value="true" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:script>
var result = doCmd('DebitAccount', {
accountNumber: transaction.accountNumber,
amount: transaction.amount
});
</bpmn:script>
</bpmn:scriptTask>
<!-- API returns here, below continues in background -->
<!-- Call subprocess: Get credit score -->
<bpmn:scriptTask id="GetCreditScore" name="Get Credit Score">
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="serviceType" value="Subprocess Call" />
<camunda:property name="subprocessId" value="CreditScoringProcess" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>
<!-- Timer: Wait 2 hours before final check -->
<bpmn:intermediateCatchEvent id="Wait2Hours" name="Wait 2 Hours">
<bpmn:timerEventDefinition>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="timerType" value="duration" />
<camunda:property name="duration" value="PT2H" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<!-- Message throw: Send confirmation -->
<bpmn:intermediateThrowEvent id="SendConfirmation" name="Send Confirmation">
<bpmn:messageEventDefinition>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="messageName" value="TransactionConfirmation" />
<camunda:property name="endpoint" value="https://external.com/notify" />
</camunda:properties>
</bpmn:extensionElements>
</bpmn:messageEventDefinition>
</bpmn:intermediateThrowEvent>
</bpmn:subProcess>
<!-- 3. Generate report -->
<bpmn:scriptTask id="GenerateReport" name="Generate Report">
<bpmn:script>
var report = generateReport(processingResults);
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="End" name="End" />
</bpmn:process>
This process uses ALL FIVE enhancements:
- ✅ Multi-instance subprocess (loops through transactions)
- ✅ Async boundary (returns after debit)
- ✅ Subprocess call (calls credit scoring)
- ✅ Timer event (waits 2 hours)
- ✅ Message throw event (sends notification)
🚀 Production-ready BPM engine with complete BPMN 2.0 support!