Skip to main content

ScriptTask - Inline Code Execution

ScriptTask executes JavaScript/Node.js code directly within the process workflow. It has full access to process variables and can perform calculations, data transformations, and business logic.

Properties

Required Properties

  • Script: The JavaScript code to execute
  • TaskType: Must be "ScriptTask"
  • Name: Display name of the task

Optional Properties

  • ScriptLanguage: Language (default: "javascript")
  • ResultVariable: Variable name to store full script result
  • OutputMapping: JSON mapping to extract specific fields from result
  • InputMapping: Map process variables to script inputs
  • EntityState: State during execution
  • AsyncBefore/AsyncAfter: Async execution flags
Result Storage Strategy

Use ResultVariable to store the entire result object, or OutputMapping to extract only specific fields. If both are set, both will be applied. If neither is set, the result goes to lastScriptResult (default).

Result Handling

Three-Step Result Processing

When a ScriptTask executes, the result is processed in this order:

  1. ResultVariable: Store the full result in a named variable (if specified)
  2. OutputMapping: Extract specific fields and map them to variables (if specified)
  3. Default: Store in lastScriptResult (if neither ResultVariable nor OutputMapping is set)
// Example: Script returns complex object
return {
creditScore: 720,
riskLevel: "Medium",
analysis: {
debtToIncome: 38.5,
paymentHistory: "Good",
recommendations: ["Consider larger down payment", "Review insurance options"]
},
timestamp: new Date().toISOString()
};

Scenario 1: Full Result Storage

<custom:property name="ResultVariable" value="creditAnalysis" />
<!-- Stores entire object in creditAnalysis -->

Scenario 2: Selective Field Mapping

<custom:property name="OutputMapping" value="{
&quot;customerCreditScore&quot;: &quot;creditScore&quot;,
&quot;riskCategory&quot;: &quot;riskLevel&quot;,
&quot;debtRatio&quot;: &quot;analysis.debtToIncome&quot;
}" />
<!-- Extracts only 3 fields, ignores rest -->

Scenario 3: Both (Full + Selective)

<custom:property name="ResultVariable" value="fullCreditReport" />
<custom:property name="OutputMapping" value="{
&quot;score&quot;: &quot;creditScore&quot;,
&quot;risk&quot;: &quot;riskLevel&quot;
}" />
<!-- Stores full result in fullCreditReport AND extracts score/risk -->

BPMN XML Examples

Basic Script Task

<bpmn:scriptTask id="Task_Calculate" name="Calculate Loan Metrics">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Calculate monthly payment
const monthlyRate = annualRate / 12 / 100;
const numPayments = termMonths;

monthlyPayment = loanAmount *
(monthlyRate * Math.pow(1 + monthlyRate, numPayments)) /
(Math.pow(1 + monthlyRate, numPayments) - 1);

totalPayment = monthlyPayment * numPayments;
totalInterest = totalPayment - loanAmount;

console.log('Monthly Payment:', monthlyPayment);
" />
<custom:property name="ScriptLanguage" value="javascript" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_In</bpmn:incoming>
<bpmn:outgoing>Flow_Out</bpmn:outgoing>
</bpmn:scriptTask>

Script with Result Variable

<bpmn:scriptTask id="Task_ComputeScore" name="Calculate Credit Score">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Calculate credit score based on multiple factors
let score = 600; // Base score

// Payment history (35%)
if (onTimePayments > 95) score += 100;
else if (onTimePayments > 80) score += 50;

// Credit utilization (30%)
const utilization = creditUsed / creditLimit * 100;
if (utilization &lt; 30) score += 80;
else if (utilization &lt; 50) score += 40;
else score -= 20;

// Length of credit history (15%)
if (accountAgeMonths > 60) score += 40;
else if (accountAgeMonths > 24) score += 20;

// Cap at 850
score = Math.min(score, 850);

return score;
" />
<custom:property name="ResultVariable" value="creditScore" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

Script with Input/Output Mapping

<bpmn:scriptTask id="Task_ValidateData" name="Validate Application Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Validate input data
const errors = [];

if (!customerName || customerName.trim() === '') {
errors.push('Customer name is required');
}

if (!email || !email.includes('@')) {
errors.push('Valid email is required');
}

