Multi-Instance Subprocess (Phase 1)
Multi-Instance Subprocess enables parallel or sequential processing of collections, allowing a subprocess to execute multiple times with different data.
Overview
Multi-instance provides:
- ✅ Parallel processing of collections (process all items simultaneously)
- ✅ Sequential processing of collections (process items one at a time)
- ✅ Result aggregation from all instances
- ✅ Batch operation support for large datasets
- ✅ Dynamic iteration over runtime data
Basic Concepts
Multi-Instance Properties
| Property | Type | Description | Example |
|---|---|---|---|
IsMultiInstance | boolean | Enables multi-instance execution | true |
IsSequential | boolean | Sequential (true) vs parallel (false) | false |
Collection | string | Context variable containing array | context.loanApplications |
ElementVariable | string | Variable name for current item | currentApplication |
AggregationVariable | string | Variable to collect results | approvalResults |
How It Works
- Collection: Specify array of items to process
- Iteration: Subprocess executes once per item
- Context: Each instance gets current item in
ElementVariable - Aggregation: Results collected in
AggregationVariable - Completion: Parent process continues when all instances complete
Parallel Multi-Instance
Process all items simultaneously for maximum performance.
Example: Parallel Loan Approval
<bpmn:callActivity id="ApproveLoanApplications"
name="Approve Loan Applications"
calledElement="ApproveSingleLoan">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.loanApplications"/>
<custom:property name="elementVariable" value="currentApplication"/>
<custom:property name="aggregationVariable" value="approvalResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
Subprocess (ApproveSingleLoan):
<bpmn:startEvent id="SubprocessStart" name="Start"/>
<!-- Access current item -->
<bpmn:scriptTask id="ValidateApplication" name="Validate Application">
<bpmn:script>
// Current item available in elementVariable
var application = context.currentApplication;
logger.info('Validating loan application ' + application.id);
// Validation logic
var isValid = application.amount > 0 &&
application.amount <= 1000000 &&
application.customerId;
context.validationPassed = isValid;
if (!isValid) {
throw new BpmnError('VALIDATION_ERROR',
'Invalid application: ' + application.id);
}
return {
applicationId: application.id,
isValid: isValid
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Credit check -->
<bpmn:serviceTask id="PerformCreditCheck" name="Credit Check">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CreditCheckCommand"/>
<custom:property name="customerId" value="${context.currentApplication.customerId}"/>
<custom:property name="amount" value="${context.currentApplication.amount}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Auto-approve based on credit score -->
<bpmn:scriptTask id="DetermineApproval" name="Determine Approval">
<bpmn:script>
var creditScore = context.creditScore;
var amount = context.currentApplication.amount;
// Auto-approve logic
var approved = false;
if (creditScore >= 700 && amount <= 100000) {
approved = true;
} else if (creditScore >= 650 && amount <= 50000) {
approved = true;
}
context.approved = approved;
context.approvalReason = approved ?
'AUTO_APPROVED_CREDIT_SCORE_' + creditScore :
'REQUIRES_MANUAL_REVIEW';
logger.info('Application ' + context.currentApplication.id +
': ' + context.approvalReason);
// Return result for aggregation
return {
applicationId: context.currentApplication.id,
customerId: context.currentApplication.customerId,
amount: context.currentApplication.amount,
creditScore: creditScore,
approved: approved,
reason: context.approvalReason,
processedAt: new Date().toISOString()
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="SubprocessEnd" name="End"/>
Parent Process After Multi-Instance:
<!-- All instances complete, aggregated results available -->
<bpmn:scriptTask id="ProcessResults" name="Process Results">
<bpmn:script>
// approvalResults contains array of results from all instances
var results = context.approvalResults;
var totalProcessed = results.length;
var approvedCount = results.filter(r => r.approved).length;
var rejectedCount = totalProcessed - approvedCount;
var totalAmount = results.filter(r => r.approved)
.reduce((sum, r) => sum + r.amount, 0);
context.totalProcessed = totalProcessed;
context.approvedCount = approvedCount;
context.rejectedCount = rejectedCount;
context.totalApprovedAmount = totalAmount;
logger.info('Batch approval complete: ' + approvedCount + ' approved, ' +
rejectedCount + ' rejected, total: $' + totalAmount);
return {
totalProcessed: totalProcessed,
approved: approvedCount,
rejected: rejectedCount,
totalAmount: totalAmount
};
</bpmn:script>
</bpmn:scriptTask>
Performance:
- ✅ All 100 applications processed simultaneously
- ✅ Completes in time of longest instance (not sum of all)
- ✅ Efficient for I/O-bound operations (API calls, database queries)
Sequential Multi-Instance
Process items one at a time, useful for resource-limited operations or when order matters.
Example: Sequential Document Generation
<bpmn:callActivity id="GenerateDocuments"
name="Generate Documents"
calledElement="GenerateSingleDocument">
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<custom:property name="collection" value="context.customers"/>
<custom:property name="elementVariable" value="currentCustomer"/>
<custom:property name="aggregationVariable" value="generatedDocuments"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
Subprocess (GenerateSingleDocument):
<bpmn:startEvent id="SubprocessStart" name="Start"/>
<bpmn:scriptTask id="GenerateStatement" name="Generate Statement">
<bpmn:script>
var customer = context.currentCustomer;
logger.info('Generating statement for customer ' + customer.id);
// Generate PDF (resource-intensive)
var document = BankLingo.ExecuteCommand('GenerateCustomerStatement', {
customerId: customer.id,
accountId: customer.accountId,
startDate: context.startDate,
endDate: context.endDate
});
context.documentId = document.result.documentId;
context.documentUrl = document.result.downloadUrl;
return {
customerId: customer.id,
documentId: document.result.documentId,
documentUrl: document.result.downloadUrl,
generatedAt: new Date().toISOString()
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="SubprocessEnd" name="End"/>
Why Sequential:
- ✅ PDF generation is CPU-intensive
- ✅ Prevents server overload
- ✅ Controlled resource usage
- ✅ Predictable completion order
Common Patterns
Pattern 1: Batch Approval Workflow
Process multiple approvals with aggregated results:
<bpmn:scriptTask id="GetPendingApprovals" name="Get Pending Approvals">
<bpmn:script>
// Get all pending approvals
var approvals = BankLingo.ExecuteCommand('GetPendingApprovals', {
status: 'PENDING',
type: 'TRANSACTION'
});
context.pendingApprovals = approvals.result;
context.approvalCount = approvals.result.length;
logger.info('Found ' + context.approvalCount + ' pending approvals');
return {
count: context.approvalCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Check if has approvals -->
<bpmn:exclusiveGateway id="HasApprovals" name="Has Approvals?"/>
<bpmn:sequenceFlow sourceRef="HasApprovals" targetRef="ProcessApprovals">
<bpmn:conditionExpression>${context.approvalCount > 0}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Process all approvals in parallel -->
<bpmn:callActivity id="ProcessApprovals"
name="Process Approvals"
calledElement="ProcessSingleApproval">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.pendingApprovals"/>
<custom:property name="elementVariable" value="currentApproval"/>
<custom:property name="aggregationVariable" value="approvalResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Aggregate and report -->
<bpmn:scriptTask id="GenerateApprovalReport" name="Generate Report">
<bpmn:script>
var results = context.approvalResults;
var approved = results.filter(r => r.decision === 'APPROVED').length;
var rejected = results.filter(r => r.decision === 'REJECTED').length;
var pending = results.filter(r => r.decision === 'PENDING').length;
logger.info('Approval batch: ' + approved + ' approved, ' +
rejected + ' rejected, ' + pending + ' still pending');
return {
approved: approved,
rejected: rejected,
pending: pending
};
</bpmn:script>
</bpmn:scriptTask>
Pattern 2: Customer Notification Broadcast
Send notifications to multiple customers:
<bpmn:scriptTask id="PrepareNotifications" name="Prepare Notifications">
<bpmn:script>
// Get customers for notification
var customers = BankLingo.ExecuteCommand('GetCustomersForNotification', {
segment: context.targetSegment,
channel: 'EMAIL'
});
context.customers = customers.result.map(function(c) {
return {
id: c.id,
email: c.email,
name: c.fullName,
accountNumber: c.accountNumber
};
});
context.notificationCount = context.customers.length;
logger.info('Sending notifications to ' + context.notificationCount + ' customers');
return {
recipientCount: context.notificationCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Send notifications in parallel -->
<bpmn:callActivity id="SendNotifications"
name="Send Notifications"
calledElement="SendSingleNotification">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.customers"/>
<custom:property name="elementVariable" value="currentCustomer"/>
<custom:property name="aggregationVariable" value="sendResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Track delivery -->
<bpmn:scriptTask id="TrackDelivery" name="Track Delivery">
<bpmn:script>
var results = context.sendResults;
var delivered = results.filter(r => r.status === 'DELIVERED').length;
var failed = results.filter(r => r.status === 'FAILED').length;
var bounced = results.filter(r => r.status === 'BOUNCED').length;
context.deliveredCount = delivered;
context.failedCount = failed;
context.bouncedCount = bounced;
context.deliveryRate = (delivered / results.length * 100).toFixed(2);
logger.info('Notification delivery: ' + delivered + ' delivered (' +
context.deliveryRate + '%), ' + failed + ' failed, ' +
bounced + ' bounced');
return {
delivered: delivered,
failed: failed,
bounced: bounced,
deliveryRate: context.deliveryRate
};
</bpmn:script>
</bpmn:scriptTask>
Pattern 3: Account Balance Updates
Update multiple account balances (sequential for data consistency):
<bpmn:scriptTask id="GetAccountsToUpdate" name="Get Accounts">
<bpmn:script>
// Get accounts needing balance update
var accounts = BankLingo.ExecuteCommand('GetAccountsForUpdate', {
updateType: 'INTEREST_POSTING',
period: context.period
});
context.accounts = accounts.result;
context.accountCount = accounts.result.length;
logger.info('Updating ' + context.accountCount + ' account balances');
return {
accountCount: context.accountCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Update accounts SEQUENTIALLY for consistency -->
<bpmn:callActivity id="UpdateAccountBalances"
name="Update Account Balances"
calledElement="UpdateSingleAccountBalance">
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<custom:property name="collection" value="context.accounts"/>
<custom:property name="elementVariable" value="currentAccount"/>
<custom:property name="aggregationVariable" value="updateResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Verify updates -->
<bpmn:scriptTask id="VerifyUpdates" name="Verify Updates">
<bpmn:script>
var results = context.updateResults;
var successful = results.filter(r => r.success).length;
var failed = results.filter(r => !r.success).length;
var totalPosted = results.filter(r => r.success)
.reduce((sum, r) => sum + r.amountPosted, 0);
context.successCount = successful;
context.failedCount = failed;
context.totalPosted = totalPosted;
logger.info('Balance updates: ' + successful + ' successful, ' +
failed + ' failed, total posted: $' + totalPosted);
if (failed > 0) {
logger.warn('Failed accounts: ' +
results.filter(r => !r.success)
.map(r => r.accountId)
.join(', '));
}
return {
successful: successful,
failed: failed,
totalPosted: totalPosted
};
</bpmn:script>
</bpmn:scriptTask>
Pattern 4: Document Verification Workflow
Verify multiple documents with manual review for failures:
<bpmn:scriptTask id="GetDocumentsForVerification" name="Get Documents">
<bpmn:script>
var documents = BankLingo.ExecuteCommand('GetSubmittedDocuments', {
status: 'SUBMITTED',
verificationType: 'IDENTITY'
});
context.documents = documents.result;
context.documentCount = documents.result.length;
return {
documentCount: context.documentCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Verify documents in parallel -->
<bpmn:callActivity id="VerifyDocuments"
name="Verify Documents"
calledElement="VerifySingleDocument">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.documents"/>
<custom:property name="elementVariable" value="currentDocument"/>
<custom:property name="aggregationVariable" value="verificationResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Separate verified from failed -->
<bpmn:scriptTask id="SeparateResults" name="Separate Results">
<bpmn:script>
var results = context.verificationResults;
var verified = results.filter(r => r.verified);
var failed = results.filter(r => !r.verified);
context.verifiedDocuments = verified;
context.failedDocuments = failed;
context.verifiedCount = verified.length;
context.failedCount = failed.length;
logger.info('Verification: ' + verified.length + ' passed, ' +
failed.length + ' failed');
return {
verified: verified.length,
failed: failed.length
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: Any failed? -->
<bpmn:exclusiveGateway id="HasFailures" name="Has Failures?"/>
<bpmn:sequenceFlow sourceRef="HasFailures" targetRef="CreateManualReviewTasks">
<bpmn:conditionExpression>${context.failedCount > 0}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Create manual review tasks for failures -->
<bpmn:callActivity id="CreateManualReviewTasks"
name="Create Review Tasks"
calledElement="CreateReviewTask">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.failedDocuments"/>
<custom:property name="elementVariable" value="failedDocument"/>
<custom:property name="aggregationVariable" value="reviewTasks"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
Pattern 5: Data Migration Batch
Migrate data in batches with progress tracking:
<bpmn:scriptTask id="PrepareMigration" name="Prepare Migration">
<bpmn:script>
// Get records to migrate
var records = BankLingo.ExecuteCommand('GetRecordsToMigrate', {
batchSize: 1000,
offset: context.migrationOffset || 0
});
context.recordsToMigrate = records.result.records;
context.recordCount = records.result.records.length;
context.hasMoreRecords = records.result.hasMore;
context.totalRecords = records.result.totalCount;
logger.info('Migration batch: ' + context.recordCount + ' records, ' +
'offset: ' + (context.migrationOffset || 0));
return {
batchSize: context.recordCount,
totalRecords: context.totalRecords
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Migrate batch in parallel -->
<bpmn:callActivity id="MigrateRecords"
name="Migrate Records"
calledElement="MigrateSingleRecord">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.recordsToMigrate"/>
<custom:property name="elementVariable" value="currentRecord"/>
<custom:property name="aggregationVariable" value="migrationResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Track progress -->
<bpmn:scriptTask id="TrackProgress" name="Track Progress">
<bpmn:script>
var results = context.migrationResults;
var successful = results.filter(r => r.success).length;
var failed = results.filter(r => !r.success).length;
// Update cumulative counters
context.totalMigrated = (context.totalMigrated || 0) + successful;
context.totalFailed = (context.totalFailed || 0) + failed;
context.migrationOffset = (context.migrationOffset || 0) + context.recordCount;
var progress = (context.totalMigrated / context.totalRecords * 100).toFixed(2);
logger.info('Migration progress: ' + context.totalMigrated + '/' +
context.totalRecords + ' (' + progress + '%), ' +
failed + ' failed in this batch');
return {
totalMigrated: context.totalMigrated,
totalFailed: context.totalFailed,
progress: progress,
hasMore: context.hasMoreRecords
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: More records? -->
<bpmn:exclusiveGateway id="HasMoreRecords" name="Has More?"/>
<bpmn:sequenceFlow sourceRef="HasMoreRecords" targetRef="PrepareMigration">
<bpmn:conditionExpression>${context.hasMoreRecords === true}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="HasMoreRecords" targetRef="MigrationComplete">
<bpmn:conditionExpression>${context.hasMoreRecords === false}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:scriptTask id="MigrationComplete" name="Complete">
<bpmn:script>
logger.info('Migration complete: ' + context.totalMigrated + ' migrated, ' +
context.totalFailed + ' failed');
</bpmn:script>
</bpmn:scriptTask>
Error Handling in Multi-Instance
Instance-Level Error Handling
Handle errors in individual instances:
<!-- Subprocess with error handling -->
<bpmn:serviceTask id="ProcessItem" name="Process Item">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessItemCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch error in this instance -->
<bpmn:boundaryEvent id="ProcessError"
attachedToRef="ProcessItem"
cancelActivity="false">
<bpmn:errorEventDefinition/>
</bpmn:boundaryEvent>
<bpmn:scriptTask id="LogInstanceError" name="Log Error">
<bpmn:script>
// Log error but continue
logger.error('Error processing item ' + context.currentItem.id +
': ' + context._lastError.message);
// Return error result for aggregation
return {
itemId: context.currentItem.id,
success: false,
error: context._lastError.message
};
</bpmn:script>
</bpmn:scriptTask>
Behavior:
- ✅ One instance failure doesn't stop others
- ✅ Failed instances return error results
- ✅ Parent receives all results (success and failure)
- ✅ Can identify which items failed for retry
Performance Considerations
Parallel vs Sequential Decision Matrix
| Factor | Parallel | Sequential |
|---|---|---|
| Processing Time | Fast (simultaneous) | Slow (one-by-one) |
| Resource Usage | High (all at once) | Low (controlled) |
| Data Consistency | Complex | Simple |
| Error Impact | Isolated | Contained |
| Use When | I/O-bound operations | CPU-intensive or order matters |
Batch Size Guidelines
// ✅ Optimal batch sizes
// Small batch: 10-50 items
// - Complex processing
// - High resource usage per item
// - External API calls with rate limits
context.batchSize = 25;
// Medium batch: 50-500 items
// - Moderate processing
// - Database operations
// - Balanced resource usage
context.batchSize = 200;
// Large batch: 500-5000 items
// - Simple processing
// - Low resource usage per item
// - Pure data transformation
context.batchSize = 1000;
// ❌ Avoid very large batches (>5000)
// Can cause memory issues and timeouts
Pagination for Large Datasets
// ✅ Process large datasets in chunks
var pageSize = 1000;
var currentPage = context.currentPage || 0;
var result = BankLingo.ExecuteCommand('GetRecordsPageinated', {
pageSize: pageSize,
pageNumber: currentPage
});
context.recordsToProcess = result.result.records;
context.hasMorePages = result.result.hasMore;
context.currentPage = currentPage + 1;
// Process current page with multi-instance
// Then check hasMorePages and loop back if needed
Best Practices
✅ Do This
<!-- ✅ Use parallel for I/O-bound operations -->
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<!-- ✅ Use sequential for CPU-intensive operations -->
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<!-- ✅ Always specify aggregation variable -->
<custom:property name="aggregationVariable" value="results"/>
<!-- ✅ Return consistent result structure -->
return {
itemId: item.id,
success: true,
result: processedData
};
<!-- ✅ Handle errors gracefully in instances -->
try {
// process item
} catch (error) {
return { success: false, error: error.message };
}
<!-- ✅ Batch large datasets -->
if (totalRecords > 5000) {
// Process in batches of 1000
}
❌ Don't Do This
<!-- ❌ No aggregation variable -->
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<!-- Missing aggregationVariable -->
</bpmn:multiInstanceLoopCharacteristics>
<!-- ❌ Very large collections without batching -->
context.items = getAllRecords(); // 100,000 records
<!-- Should batch into smaller chunks -->
<!-- ❌ Parallel CPU-intensive operations -->
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<!-- generatePDF() - should be sequential -->
<!-- ❌ Sequential for simple operations -->
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<!-- sendEmail() - should be parallel -->
<!-- ❌ Throw errors without handling -->
throw new Error('Processing failed');
<!-- Should return error result instead -->
Related Documentation
- Call Activity - Call activity fundamentals
- Collection Processing - Advanced batch patterns
- Scheduled Tasks - Batch scheduling
- Error Recovery - Multi-instance error handling
Features Used:
- Phase 1: Multi-Instance Subprocess
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026