Skip to main content

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

PropertyTypeDescriptionExample
IsMultiInstancebooleanEnables multi-instance executiontrue
IsSequentialbooleanSequential (true) vs parallel (false)false
CollectionstringContext variable containing arraycontext.loanApplications
ElementVariablestringVariable name for current itemcurrentApplication
AggregationVariablestringVariable to collect resultsapprovalResults

How It Works

  1. Collection: Specify array of items to process
  2. Iteration: Subprocess executes once per item
  3. Context: Each instance gets current item in ElementVariable
  4. Aggregation: Results collected in AggregationVariable
  5. 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

FactorParallelSequential
Processing TimeFast (simultaneous)Slow (one-by-one)
Resource UsageHigh (all at once)Low (controlled)
Data ConsistencyComplexSimple
Error ImpactIsolatedContained
Use WhenI/O-bound operationsCPU-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 -->

Features Used:

  • Phase 1: Multi-Instance Subprocess

Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026