if (amount &lt;= 0 || amount > 1000000) {
errors.push('Loan amount must be between $1 and $1,000,000');
}

if (termMonths &lt; 12 || termMonths > 360) {
errors.push('Term must be between 12 and 360 months');
}

// Set validation results
isValid = errors.length === 0;
validationErrors = errors;
validatedAt = new Date().toISOString();
" />

<!-- Map process variables to script inputs -->
<custom:property name="InputMapping" value="{
&quot;customerName&quot;: &quot;application.customerName&quot;,
&quot;email&quot;: &quot;application.email&quot;,
&quot;amount&quot;: &quot;application.loanAmount&quot;,
&quot;termMonths&quot;: &quot;application.term&quot;
}" />

<!-- Map script outputs to process variables -->
<custom:property name="OutputMapping" value="{
&quot;validationResult&quot;: &quot;isValid&quot;,
&quot;validationErrors&quot;: &quot;errors&quot;,
&quot;validationTimestamp&quot;: &quot;validatedAt&quot;
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

Script with BankLingo Commands

<bpmn:scriptTask id="Task_EnrichData" name="Enrich Customer Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Use BankLingo commands to fetch additional data

// Get customer details
const customerDetails = doCmd('GetCustomerById', { customerId });
customerName = customerDetails.fullName;
customerSegment = customerDetails.segment;

// Get account balance
const accountInfo = doCmd('GetAccountBalance', {
accountNumber: customerDetails.primaryAccount
});
availableBalance = accountInfo.availableBalance;

// Calculate debt-to-income ratio
const monthlyIncome = customerDetails.monthlyIncome;
const existingDebts = doCmd('GetCustomerDebts', { customerId });
const totalMonthlyDebt = existingDebts.reduce((sum, debt) => sum + debt.monthlyPayment, 0);

debtToIncomeRatio = (totalMonthlyDebt / monthlyIncome) * 100;

// Set risk category
if (debtToIncomeRatio > 43) {
riskCategory = 'High';
} else if (debtToIncomeRatio > 36) {
riskCategory = 'Medium';
} else {
riskCategory = 'Low';
}

console.log('Customer enriched:', customerName, 'Risk:', riskCategory);
" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

Conditional Logic Script

<bpmn:scriptTask id="Task_DetermineApprovalPath" name="Determine Approval Requirements">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Determine which approval path to take based on business rules

let requiredApprovers = [];
let approvalLevel = 'Standard';

// High amount requires senior approval
if (loanAmount > 100000) {
requiredApprovers.push('SeniorOfficer');
approvalLevel = 'Senior';
}

// High risk requires additional approval
if (riskScore > 70 || debtToIncomeRatio > 40) {
requiredApprovers.push('RiskManager');
approvalLevel = 'Enhanced';
}

// New customers need compliance check
if (customerTenureMonths &lt; 12) {
requiredApprovers.push('ComplianceOfficer');
}

// VIP customers get fast track
if (customerSegment === 'VIP' &amp;&amp; loanAmount &lt; 50000) {
requiredApprovers = ['LoanOfficer'];
approvalLevel = 'FastTrack';
}

// Set variables for gateway routing
approvalPath = approvalLevel;
approversNeeded = requiredApprovers.join(',');
estimatedApprovalDays = requiredApprovers.length * 2;
" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

Accessing Process Variables

Scripts have direct access to all process variables:

// Read variables
const amount = loanAmount; // Direct access
const customer = customerData.name; // Nested access

// Set variables
approvalStatus = 'Approved';
processedDate = new Date().toISOString();

// Modify variables
loanAmount = loanAmount * 1.1; // Add 10% fee
customerData.verified = true;

Using BankLingo Commands

Call any BankLingo command using doCmd():

// Execute command
const result = doCmd('CommandName', {
parameter1: value1,
parameter2: value2
});

// Example: Get account balance
const balance = doCmd('GetAccountBalance', {
accountNumber: accountNumber
});
availableBalance = balance.availableBalance;

// Example: Create transaction
const transaction = doCmd('CreateTransaction', {
accountId: accountId,
amount: amount,
type: 'Debit',
description: 'Loan disbursement'
});
transactionId = transaction.id;

Use Cases

1. Data Transformation

Transform data between different formats:

