Skip to main content

BPM Engine Enhancements: Complete Implementation Guide

Last Updated: January 10, 2026

This document outlines FIVE major enhancements to make the BPM engine production-ready:

  1. Multi-Instance Subprocesses - Loop through collections executing multiple tasks per item
  2. Async Boundaries - Return to API while continuing in background
  3. Subprocess Call Activities - Process-to-process communication with callbacks
  4. Timer Events - Wait for duration or specific time
  5. 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

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Implementation in BpmnExecutionEngine

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Enhancement 2: Async Boundaries

(Same as before - no changes)

Code Removed

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

  1. Timer Intermediate Catch Event - Wait for duration

    Send Reminder → Wait 24 Hours → Send Follow-up
  2. Timer Boundary Event - Timeout/escalation

    Approval Task → If no action in 48 hours → Escalate to Manager
  3. Timer Start Event - Scheduled process

    Every day at 2 AM → Generate EOD Report

BPMN Timer Event Class

Code Removed

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

Code Removed

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

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Enhancement 5: Message Events 📨

Use Cases

  1. Message Throw Event - Send message to external system

    Complete Order → Send Order Confirmation Message → External System receives
  2. Message Catch Event - Wait for message from external system

    Send Payment Request → Wait for Payment Confirmation Message → Continue
  3. Message Boundary Event - React to messages during task

    Processing Order → If "Cancel Order" message received → Cancel Process

BPMN Message Event Class

Code Removed

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

Code Removed

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

Code Removed

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=true on 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:

  1. ✅ Multi-instance subprocess (loops through transactions)
  2. ✅ Async boundary (returns after debit)
  3. ✅ Subprocess call (calls credit scoring)
  4. ✅ Timer event (waits 2 hours)
  5. ✅ Message throw event (sends notification)

🚀 Production-ready BPM engine with complete BPMN 2.0 support!