Skip to main content

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 };
}

Features Used:

  • Phase 4: Timer Events
  • Phase 1: Multi-Instance Subprocess

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