<bpmn:scriptTask id="Task_Transform" name="Transform Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Transform customer data from external format to internal format
internalCustomer = {
id: externalData.customer_id,
fullName: externalData.first_name + ' ' + externalData.last_name,
email: externalData.contact.email,
phone: externalData.contact.phone,
address: {
street: externalData.address.street_address,
city: externalData.address.city,
state: externalData.address.state_code,
zip: externalData.address.postal_code
},
dateOfBirth: new Date(externalData.dob).toISOString()
};
" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

2. Complex Calculations

Perform business calculations:

<bpmn:scriptTask id="Task_PricingCalculation" name="Calculate Loan Pricing">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Base rate from market
let interestRate = baseMarketRate;

// Adjust for credit score
if (creditScore &lt; 600) interestRate += 5.0;
else if (creditScore &lt; 650) interestRate += 3.0;
else if (creditScore &lt; 700) interestRate += 1.5;
else if (creditScore >= 750) interestRate -= 0.5;

// Adjust for loan-to-value ratio
const ltvRatio = loanAmount / collateralValue * 100;
if (ltvRatio > 80) interestRate += 1.0;
else if (ltvRatio > 90) interestRate += 2.0;

// Adjust for relationship
if (customerTenureYears > 5) interestRate -= 0.25;
if (numberOfAccounts > 3) interestRate -= 0.15;

// Calculate final numbers
finalInterestRate = Math.max(interestRate, minimumRate);
monthlyPayment = calculatePayment(loanAmount, finalInterestRate, termMonths);
totalInterest = (monthlyPayment * termMonths) - loanAmount;
aprRate = calculateAPR(loanAmount, finalInterestRate, fees, termMonths);
" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

3. Validation

Validate business rules:

<bpmn:scriptTask id="Task_BusinessRules" name="Validate Business Rules">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
const violations = [];

// Rule 1: Debt-to-income ratio
if (debtToIncomeRatio > 43) {
violations.push({
rule: 'DTI_LIMIT',
message: 'Debt-to-income ratio exceeds maximum of 43%',
severity: 'Critical'
});
}

// Rule 2: Minimum credit score
if (creditScore &lt; 580) {
violations.push({
rule: 'MIN_CREDIT_SCORE',
message: 'Credit score below minimum requirement',
severity: 'Critical'
});
}

// Rule 3: Maximum loan amount
const maxLoan = annualIncome * 3;
if (loanAmount > maxLoan) {
violations.push({
rule: 'MAX_LOAN_AMOUNT',
message: 'Loan amount exceeds 3x annual income',
severity: 'High'
});
}

// Rule 4: Recent bankruptcies
if (bankruptcyInLast7Years) {
violations.push({
rule: 'BANKRUPTCY_HISTORY',
message: 'Recent bankruptcy on record',
severity: 'High'
});
}

ruleViolations = violations;
rulesPass = violations.length === 0;
criticalViolations = violations.filter(v => v.severity === 'Critical').length;
" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

4. Decision Logic

Make routing decisions:

<bpmn:scriptTask id="Task_RouteDecision" name="Determine Routing">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Complex routing decision

// Check for auto-approval eligibility
const autoApproveEligible =
creditScore >= 720 &amp;&amp;
debtToIncomeRatio &lt; 36 &amp;&amp;
loanAmount &lt;= 25000 &amp;&amp;
customerTenureMonths >= 24 &amp;&amp;
!recentDelinquencies;

if (autoApproveEligible) {
routingDecision = 'AutoApprove';
return;
}

// Check for auto-rejection
const autoRejectCriteria =
creditScore &lt; 580 ||
debtToIncomeRatio > 50 ||
recentBankruptcy ||
fraudFlagged;

if (autoRejectCriteria) {
routingDecision = 'AutoReject';
rejectionReason = 'Does not meet minimum criteria';
return;
}

// Needs manual review - determine level
if (loanAmount > 100000 || riskScore > 70) {
routingDecision = 'SeniorReview';
} else {
routingDecision = 'StandardReview';
}
" />
<custom:property name="ResultVariable" value="routingDecision" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

OutputMapping Examples

Example 1: Extract Specific Fields

When your script returns a large object but you only need a few fields:

<bpmn:scriptTask id="Task_FetchCustomerData" name="Fetch Customer Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Fetch comprehensive customer data
const customer = doCmd('GetCustomerProfile', { customerId });

