Scheduled Tasks & Batch Processing
Comprehensive patterns for scheduled tasks, batch jobs, and recurring processes using timer events.
Overview
Scheduled tasks enable:
- ✅ Automated batch processing at specific times
- ✅ Recurring business processes (daily, weekly, monthly)
- ✅ Data synchronization and ETL jobs
- ✅ Report generation on schedule
- ✅ Cleanup and maintenance tasks
Pattern 1: Daily Batch Processing
Process data daily at scheduled time:
<!-- Start every day at 2 AM -->
<bpmn:startEvent id="DailyBatchStart" name="Daily Batch - 2 AM">
<bpmn:timerEventDefinition>
<bpmn:timeCycle>0 2 * * *</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:startEvent>
<!-- Prepare batch data -->
<bpmn:scriptTask id="PrepareBatchData" name="Prepare Batch Data">
<bpmn:script>
// Get yesterday's date
var yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
var batchDate = yesterday.toISOString().split('T')[0];
context.batchDate = batchDate;
context.batchStartTime = new Date().toISOString();
logger.info('Starting daily batch for ' + batchDate);
// Get data to process
var dataResult = BankLingo.ExecuteCommand('GetBatchData', {
date: batchDate,
type: 'transactions'
});
context.batchItems = dataResult.result.items;
context.batchCount = dataResult.result.items.length;
logger.info('Batch contains ' + context.batchCount + ' items');
return {
batchDate: batchDate,
itemCount: context.batchCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Check if batch has data -->
<bpmn:exclusiveGateway id="HasData" name="Has Data?"/>
<bpmn:sequenceFlow sourceRef="HasData" targetRef="ProcessBatch">
<bpmn:conditionExpression>${context.batchCount > 0}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="HasData" targetRef="NoBatchData">
<bpmn:conditionExpression>${context.batchCount === 0}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Process batch items in parallel -->
<bpmn:callActivity id="ProcessBatch"
name="Process Batch Items"
calledElement="ProcessSingleItem">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.batchItems"/>
<custom:property name="elementVariable" value="currentItem"/>
<custom:property name="aggregationVariable" value="batchResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Generate batch report -->
<bpmn:scriptTask id="GenerateBatchReport" name="Generate Report">
<bpmn:script>
var endTime = new Date().toISOString();
var startTime = new Date(context.batchStartTime);
var duration = (new Date(endTime) - startTime) / 1000; // seconds
// Calculate statistics
var successCount = context.batchResults.filter(r => r.success).length;
var errorCount = context.batchResults.filter(r => !r.success).length;
context.batchDuration = duration;
context.successCount = successCount;
context.errorCount = errorCount;
logger.info('Batch complete: ' + successCount + ' success, ' + errorCount + ' errors, ' + duration + 's');
// Generate report
var report = BankLingo.ExecuteCommand('GenerateBatchReport', {
batchDate: context.batchDate,
totalCount: context.batchCount,
successCount: successCount,
errorCount: errorCount,
duration: duration,
results: context.batchResults
});
return {
reportId: report.result.reportId
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Send report notification -->
<bpmn:sendTask id="SendReportEmail" name="Send Report Email">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="operations@bank.com"/>
<custom:property name="subject" value="Daily Batch Report - ${context.batchDate}"/>
<custom:property name="body" value="Batch processing complete. Success: ${context.successCount}, Errors: ${context.errorCount}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>
<!-- No data path -->
<bpmn:scriptTask id="NoBatchData" name="Log No Data">
<bpmn:script>
logger.info('No batch data for ' + context.batchDate);
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="BatchComplete" name="Batch Complete"/>
Use Cases:
- Transaction reconciliation
- Daily account updates
- End-of-day processing
- Data aggregation
- Report generation
Pattern 2: Weekly Reporting
Generate reports weekly:
<!-- Start every Monday at 9 AM -->
<bpmn:startEvent id="WeeklyReportStart" name="Weekly Report - Monday 9 AM">
<bpmn:timerEventDefinition>
<bpmn:timeCycle>0 9 * * 1</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:startEvent>
<bpmn:scriptTask id="CalculateWeekRange" name="Calculate Week Range">
<bpmn:script>
// Get previous week (Monday to Sunday)
var today = new Date();
var lastMonday = new Date(today);
lastMonday.setDate(today.getDate() - 7); // Last Monday
var lastSunday = new Date(lastMonday);
lastSunday.setDate(lastMonday.getDate() + 6); // Last Sunday
context.weekStartDate = lastMonday.toISOString().split('T')[0];
context.weekEndDate = lastSunday.toISOString().split('T')[0];
context.weekNumber = getWeekNumber(lastMonday);
logger.info('Generating weekly report for week ' + context.weekNumber +
' (' + context.weekStartDate + ' to ' + context.weekEndDate + ')');
return {
weekNumber: context.weekNumber,
startDate: context.weekStartDate,
endDate: context.weekEndDate
};
function getWeekNumber(date) {
var d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
var dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
return Math.ceil((((d - yearStart) / 86400000) + 1)/7);
}
</bpmn:script>
</bpmn:scriptTask>
<!-- Generate reports in parallel -->
<bpmn:scriptTask id="GetReportTypes" name="Get Report Types">
<bpmn:script>
context.reportTypes = [
{ type: 'TRANSACTIONS', name: 'Transaction Summary' },
{ type: 'LOANS', name: 'Loan Activity' },
{ type: 'DEPOSITS', name: 'Deposit Summary' },
{ type: 'CUSTOMERS', name: 'Customer Analytics' }
];
return {
reportCount: context.reportTypes.length
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:callActivity id="GenerateReports"
name="Generate Reports"
calledElement="GenerateSingleReport">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.reportTypes"/>
<custom:property name="elementVariable" value="reportType"/>
<custom:property name="aggregationVariable" value="generatedReports"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Combine and distribute reports -->
<bpmn:scriptTask id="CombineReports" name="Combine Reports">
<bpmn:script>
logger.info('Combining ' + context.generatedReports.length + ' reports');
var reportPackage = BankLingo.ExecuteCommand('CreateReportPackage', {
weekNumber: context.weekNumber,
startDate: context.weekStartDate,
endDate: context.weekEndDate,
reports: context.generatedReports
});
context.reportPackageId = reportPackage.result.packageId;
context.reportPackageUrl = reportPackage.result.downloadUrl;
return {
packageId: context.reportPackageId
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:sendTask id="DistributeReports" name="Distribute Reports">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="management@bank.com,operations@bank.com"/>
<custom:property name="subject" value="Weekly Reports - Week ${context.weekNumber}"/>
<custom:property name="body" value="Weekly reports available at: ${context.reportPackageUrl}"/>
<custom:property name="attachmentUrl" value="${context.reportPackageUrl}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>
<bpmn:endEvent id="ReportsComplete" name="Reports Complete"/>
Use Cases:
- Weekly performance reports
- Team activity summaries
- KPI dashboards
- Management reports
- Compliance reporting
Pattern 3: Monthly End-of-Month Processing
Month-end closing process:
<!-- Start on 1st of every month at midnight -->
<bpmn:startEvent id="MonthEndStart" name="Month End - 1st at Midnight">
<bpmn:timerEventDefinition>
<bpmn:timeCycle>0 0 1 * *</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:startEvent>
<bpmn:scriptTask id="GetPreviousMonth" name="Get Previous Month">
<bpmn:script>
// Get previous month details
var today = new Date();
var lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
var lastDayOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0);
context.monthYear = lastMonth.toISOString().substring(0, 7); // YYYY-MM
context.monthStartDate = lastMonth.toISOString().split('T')[0];
context.monthEndDate = lastDayOfLastMonth.toISOString().split('T')[0];
context.monthName = lastMonth.toLocaleString('default', { month: 'long' });
context.year = lastMonth.getFullYear();
logger.info('Starting month-end processing for ' + context.monthName + ' ' + context.year);
return {
month: context.monthName,
year: context.year
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Step 1: Close accounts -->
<bpmn:serviceTask id="CloseAccounts" name="Close Accounts">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CloseMonthEndAccountsCommand"/>
<custom:property name="monthYear" value="${context.monthYear}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Step 2: Calculate interest -->
<bpmn:serviceTask id="CalculateInterest" name="Calculate Interest">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CalculateMonthlyInterestCommand"/>
<custom:property name="monthYear" value="${context.monthYear}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Step 3: Post interest -->
<bpmn:serviceTask id="PostInterest" name="Post Interest">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="PostInterestCommand"/>
<custom:property name="monthYear" value="${context.monthYear}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Step 4: Generate statements -->
<bpmn:serviceTask id="GenerateStatements" name="Generate Statements">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GenerateMonthlyStatementsCommand"/>
<custom:property name="monthYear" value="${context.monthYear}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Step 5: Reconcile balances -->
<bpmn:serviceTask id="ReconcileBalances" name="Reconcile Balances">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ReconcileMonthEndBalancesCommand"/>
<custom:property name="monthYear" value="${context.monthYear}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Step 6: Generate reports -->
<bpmn:scriptTask id="GenerateMonthEndReports" name="Generate Reports">
<bpmn:script>
var reports = BankLingo.ExecuteCommand('GenerateMonthEndReports', {
monthYear: context.monthYear,
month: context.monthName,
year: context.year
});
context.reportUrls = reports.result.reportUrls;
logger.info('Generated ' + reports.result.reportCount + ' month-end reports');
return {
reportCount: reports.result.reportCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Step 7: Send notifications -->
<bpmn:sendTask id="SendMonthEndNotification" name="Send Notification">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="finance@bank.com,management@bank.com"/>
<custom:property name="subject" value="Month End Processing Complete - ${context.monthName} ${context.year}"/>
<custom:property name="body" value="Month-end processing for ${context.monthName} ${context.year} is complete. Reports available."/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>
<bpmn:endEvent id="MonthEndComplete" name="Month End Complete"/>
<!-- Error handling -->
<bpmn:boundaryEvent id="MonthEndError"
attachedToRef="CloseAccounts"
cancelActivity="true">
<bpmn:errorEventDefinition/>
</bpmn:boundaryEvent>
<bpmn:sendTask id="SendErrorAlert" name="Send Error Alert">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="finance@bank.com"/>
<custom:property name="subject" value="ALERT: Month End Processing Failed"/>
<custom:property name="body" value="Month-end processing failed. Error: ${context._lastError.message}"/>
<custom:property name="priority" value="HIGH"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>
Use Cases:
- Month-end account closing
- Interest calculations
- Statement generation
- Financial reconciliation
- Regulatory reporting
Pattern 4: Hourly Data Synchronization
Sync data with external systems hourly:
<!-- Start every hour -->
<bpmn:startEvent id="HourlySyncStart" name="Hourly Sync">
<bpmn:timerEventDefinition>
<bpmn:timeCycle>0 * * * *</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:startEvent>
<bpmn:scriptTask id="PrepareSync" name="Prepare Sync">
<bpmn:script>
// Get last sync time
var lastSync = context.lastSyncTime || new Date(Date.now() - 3600000).toISOString();
var currentTime = new Date().toISOString();
context.syncStartTime = currentTime;
context.syncFromTime = lastSync;
context.syncToTime = currentTime;
logger.info('Starting hourly sync from ' + lastSync + ' to ' + currentTime);
return {
fromTime: lastSync,
toTime: currentTime
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Get changed records -->
<bpmn:serviceTask id="GetChangedRecords" name="Get Changed Records">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GetChangedRecordsCommand"/>
<custom:property name="fromTime" value="${context.syncFromTime}"/>
<custom:property name="toTime" value="${context.syncToTime}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="ProcessChanges" name="Process Changes">
<bpmn:script>
// Result from GetChangedRecords
var changes = context.changedRecords || [];
context.changeCount = changes.length;
logger.info('Found ' + changes.length + ' changed records');
if (changes.length === 0) {
return { hasChanges: false };
}
context.recordsToSync = changes;
return {
hasChanges: true,
changeCount: changes.length
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Check if has changes -->
<bpmn:exclusiveGateway id="HasChanges" name="Has Changes?"/>
<bpmn:sequenceFlow sourceRef="HasChanges" targetRef="SyncRecords">
<bpmn:conditionExpression>${context.changeCount > 0}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="HasChanges" targetRef="NoChanges">
<bpmn:conditionExpression>${context.changeCount === 0}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Sync records to external system -->
<bpmn:callActivity id="SyncRecords"
name="Sync Records"
calledElement="SyncSingleRecord">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.recordsToSync"/>
<custom:property name="elementVariable" value="currentRecord"/>
<custom:property name="aggregationVariable" value="syncResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<bpmn:scriptTask id="UpdateSyncStatus" name="Update Sync Status">
<bpmn:script>
var successCount = context.syncResults.filter(r => r.success).length;
var errorCount = context.syncResults.filter(r => !r.success).length;
// Update last sync time
context.lastSyncTime = context.syncToTime;
logger.info('Sync complete: ' + successCount + ' success, ' + errorCount + ' errors');
// Save sync status
BankLingo.ExecuteCommand('SaveSyncStatus', {
syncTime: context.syncToTime,
recordCount: context.changeCount,
successCount: successCount,
errorCount: errorCount
});
return {
successCount: successCount,
errorCount: errorCount
};
</bpmn:script>
</bpmn:scriptTask>
<!-- No changes path -->
<bpmn:scriptTask id="NoChanges" name="Log No Changes">
<bpmn:script>
logger.info('No changes to sync');
context.lastSyncTime = context.syncToTime;
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="SyncComplete" name="Sync Complete"/>
Use Cases:
- Real-time data replication
- System-to-system sync
- Cache updates
- Search index updates
- External API synchronization
Pattern 5: Cleanup and Maintenance
Regular cleanup tasks:
<!-- Run every Sunday at 3 AM -->
<bpmn:startEvent id="WeeklyCleanupStart" name="Weekly Cleanup - Sunday 3 AM">
<bpmn:timerEventDefinition>
<bpmn:timeCycle>0 3 * * 0</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:startEvent>
<bpmn:scriptTask id="PrepareCleanup" name="Prepare Cleanup">
<bpmn:script>
context.cleanupStartTime = new Date().toISOString();
// Define cleanup tasks
context.cleanupTasks = [
{ type: 'OLD_LOGS', retentionDays: 90 },
{ type: 'TEMP_FILES', retentionDays: 7 },
{ type: 'EXPIRED_SESSIONS', retentionDays: 30 },
{ type: 'ARCHIVED_PROCESSES', retentionDays: 365 }
];
logger.info('Starting weekly cleanup');
return {
taskCount: context.cleanupTasks.length
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Execute cleanup tasks -->
<bpmn:callActivity id="ExecuteCleanup"
name="Execute Cleanup Tasks"
calledElement="ExecuteSingleCleanupTask">
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<custom:property name="collection" value="context.cleanupTasks"/>
<custom:property name="elementVariable" value="cleanupTask"/>
<custom:property name="aggregationVariable" value="cleanupResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<bpmn:scriptTask id="GenerateCleanupReport" name="Generate Report">
<bpmn:script>
var totalDeleted = 0;
var totalFreedSpace = 0;
context.cleanupResults.forEach(function(result) {
totalDeleted += result.deletedCount;
totalFreedSpace += result.freedSpaceMB;
});
context.totalDeleted = totalDeleted;
context.totalFreedSpaceMB = totalFreedSpace;
var endTime = new Date().toISOString();
var duration = (new Date(endTime) - new Date(context.cleanupStartTime)) / 1000;
logger.info('Cleanup complete: ' + totalDeleted + ' items deleted, ' +
totalFreedSpace + ' MB freed, ' + duration + 's');
return {
deletedCount: totalDeleted,
freedSpaceMB: totalFreedSpace,
durationSeconds: duration
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:sendTask id="SendCleanupReport" name="Send Report">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="operations@bank.com"/>
<custom:property name="subject" value="Weekly Cleanup Report"/>
<custom:property name="body" value="Cleanup complete: ${context.totalDeleted} items deleted, ${context.totalFreedSpaceMB} MB freed"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>
<bpmn:endEvent id="CleanupComplete" name="Cleanup Complete"/>
Use Cases:
- Log file cleanup
- Temporary file removal
- Session cleanup
- Archive old data
- Database maintenance
Best Practices
✅ Do This
<!-- ✅ Use appropriate schedule frequency -->
<!-- Hourly for critical sync -->
<bpmn:timeCycle>0 * * * *</bpmn:timeCycle>
<!-- Daily for batch jobs -->
<bpmn:timeCycle>0 2 * * *</bpmn:timeCycle>
<!-- ✅ Run during off-peak hours -->
<!-- 2 AM for intensive processing -->
<!-- ✅ Log batch execution -->
logger.info('Batch started: ' + batchDate);
<!-- ✅ Send completion notifications -->
<!-- Email on success/failure -->
<!-- ✅ Use multi-instance for parallel processing -->
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<!-- ✅ Handle empty batches gracefully -->
if (items.length === 0) {
logger.info('No items to process');
}
<!-- ✅ Track batch statistics -->
context.successCount = ...;
context.errorCount = ...;
context.duration = ...;
❌ Don't Do This
<!-- ❌ Very frequent schedules -->
<bpmn:timeCycle>R/PT1M</bpmn:timeCycle> <!-- Every minute - use event-driven instead -->
<!-- ❌ No error handling -->
<!-- Always add error boundaries and notifications -->
<!-- ❌ Processing during peak hours -->
<bpmn:timeCycle>0 12 * * *</bpmn:timeCycle> <!-- Noon - use off-peak times -->
<!-- ❌ No batch size limits -->
<!-- Always paginate or limit batch sizes -->
<!-- ❌ Sequential processing of large batches -->
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<!-- Use parallel for large batches -->
Performance Optimization
Batch Size Management
// ✅ Process in manageable chunks
var allItems = getItems(); // 10,000 items
var batchSize = 100;
var batches = [];
for (var i = 0; i < allItems.length; i += batchSize) {
batches.push(allItems.slice(i, i + batchSize));
}
context.batches = batches;
context.batchCount = batches.length;
Parallel Processing
<!-- ✅ Process multiple items in parallel -->
<bpmn:callActivity id="ProcessItems" calledElement="ProcessItem">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.items"/>
<custom:property name="elementVariable" value="item"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
Error Isolation
// ✅ Continue on individual failures
try {
processItem(item);
return { success: true, itemId: item.id };
} catch (error) {
logger.error('Failed to process item ' + item.id + ': ' + error.message);
return { success: false, itemId: item.id, error: error.message };
}
Related Documentation
- Timer Events - Timer event fundamentals
- ISO 8601 Formats - Timer format reference
- Multi-Instance - Parallel batch processing
- Error Recovery - Batch error handling
Features Used:
- Phase 4: Timer Events
- Phase 1: Multi-Instance Subprocess
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026