// Returns large object with 50+ fields:
// { personalInfo: {...}, financials: {...}, history: {...}, etc. }
return customer;
" />

<!-- Extract only the fields needed for this workflow -->
<custom:property name="OutputMapping" value="{
&quot;customerName&quot;: &quot;personalInfo.fullName&quot;,
&quot;email&quot;: &quot;personalInfo.email&quot;,
&quot;monthlyIncome&quot;: &quot;financials.income.monthly&quot;,
&quot;creditScore&quot;: &quot;financials.creditScore&quot;
}" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_In</bpmn:incoming>
<bpmn:outgoing>Flow_Out</bpmn:outgoing>
</bpmn:scriptTask>

<!-- Subsequent tasks can access: customerName, email, monthlyIncome, creditScore -->

Example 2: Flatten Nested Structures

Use dot notation to extract deeply nested values:

<bpmn:scriptTask id="Task_AnalyzeLoan" name="Analyze Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Complex analysis returns nested result
const analysis = {
risk: {
overall: 'Medium',
factors: {
credit: { score: 720, rating: 'Good' },
income: { dti: 38.5, stability: 'High' },
collateral: { ltv: 80, value: 250000 }
}
},
recommendation: {
approved: true,
conditions: ['Verify employment', 'Appraisal required'],
interestRate: 4.25
},
timestamp: new Date().toISOString()
};

return analysis;
" />

<!-- Flatten nested data for easier access -->
<custom:property name="OutputMapping" value="{
&quot;riskLevel&quot;: &quot;risk.overall&quot;,
&quot;creditScore&quot;: &quot;risk.factors.credit.score&quot;,
&quot;debtToIncome&quot;: &quot;risk.factors.income.dti&quot;,
&quot;loanToValue&quot;: &quot;risk.factors.collateral.ltv&quot;,
&quot;isApproved&quot;: &quot;recommendation.approved&quot;,
&quot;approvedRate&quot;: &quot;recommendation.interestRate&quot;,
&quot;conditions&quot;: &quot;recommendation.conditions&quot;
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

<!-- Process variables now: riskLevel, creditScore, debtToIncome, etc. -->

Example 3: Combine Full Result + Selected Fields

Store full result for audit while extracting key fields for workflow:

<bpmn:scriptTask id="Task_CreditCheck" name="Perform Credit Check">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Call external credit bureau API
const creditReport = doCmd('GetCreditReport', { ssn, firstName, lastName });

// Returns comprehensive credit report (hundreds of data points)
return creditReport;
" />

<!-- Store full report for compliance/audit -->
<custom:property name="ResultVariable" value="fullCreditReport" />

<!-- Extract only decision-making fields for workflow -->
<custom:property name="OutputMapping" value="{
&quot;fico&quot;: &quot;scores.fico&quot;,
&quot;vantage&quot;: &quot;scores.vantage&quot;,
&quot;bankruptcies&quot;: &quot;publicRecords.bankruptcyCount&quot;,
&quot;collections&quot;: &quot;publicRecords.collectionCount&quot;,
&quot;inquiries&quot;: &quot;recentInquiries.count&quot;
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

<!-- Available: fullCreditReport (entire object) + fico, vantage, bankruptcies, etc. -->

Example 4: Array Access

Extract values from arrays using index notation:

<bpmn:scriptTask id="Task_GetLatestTransactions" name="Get Recent Transactions">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
const transactions = doCmd('GetAccountTransactions', {
accountNumber,
limit: 10
});

return {
transactions: transactions,
count: transactions.length
};
" />

<!-- Extract specific transactions -->
<custom:property name="OutputMapping" value="{
&quot;latestTransaction&quot;: &quot;transactions[0]&quot;,
&quot;secondLatest&quot;: &quot;transactions[1]&quot;,
&quot;transactionCount&quot;: &quot;count&quot;
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

Example 5: Validation Results

Extract validation outcomes without storing entire validation object:

<bpmn:scriptTask id="Task_ValidateApplication" name="Validate Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
const errors = [];
const warnings = [];

// Perform extensive validation
if (!customerName) errors.push({ field: 'customerName', message: 'Required' });
if (amount &lt; 1000) warnings.push({ field: 'amount', message: 'Below minimum recommended' });
// ... 50 more validation rules

return {
isValid: errors.length === 0,
errorCount: errors.length,
warningCount: warnings.length,
errors: errors,
warnings: warnings,
validatedAt: new Date().toISOString(),
validatedBy: 'System'
};
" />

<!-- Extract only summary fields for routing decision -->
<custom:property name="OutputMapping" value="{
&quot;validationPassed&quot;: &quot;isValid&quot;,
&quot;validationErrors&quot;: &quot;errorCount&quot;,
&quot;validationWarnings&quot;: &quot;warningCount&quot;
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

<!-- Use validationPassed in gateway condition: validationPassed === true -->

Best Practices

1. Keep Scripts Focused

Each script task should do one thing well:

<!-- Good - Single responsibility -->
<bpmn:scriptTask id="Task_Calculate" name="Calculate Metrics" />
<bpmn:scriptTask id="Task_Validate" name="Validate Results" />

<!-- Avoid - Doing too much -->
<bpmn:scriptTask id="Task_Everything" name="Calculate, Validate, and Route" />

2. Handle Errors

Use try-catch for error handling:

try {
const result = doCmd('SomeCommand', { params });
processedResult = result.data;
} catch (error) {
console.error('Command failed:', error.message);
errorOccurred = true;
errorMessage = error.message;
}

3. Log for Debugging

Use console.log for debugging:

console.log('Starting calculation with amount:', loanAmount);
const result = calculateSomething();
console.log('Calculation result:', result);

4. Use Meaningful Variable Names

// Good
const monthlyPayment = calculatePayment(principal, rate, term);
const annualPercentageRate = calculateAPR(amount, rate, fees);

// Avoid
const mp = calc(p, r, t);
const apr = calcAPR(a, r, f);

5. Comment Complex Logic

// Calculate effective interest rate
// Formula: ((1 + r/n)^n) - 1
// Where r = nominal rate, n = compounding periods
const effectiveRate = Math.pow(1 + (nominalRate / compoundingPeriods), compoundingPeriods) - 1;

Performance Considerations

  • Keep scripts lightweight: Complex calculations should use ServiceTask to call APIs
  • Avoid loops over large datasets: Use BankLingo commands for bulk operations
  • Cache repeated calculations: Store results in variables
  • Use async for long operations: Set AsyncAfter="true" for better performance

Error Handling (Phase 5)

Throwing BpmnError from Scripts

ScriptTask supports throwing BpmnError to trigger boundary error events for controlled error handling:

<bpmn:scriptTask id="ValidateApplication" name="Validate Application">
<bpmn:script>
// Validate credit score
if (!context.creditScore || context.creditScore < 300) {
throw new BpmnError('INVALID_CREDIT_SCORE',
'Credit score is missing or below minimum threshold');
}

// Validate loan amount
if (context.loanAmount > 1000000) {
throw new BpmnError('AMOUNT_EXCEEDS_LIMIT',
'Loan amount exceeds $1M limit');
}

// Validate income
const debtToIncomeRatio = context.monthlyDebt / context.monthlyIncome;
if (debtToIncomeRatio > 0.43) {
throw new BpmnError('HIGH_DEBT_TO_INCOME',
'Debt-to-income ratio exceeds 43%');
}

return {
validated: true,
validationDate: new Date().toISOString()
};
</bpmn:script>
</bpmn:scriptTask>

<!-- Catch validation errors -->
<bpmn:boundaryEvent id="ValidationError"
attachedToRef="ValidateApplication"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>

<!-- Handle error -->
<bpmn:scriptTask id="HandleValidationError" name="Handle Validation Error">
<bpmn:script>
var error = context._lastError;

logger.warn('Validation failed: ' + error.errorCode + ' - ' + error.errorMessage);

// Send rejection notification
context.rejectionReason = error.errorMessage;
context.rejectionCode = error.errorCode;

return {
handled: true,
action: 'SEND_REJECTION'
};
</bpmn:script>
</bpmn:scriptTask>

<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />

BpmnError Constructor

// Syntax
throw new BpmnError(errorCode, errorMessage);

// Parameters:
// - errorCode (string): Error identifier (e.g., 'VALIDATION_ERROR')
// - errorMessage (string): Human-readable error description

Common Error Codes

Error CodeUse CaseExample
VALIDATION_ERRORData validation failuresInvalid email format
BUSINESS_RULE_VIOLATIONBusiness logic violationsInsufficient balance
MISSING_DATARequired data not foundCustomer ID missing
CALCULATION_ERRORMath/calculation failuresDivide by zero
CONFIGURATION_ERRORSetup/config issuesMissing API key
EXTERNAL_SYSTEM_ERRORExternal dependency failuresDatabase timeout
AUTHORIZATION_ERRORPermission issuesUser not authorized
DUPLICATE_ERRORDuplicate detectionAccount already exists
TIMEOUT_ERROROperation timeoutProcess took too long
UNKNOWN_ERRORUnexpected errorsCatch-all

Accessing Error Context

Access error information in subsequent tasks:

// Access the last error
var lastError = context._lastError;

if (lastError) {
console.log('Error Code:', lastError.errorCode); // e.g., 'VALIDATION_ERROR'
console.log('Error Message:', lastError.errorMessage); // e.g., 'Credit score too low'
console.log('Error Timestamp:', lastError.timestamp); // ISO 8601 timestamp
console.log('Error Task:', lastError.taskId); // Task that threw error
}

// Access error history
var errorHistory = context._errorHistory || [];
console.log('Total errors:', errorHistory.length);

errorHistory.forEach(function(err) {
console.log('Previous error:', err.errorCode, '-', err.errorMessage);
});

Pattern: Retry with Error Tracking

<bpmn:scriptTask id="CallExternalAPI" name="Call External API">
<bpmn:script>
var maxRetries = 3;
var currentRetry = context.apiRetryCount || 0;

try {
// Attempt API call
var response = BankLingo.ExecuteCommand('CallPartnerAPI', {
endpoint: context.apiEndpoint,
data: context.requestData
});

// Success - clear retry count
context.apiRetryCount = 0;
return response;

} catch (error) {
currentRetry++;
context.apiRetryCount = currentRetry;

if (currentRetry >= maxRetries) {
// Max retries reached - throw BpmnError
throw new BpmnError('API_RETRY_EXHAUSTED',
'Failed after ' + maxRetries + ' attempts: ' + error.message);
} else {
// Retry - throw temporary error
throw new BpmnError('API_RETRY_NEEDED',
'Retry ' + currentRetry + ' of ' + maxRetries);
}
}
</bpmn:script>
</bpmn:scriptTask>

<!-- Non-interrupting boundary for retries -->
<bpmn:boundaryEvent id="RetryNeeded"
attachedToRef="CallExternalAPI"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="RetryError" />
</bpmn:boundaryEvent>

<!-- Interrupting boundary for exhausted retries -->
<bpmn:boundaryEvent id="RetriesExhausted"
attachedToRef="CallExternalAPI"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ExhaustedError" />
</bpmn:boundaryEvent>

<bpmn:error id="RetryError" errorCode="API_RETRY_NEEDED" />
<bpmn:error id="ExhaustedError" errorCode="API_RETRY_EXHAUSTED" />

Pattern: Validation Chain

<bpmn:scriptTask id="ValidateAllFields" name="Validate All Fields">
<bpmn:script>
var validationErrors = [];

// Validate multiple fields
if (!context.customerName || context.customerName.trim().length === 0) {
validationErrors.push('Customer name is required');
}

if (!context.email || !context.email.includes('@')) {
validationErrors.push('Valid email is required');
}

if (!context.phoneNumber || context.phoneNumber.length < 10) {
validationErrors.push('Valid phone number is required');
}

if (context.loanAmount < 1000 || context.loanAmount > 500000) {
validationErrors.push('Loan amount must be between $1,000 and $500,000');
}

// If any errors, throw with all messages
if (validationErrors.length > 0) {
throw new BpmnError('VALIDATION_ERROR',
validationErrors.join('; '));
}

return {
validated: true,
fieldCount: 4
};
</bpmn:script>
</bpmn:scriptTask>

<!-- Handle validation errors -->
<bpmn:boundaryEvent id="ValidationFailed"
attachedToRef="ValidateAllFields"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="SendValidationErrors" name="Send Validation Errors">
<bpmn:script>
var error = context._lastError;

// Send detailed error message back to user
BankLingo.ExecuteCommand('SendEmail', {
to: context.email,
subject: 'Application Validation Failed',
body: 'Please correct the following issues:\n\n' + error.errorMessage
});

return { notificationSent: true };
</bpmn:script>
</bpmn:scriptTask>

Pattern: Circuit Breaker

<bpmn:scriptTask id="CheckCircuitBreaker" name="Check Circuit Breaker">
<bpmn:script>
var circuitState = context.circuitBreakerState || 'CLOSED';
var failureCount = context.circuitFailureCount || 0;
var lastFailureTime = context.circuitLastFailureTime;
var threshold = 5; // Open circuit after 5 failures
var resetTime = 60000; // 1 minute

if (circuitState === 'OPEN') {
var now = Date.now();
var timeSinceFailure = now - new Date(lastFailureTime).getTime();

if (timeSinceFailure > resetTime) {
// Try half-open state
context.circuitBreakerState = 'HALF_OPEN';
console.log('Circuit breaker: HALF_OPEN (testing)');
} else {
// Still open - fail fast
throw new BpmnError('CIRCUIT_BREAKER_OPEN',
'Circuit breaker is open. Service unavailable.');
}
}

// Continue with normal flow
return {
circuitState: context.circuitBreakerState,
failureCount: failureCount
};
</bpmn:script>
</bpmn:scriptTask>

<bpmn:scriptTask id="ExecuteWithCircuitBreaker" name="Execute Operation">
<bpmn:script>
try {
// Attempt operation
var result = BankLingo.ExecuteCommand('RiskyOperation', context);

// Success - reset circuit breaker
if (context.circuitBreakerState === 'HALF_OPEN') {
context.circuitBreakerState = 'CLOSED';
context.circuitFailureCount = 0;
console.log('Circuit breaker: CLOSED (reset)');
}

return result;

} catch (error) {
// Failure - update circuit breaker
var failureCount = (context.circuitFailureCount || 0) + 1;
context.circuitFailureCount = failureCount;
context.circuitLastFailureTime = new Date().toISOString();

if (failureCount >= 5) {
context.circuitBreakerState = 'OPEN';
console.log('Circuit breaker: OPEN (threshold reached)');
}

throw new BpmnError('OPERATION_FAILED', error.message);
}
</bpmn:script>
</bpmn:scriptTask>

Best Practices for Error Handling

✅ Do This

// ✅ Use specific error codes
throw new BpmnError('CREDIT_SCORE_TOO_LOW', 'Credit score below 600');

// ✅ Include helpful error messages
throw new BpmnError('MISSING_DATA',
'Customer ID is required for credit check');

// ✅ Validate before processing
if (!context.customerId) {
throw new BpmnError('MISSING_DATA', 'Customer ID is required');
}

// ✅ Track retry attempts
context.retryCount = (context.retryCount || 0) + 1;
if (context.retryCount > 3) {
throw new BpmnError('RETRY_EXHAUSTED', 'Failed after 3 attempts');
}

// ✅ Log errors for debugging
console.log('Error occurred:', errorCode, '-', errorMessage);

❌ Don't Do This

// ❌ Generic error codes
throw new BpmnError('ERROR', 'Something went wrong');

// ❌ No error message
throw new BpmnError('VALIDATION_ERROR', '');

// ❌ Silently fail
if (error) {
return null; // No error thrown
}

// ❌ Throw regular errors instead of BpmnError
throw new Error('Validation failed'); // Won't trigger boundary events

// ❌ No retry limit
while (true) {
try { /* ... */ } catch(e) { /* retry forever */ }
}

Error End Events

End the process with an error:

<bpmn:scriptTask id="CriticalValidation" name="Critical Validation">
<bpmn:script>
if (context.fraudScore > 90) {
// Critical fraud detected - end process with error
throw new BpmnError('FRAUD_DETECTED',
'Critical fraud indicators detected. Score: ' + context.fraudScore);
}

return { validated: true };
</bpmn:script>
</bpmn:scriptTask>

<!-- Error end event -->
<bpmn:endEvent id="FraudDetectedEnd" name="Fraud Detected">
<bpmn:errorEventDefinition errorRef="FraudError" />
</bpmn:endEvent>

<bpmn:error id="FraudError" errorCode="FRAUD_DETECTED" />

Next Steps