ServiceTask - External Service Integration
ServiceTask executes external operations by calling BankLingo commands through connectors. It's ideal for integrating with external APIs, databases, or BankLingo internal services without writing inline scripts.
Overview
ServiceTask provides a declarative way to call pre-configured services/commands:
- Calls BankLingo commands via
connectorKey - Automatically passes process
contextto the command - Stores results in process variables
- Supports
resultVariablefor named result storage - No scripting required (unlike ScriptTask)
Properties
Required Properties
ConnectorKey: The BankLingo command name to executeTaskType: Must be "ServiceTask"Name: Display name of the task
Optional Properties
ResultVariable: Variable name to store full service resultOutputMapping: JSON mapping to extract specific fields from resultInputMapping: Map process variables to service inputsServiceName: Descriptive service nameEndpoint: Service endpoint (for external HTTP services)Method: HTTP method (GET, POST, etc.)PayloadTemplate: Request payload templateHeaders: HTTP headersRetries: Number of retry attemptsTimeoutMs: Timeout in millisecondsEntityState: State during executionAsyncBefore/AsyncAfter: Async execution flags
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 lastServiceResult (default).
Result Handling
Three-Step Result Processing
ServiceTask uses the same result processing as ScriptTask:
- ResultVariable: Store the full result in a named variable (if specified)
- OutputMapping: Extract specific fields and map them to variables (if specified)
- Default: Store in
lastServiceResult(if neither ResultVariable nor OutputMapping is set)
// Example: Command returns customer object
{
customerId: "C12345",
personalInfo: {
fullName: "John Doe",
email: "john@example.com",
phone: "+1234567890"
},
financials: {
monthlyIncome: 5000,
creditScore: 720,
accounts: [...]
},
metadata: {
lastUpdated: "2024-01-15T10:30:00Z",
source: "CoreBanking"
}
}
Scenario 1: Full Result Storage
<custom:property name="ResultVariable" value="customerData" />
<!-- Stores entire customer object in customerData -->
Scenario 2: Selective Field Mapping
<custom:property name="OutputMapping" value="{
"customerName": "personalInfo.fullName",
"email": "personalInfo.email",
"income": "financials.monthlyIncome",
"creditScore": "financials.creditScore"
}" />
<!-- Extracts only 4 fields, ignores rest -->
Scenario 3: Both (Full + Selective)
<custom:property name="ResultVariable" value="fullCustomerRecord" />
<custom:property name="OutputMapping" value="{
"name": "personalInfo.fullName",
"score": "financials.creditScore"
}" />
<!-- Stores full result in fullCustomerRecord AND extracts name/score -->
How ServiceTask Works
Execution Flow
1. ServiceTask reaches execution
2. Reads ConnectorKey property
3. Builds command: doCmd('ConnectorKey', context)
4. Executes command with current process variables as context
5. Stores result:
- If ResultVariable set: context[ResultVariable] = result
- Else: context['lastServiceResult'] = result
6. Continues to next element
Context Passing
IMPORTANT: The entire context object (all process variables) is automatically passed to the command.
The engine executes: doCmd('ConnectorKey', context)
Where context contains all current process variables. Your command receives this context object and can access any variable defined in the process.
BPMN XML Examples
Basic ServiceTask with ConnectorKey
<bpmn:serviceTask id="Task_FetchCustomer" name="Fetch Customer Details">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FetchCustomerCommand" />
<custom:property name="Description" value="Retrieves customer information from core banking" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:serviceTask>
Result Storage: context.lastServiceResult will contain the command's return value.
ServiceTask with ResultVariable
<bpmn:serviceTask id="Task_CalculateCredit" name="Calculate Credit Score"
custom:resultVariable="creditScoreData">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CalculateCreditScoreCommand" />
<custom:property name="ResultVariable" value="creditScoreData" />
<custom:property name="Description" value="Calculates credit score based on customer history" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:serviceTask>
Result Storage: context.creditScoreData will contain:
{
score: 750,
rating: "Good",
factors: ["Payment History", "Credit Utilization"]
}
Sequential ServiceTasks with Named Results
<!-- Task 1: Fetch Customer -->
<bpmn:serviceTask id="Task_FetchCustomer" name="Fetch Customer"
custom:resultVariable="customerData">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FetchCustomerCommand" />
<custom:property name="ResultVariable" value="customerData" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:serviceTask>
<!-- Task 2: Calculate Credit Score (uses customerData from context) -->
<bpmn:serviceTask id="Task_CalcScore" name="Calculate Credit Score"
custom:resultVariable="creditScore">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CalculateCreditScoreCommand" />
<custom:property name="ResultVariable" value="creditScore" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_2</bpmn:incoming>
<bpmn:outgoing>Flow_3</bpmn:outgoing>
</bpmn:serviceTask>
<!-- Task 3: Make Decision (uses both customerData and creditScore from context) -->
<bpmn:serviceTask id="Task_MakeDecision" name="Make Loan Decision"
custom:resultVariable="loanDecision">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="MakeLoanDecisionCommand" />
<custom:property name="ResultVariable" value="loanDecision" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_3</bpmn:incoming>
<bpmn:outgoing>Flow_4</bpmn:outgoing>
</bpmn:serviceTask>
Available in Task 3 Context:
{
// Initial variables
customerId: "12345",
loanAmount: 50000,
// From Task 1
customerData: { id: "12345", name: "John Doe", accountBalance: 15000 },
// From Task 2
creditScore: { score: 750, rating: "Good" },
// Task 3 will add
loanDecision: { approved: true, reason: "Good credit score" }
}
ServiceTask with HTTP Configuration
<bpmn:serviceTask id="Task_CallExternalAPI" name="Call External Credit Bureau">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CallExternalAPICommand" />
<custom:property name="ServiceName" value="CreditBureauAPI" />
<custom:property name="Endpoint" value="https://api.creditbureau.com/v2/check" />
<custom:property name="Method" value="POST" />
<custom:property name="ResultVariable" value="bureauResponse" />
<custom:property name="Retries" value="3" />
<custom:property name="TimeoutMs" value="5000" />
<custom:property name="PayloadTemplate" value='{
"customerId": "{{context.customerId}}",
"requestType": "credit_check"
}' />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:serviceTask>
Accessing Context in ServiceTask
Understanding Context
When a ServiceTask executes, it receives ALL process variables as context:
// Initial process start
{
"processDefinitionId": 15,
"runtimeContext": {
"customerId": "12345",
"loanAmount": 50000,
"applicationType": "new"
}
}
// Inside ServiceTask's command, you have access to:
context.customerId // "12345"
context.loanAmount // 50000
context.applicationType // "new"
Accessing Previous Task Results
// After Task 1 (resultVariable: "customerData")
context.customerData // { id: "12345", name: "John Doe", ... }
// After Task 2 (resultVariable: "creditScore")
context.creditScore // { score: 750, rating: "Good" }
context.creditScore.score // 750
context.creditScore.rating // "Good"
// All previous results are available!
context.customerId // Still available from initial context
context.loanAmount // Still available
context.customerData // Still available
Accessing Results in BankLingo Commands
When you create a BankLingo command that will be called from ServiceTask:
Command receives process context automatically:
- The command's properties are automatically mapped from the process context
- If context has
customerId, your command'sCustomerIdproperty receives it - If context has
loanAmount, your command'sLoanAmountproperty receives it - The command executes its business logic
- Returns a result object that can be stored in ResultVariable or mapped via OutputMapping
Example Context Mapping:
Process Context:
{
customerId: "C12345",
loanAmount: 50000,
term: 60
}
}
ResultVariable vs lastServiceResult
Without ResultVariable (Default Behavior)
<bpmn:serviceTask id="Task1" name="Get Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GetDataCommand" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
Result: Stored in context.lastServiceResult
Problem: Gets overwritten by the next ServiceTask!
// After Task1
context.lastServiceResult = { data: "Task1 result" }
// After Task2 (overwrites Task1's result!)
context.lastServiceResult = { data: "Task2 result" } // Task1 result is LOST!
With ResultVariable (Recommended)
<bpmn:serviceTask id="Task1" name="Get Customer"
custom:resultVariable="customer">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GetCustomerCommand" />
<custom:property name="ResultVariable" value="customer" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:serviceTask id="Task2" name="Get Account"
custom:resultVariable="account">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GetAccountCommand" />
<custom:property name="ResultVariable" value="account" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
Result: Both results are preserved!
// After Task1
context.customer = { id: "12345", name: "John Doe" }
// After Task2
context.customer = { id: "12345", name: "John Doe" } // Still available!
context.account = { id: "ACC001", balance: 15000 } // New result
Complete Example: Loan Application Workflow
BPMN Process
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:custom="http://banklingo.com/schema/bpmn"
xmlns:custom="http://banklingo.app/schema/bpmn">
<bpmn:process id="LoanApplicationProcess" name="Loan Application" isExecutable="true">
<!-- Start Event -->
<bpmn:startEvent id="StartEvent_1" name="Loan Application Submitted">
<bpmn:outgoing>Flow_1</bpmn:outgoing>
</bpmn:startEvent>
<!-- Task 1: Fetch Customer Details -->
<bpmn:serviceTask id="Task_FetchCustomer" name="Fetch Customer Details"
custom:resultVariable="customerData">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FetchCustomerDetailsCommand" />
<custom:property name="ResultVariable" value="customerData" />
<custom:property name="Description" value="Retrieves customer profile from core banking" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:serviceTask>
<!-- Task 2: Calculate Credit Score -->
<bpmn:serviceTask id="Task_CalcCredit" name="Calculate Credit Score"
custom:resultVariable="creditScore">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CalculateCreditScoreCommand" />
<custom:property name="ResultVariable" value="creditScore" />
<custom:property name="Description" value="Calculates credit score using customer data" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_2</bpmn:incoming>
<bpmn:outgoing>Flow_3</bpmn:outgoing>
</bpmn:serviceTask>
<!-- Task 3: Assess Risk -->
<bpmn:serviceTask id="Task_AssessRisk" name="Assess Loan Risk"
custom:resultVariable="riskAssessment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="AssessLoanRiskCommand" />
<custom:property name="ResultVariable" value="riskAssessment" />
<custom:property name="Description" value="Evaluates risk based on score and customer data" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_3</bpmn:incoming>
<bpmn:outgoing>Flow_4</bpmn:outgoing>
</bpmn:serviceTask>
<!-- Task 4: Make Decision -->
<bpmn:serviceTask id="Task_MakeDecision" name="Make Loan Decision"
custom:resultVariable="loanDecision">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="MakeLoanDecisionCommand" />
<custom:property name="ResultVariable" value="loanDecision" />
<custom:property name="Description" value="Final loan approval decision" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_4</bpmn:incoming>
<bpmn:outgoing>Flow_5</bpmn:outgoing>
</bpmn:serviceTask>
<!-- End Event -->
<bpmn:endEvent id="EndEvent_1" name="Decision Made">
<bpmn:incoming>Flow_5</bpmn:incoming>
</bpmn:endEvent>
<!-- Sequence Flows -->
<bpmn:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="Task_FetchCustomer" />
<bpmn:sequenceFlow id="Flow_2" sourceRef="Task_FetchCustomer" targetRef="Task_CalcCredit" />
<bpmn:sequenceFlow id="Flow_3" sourceRef="Task_CalcCredit" targetRef="Task_AssessRisk" />
<bpmn:sequenceFlow id="Flow_4" sourceRef="Task_AssessRisk" targetRef="Task_MakeDecision" />
<bpmn:sequenceFlow id="Flow_5" sourceRef="Task_MakeDecision" targetRef="EndEvent_1" />
</bpmn:process>
</bpmn:definitions>
Process Execution with Context Evolution
1. Start Process
{
"cmd": "StartProcessInstanceCommand",
"data": {
"processDefinitionId": 15,
"runtimeContext": {
"customerId": "CUST12345",
"loanAmount": 50000,
"term": 24,
"purpose": "home_improvement"
}
}
}
Initial Context:
{
customerId: "CUST12345",
loanAmount: 50000,
term: 24,
purpose: "home_improvement"
}
2. After Task_FetchCustomer (resultVariable: "customerData")
Context Now Contains:
{
// Initial variables
customerId: "CUST12345",
loanAmount: 50000,
term: 24,
purpose: "home_improvement",
// Task 1 result
customerData: {
id: "CUST12345",
name: "John Doe",
accountNumber: "ACC001",
accountBalance: 15000,
employmentStatus: "employed",
yearsAtJob: 5
}
}
3. After Task_CalcCredit (resultVariable: "creditScore")
Context Now Contains:
{
// All previous variables still available
customerId: "CUST12345",
loanAmount: 50000,
term: 24,
purpose: "home_improvement",
customerData: { ... },
// Task 2 result
creditScore: {
score: 750,
rating: "Good",
factors: [
{ factor: "Payment History", weight: 35, value: "Excellent" },
{ factor: "Credit Utilization", weight: 30, value: "Good" }
]
}
}
4. After Task_AssessRisk (resultVariable: "riskAssessment")
Context Now Contains:
{
customerId: "CUST12345",
loanAmount: 50000,
term: 24,
purpose: "home_improvement",
customerData: { ... },
creditScore: { ... },
// Task 3 result
riskAssessment: {
riskLevel: "low",
riskScore: 85,
recommendation: "approve",
maxLoanAmount: 75000,
suggestedInterestRate: 8.5
}
}
5. After Task_MakeDecision (resultVariable: "loanDecision")
Final Context:
{
customerId: "CUST12345",
loanAmount: 50000,
term: 24,
purpose: "home_improvement",
customerData: { ... },
creditScore: { ... },
riskAssessment: { ... },
// Task 4 result
loanDecision: {
approved: true,
approvedAmount: 50000,
interestRate: 8.5,
monthlyPayment: 2273.93,
reason: "Good credit score and low risk",
decisionDate: "2025-12-18T10:30:00Z"
}
}
ServiceTask vs ScriptTask
| Feature | ServiceTask | ScriptTask |
|---|---|---|
| Purpose | Call external services/commands | Execute inline code |
| Configuration | ConnectorKey only | Full JavaScript code |
| Complexity | Simple, declarative | Flexible, programmatic |
| Reusability | High (command is reusable) | Low (script is inline) |
| Maintainability | Easy (command in separate file) | Harder (script in BPMN) |
| Testing | Easy (test command separately) | Harder (need process context) |
| Use When | Calling existing services | Custom logic needed |
| Context Access | Automatic (via command mapping) | Manual (via context object) |
Best Practices
1. Always Use ResultVariable
⌠Bad:
<bpmn:serviceTask id="Task1" name="Get Data">
<custom:property name="ConnectorKey" value="GetDataCommand" />
</bpmn:serviceTask>
✅ Good:
<bpmn:serviceTask id="Task1" name="Get Customer"
custom:resultVariable="customer">
<custom:property name="ConnectorKey" value="GetCustomerCommand" />
<custom:property name="ResultVariable" value="customer" />
</bpmn:serviceTask>
2. Use Descriptive ResultVariable Names
⌠Bad:
custom:resultVariable="result1"
custom:resultVariable="data"
custom:resultVariable="temp"
✅ Good:
custom:resultVariable="customerData"
custom:resultVariable="creditScore"
custom:resultVariable="riskAssessment"
3. Document Your ServiceTasks
<custom:property name="Description" value="Fetches customer details from core banking system" />
<custom:property name="ResultVariable" value="customerData" />
<custom:property name="ConnectorKey" value="FetchCustomerDetailsCommand" />
4. Handle Errors in Commands
Implement proper error handling in your BankLingo commands:
Error Handling Best Practices:
- Validate input parameters before executing business logic
- Log errors with sufficient context for debugging
- Return meaningful error messages
- Use appropriate exception types
- Consider retry logic for transient failures
Example Error Scenarios:
- Customer not found → Throw descriptive error
- Invalid amount → Validate and throw
- External service timeout → Log and retry
- Database connection issue → Handle gracefully
OutputMapping Examples
Example 1: Extract Customer Information
<bpmn:serviceTask id="Task_FetchCustomer" name="Fetch Customer Profile">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GetCustomerProfileCommand" />
<!-- Extract only needed fields from large customer object -->
<custom:property name="OutputMapping" value="{
"customerName": "personalInfo.fullName",
"email": "contact.email",
"phone": "contact.phone",
"monthlyIncome": "employment.income",
"employmentStatus": "employment.status"
}" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_In</bpmn:incoming>
<bpmn:outgoing>Flow_Out</bpmn:outgoing>
</bpmn:serviceTask>
Example 2: Credit Bureau Integration
<bpmn:serviceTask id="Task_CreditCheck" name="Check Credit Bureau">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CreditBureauCheckCommand" />
<!-- Store full report for audit -->
<custom:property name="ResultVariable" value="fullCreditBureauReport" />
<!-- Extract decision fields for workflow -->
<custom:property name="OutputMapping" value="{
"creditScore": "scores.fico",
"ratingCode": "rating",
"hasBankruptcy": "publicRecords.hasBankruptcy",
"openCollections": "collections.openCount",
"inquiriesLast6Months": "inquiries.sixMonthCount"
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
Example 3: Account Balance Check
<bpmn:serviceTask id="Task_CheckBalance" name="Verify Account Balance">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GetAccountBalanceCommand" />
<!-- Extract balance information -->
<custom:property name="OutputMapping" value="{
"availableBalance": "balances.available",
"currentBalance": "balances.current",
"overdraftLimit": "limits.overdraft",
"accountStatus": "status"
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Use in gateway condition: availableBalance >= requestedAmount -->
Example 4: Risk Assessment Service
<bpmn:serviceTask id="Task_AssessRisk" name="Assess Loan Risk">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="LoanRiskAssessmentCommand" />
<!-- Store full assessment for reporting -->
<custom:property name="ResultVariable" value="riskAssessmentReport" />
<!-- Extract key decision metrics -->
<custom:property name="OutputMapping" value="{
"riskScore": "assessment.totalScore",
"riskCategory": "assessment.category",
"creditRisk": "factors.creditRisk",
"incomeRisk": "factors.incomeRisk",
"collateralRisk": "factors.collateralRisk",
"recommendedAction": "recommendation.action",
"requiredApprovalLevel": "recommendation.approvalLevel"
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Available for routing:
- riskAssessmentReport (full object for audit)
- riskScore, riskCategory, recommendedAction (for workflow logic)
-->
Example 5: Document Verification
<bpmn:serviceTask id="Task_VerifyDocuments" name="Verify Uploaded Documents">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="DocumentVerificationCommand" />
<!-- Extract verification results -->
<custom:property name="OutputMapping" value="{
"allDocumentsValid": "summary.allValid",
"missingDocuments": "summary.missingCount",
"invalidDocuments": "summary.invalidCount",
"verificationStatus": "summary.status"
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Use in exclusive gateway:
- If allDocumentsValid === true → Continue
- Else → Request additional documents
-->
5. Keep Services Focused
Each ServiceTask should do one thing:
⌠Bad: One ServiceTask that fetches customer, calculates score, and makes decision
✅ Good: Three ServiceTasks, each with a specific responsibility
Use Cases
1. Core Banking Integration
<bpmn:serviceTask custom:resultVariable="accountBalance">
<custom:property name="ConnectorKey" value="FetchAccountBalanceCommand" />
</bpmn:serviceTask>
2. External API Calls
<bpmn:serviceTask custom:resultVariable="creditBureauReport">
<custom:property name="ConnectorKey" value="CallCreditBureauAPICommand" />
</bpmn:serviceTask>
3. Business Rule Execution
<bpmn:serviceTask custom:resultVariable="loanEligibility">
<custom:property name="ConnectorKey" value="EvaluateLoanEligibilityCommand" />
</bpmn:serviceTask>
4. Data Transformation
<bpmn:serviceTask custom:resultVariable="formattedReport">
<custom:property name="ConnectorKey" value="FormatLoanReportCommand" />
</bpmn:serviceTask>
5. Notification Services
<bpmn:serviceTask custom:resultVariable="notificationResult">
<custom:property name="ConnectorKey" value="SendApprovalNotificationCommand" />
</bpmn:serviceTask>
Troubleshooting
Issue: "No connector key, skipping"
Cause: ConnectorKey property not set
Solution: Add ConnectorKey to extensionElements
<custom:property name="ConnectorKey" value="YourCommandName" />
Issue: Result is undefined in next task
Cause: Not using ResultVariable, result got overwritten
Solution: Always use ResultVariable
<custom:property name="ResultVariable" value="uniqueName" />
Issue: Command not found
Cause: ConnectorKey doesn't match registered command
Solution: Verify command is registered in BankLingo command registry
Issue: Context data missing in command
Cause: Command properties don't match context properties
Solution: Ensure command properties match context variable names exactly. For example:
- If context has
customerId: "12345"andloanAmount: 50000 - Command properties should be named
CustomerIdandLoanAmount(property name matching, case-insensitive) - The engine automatically maps context properties to command properties by name
Error Handling (Phase 5)
Handling Service Failures with BpmnError
ServiceTask integrates with Phase 5 error handling. Commands can throw BpmnError to trigger boundary error events:
<bpmn:serviceTask id="CallPaymentGateway" name="Process Payment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessPaymentCommand"/>
<custom:property name="ResultVariable" value="paymentResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch payment errors -->
<bpmn:boundaryEvent id="PaymentFailed"
attachedToRef="CallPaymentGateway"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="PaymentError" />
</bpmn:boundaryEvent>
<!-- Handle error -->
<bpmn:scriptTask id="HandlePaymentError" name="Handle Payment Error">
<bpmn:script>
var error = context._lastError;
logger.error('Payment failed: ' + error.errorCode + ' - ' + error.errorMessage);
// Determine recovery action based on error code
if (error.errorCode === 'INSUFFICIENT_FUNDS') {
context.recoveryAction = 'NOTIFY_CUSTOMER';
} else if (error.errorCode === 'GATEWAY_TIMEOUT') {
context.recoveryAction = 'RETRY_PAYMENT';
} else {
context.recoveryAction = 'ESCALATE_TO_SUPPORT';
}
return {
handled: true,
action: context.recoveryAction
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:error id="PaymentError" errorCode="PAYMENT_ERROR" />
Throwing BpmnError from Commands
Commands should throw BpmnErrorException to trigger error boundaries:
Implementation details removed for security.
Contact support for implementation guidance.
Common Service Error Codes
| Error Code | Use Case | Recovery Action |
|---|---|---|
INVALID_REQUEST | Invalid input parameters | Validate and retry |
AUTHENTICATION_ERROR | Auth/credentials failed | Refresh token and retry |
AUTHORIZATION_ERROR | Insufficient permissions | Escalate to admin |
RESOURCE_NOT_FOUND | Entity doesn't exist | Create or use fallback |
DUPLICATE_RESOURCE | Resource already exists | Use existing or merge |
VALIDATION_ERROR | Business validation failed | Notify user, request correction |
INSUFFICIENT_FUNDS | Account balance too low | Notify customer |
GATEWAY_TIMEOUT | External system timeout | Retry with backoff |
GATEWAY_ERROR | External system error | Retry or use fallback |
RATE_LIMIT_EXCEEDED | Too many requests | Wait and retry |
SERVICE_UNAVAILABLE | Service down | Circuit breaker pattern |
Pattern: Retry with Exponential Backoff
<bpmn:serviceTask id="CallUnreliableAPI" name="Call Unreliable API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CallPartnerAPICommand"/>
<custom:property name="ResultVariable" value="apiResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Boundary for retryable errors -->
<bpmn:boundaryEvent id="RetryableError"
attachedToRef="CallUnreliableAPI"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="RetryError" />
</bpmn:boundaryEvent>
<!-- Boundary for permanent failures -->
<bpmn:boundaryEvent id="PermanentError"
attachedToRef="CallUnreliableAPI"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="FatalError" />
</bpmn:boundaryEvent>
<!-- Retry logic -->
<bpmn:scriptTask id="CalculateRetry" name="Calculate Retry Delay">
<bpmn:script>
var retryCount = context.apiRetryCount || 0;
var maxRetries = 5;
retryCount++;
context.apiRetryCount = retryCount;
if (retryCount > maxRetries) {
// Convert to permanent error
throw new BpmnError('API_RETRY_EXHAUSTED',
'Failed after ' + maxRetries + ' retry attempts');
}
// Exponential backoff: 2^retryCount * 1000ms
var delayMs = Math.pow(2, retryCount) * 1000;
context.retryDelayMs = delayMs;
logger.info('Retry attempt ' + retryCount + ' after ' + delayMs + 'ms');
return {
retryCount: retryCount,
delayMs: delayMs
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Timer for backoff delay -->
<bpmn:intermediateCatchEvent id="WaitBeforeRetry" name="Wait">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>${context.retryDelayMs}ms</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<!-- Loop back to retry -->
<bpmn:sequenceFlow sourceRef="WaitBeforeRetry" targetRef="CallUnreliableAPI" />
<bpmn:error id="RetryError" errorCode="GATEWAY_TIMEOUT" />
<bpmn:error id="FatalError" errorCode="API_RETRY_EXHAUSTED" />
Pattern: Fallback Service
<bpmn:serviceTask id="CallPrimaryService" name="Call Primary Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="PrimaryServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch primary service failures -->
<bpmn:boundaryEvent id="PrimaryFailed"
attachedToRef="CallPrimaryService"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ServiceError" />
</bpmn:boundaryEvent>
<!-- Try fallback service -->
<bpmn:serviceTask id="CallFallbackService" name="Call Fallback Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FallbackServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch fallback failures -->
<bpmn:boundaryEvent id="FallbackFailed"
attachedToRef="CallFallbackService"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ServiceError" />
</bpmn:boundaryEvent>
<!-- Both services failed - use cached data -->
<bpmn:scriptTask id="UseCachedData" name="Use Cached Data">
<bpmn:script>
logger.warn('Both primary and fallback services failed. Using cached data.');
// Use last known good data
context.serviceResult = context.cachedServiceData;
context.dataSource = 'CACHE';
context.dataFreshness = 'STALE';
return {
usingCache: true,
cacheAge: Date.now() - new Date(context.cacheTimestamp).getTime()
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:error id="ServiceError" errorCode="SERVICE_ERROR" />
Pattern: Validation Before Service Call
<bpmn:scriptTask id="ValidateServiceInput" name="Validate Input">
<bpmn:script>
// Validate all required fields before expensive service call
var errors = [];
if (!context.customerId) {
errors.push('Customer ID is required');
}
if (!context.accountNumber || context.accountNumber.length !== 10) {
errors.push('Valid 10-digit account number is required');
}
if (!context.amount || context.amount <= 0) {
errors.push('Amount must be greater than zero');
}
if (context.amount > 1000000) {
errors.push('Amount exceeds maximum limit of $1,000,000');
}
// Throw validation error if any issues
if (errors.length > 0) {
throw new BpmnError('VALIDATION_ERROR',
'Input validation failed: ' + errors.join('; '));
}
return { validated: true };
</bpmn:script>
</bpmn:scriptTask>
<!-- Only call service if validation passes -->
<bpmn:serviceTask id="CallExpensiveService" name="Call Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ExpensiveServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch validation errors -->
<bpmn:boundaryEvent id="ValidationFailed"
attachedToRef="ValidateServiceInput"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>
<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />
Pattern: Circuit Breaker for Services
<bpmn:scriptTask id="CheckCircuitBreaker" name="Check Circuit Breaker">
<bpmn:script>
var serviceName = 'PaymentGateway';
var circuitKey = 'circuit_' + serviceName;
var circuitState = context[circuitKey + '_state'] || 'CLOSED';
var failureCount = context[circuitKey + '_failures'] || 0;
var lastFailureTime = context[circuitKey + '_lastFailure'];
var threshold = 10; // Open after 10 failures
var resetTimeout = 300000; // 5 minutes
if (circuitState === 'OPEN') {
var now = Date.now();
var timeSinceFailure = now - new Date(lastFailureTime).getTime();
if (timeSinceFailure > resetTimeout) {
// Try half-open
context[circuitKey + '_state'] = 'HALF_OPEN';
logger.info('Circuit breaker HALF_OPEN for ' + serviceName);
} else {
// Still open - fail fast
throw new BpmnError('CIRCUIT_BREAKER_OPEN',
serviceName + ' circuit breaker is OPEN. Service unavailable.');
}
}
return {
circuitState: context[circuitKey + '_state'],
failureCount: failureCount
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:serviceTask id="CallProtectedService" name="Call Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="PaymentGatewayCommand"/>
<custom:property name="ResultVariable" value="paymentResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Handle success/failure for circuit breaker -->
<bpmn:scriptTask id="UpdateCircuitBreaker" name="Update Circuit Breaker">
<bpmn:script>
var serviceName = 'PaymentGateway';
var circuitKey = 'circuit_' + serviceName;
var error = context._lastError;
if (error) {
// Failure - increment counter
var failureCount = (context[circuitKey + '_failures'] || 0) + 1;
context[circuitKey + '_failures'] = failureCount;
context[circuitKey + '_lastFailure'] = new Date().toISOString();
if (failureCount >= 10) {
context[circuitKey + '_state'] = 'OPEN';
logger.error('Circuit breaker OPEN for ' + serviceName);
}
} else {
// Success - reset if half-open
if (context[circuitKey + '_state'] === 'HALF_OPEN') {
context[circuitKey + '_state'] = 'CLOSED';
context[circuitKey + '_failures'] = 0;
logger.info('Circuit breaker CLOSED for ' + serviceName);
}
}
return { updated: true };
</bpmn:script>
</bpmn:scriptTask>
Accessing Error Context in Recovery Tasks
// Access error information
var error = context._lastError;
if (error) {
console.log('Error Code:', error.errorCode);
console.log('Error Message:', error.errorMessage);
console.log('Error Timestamp:', error.timestamp);
console.log('Failed Task:', error.taskId);
// Determine recovery action
switch (error.errorCode) {
case 'GATEWAY_TIMEOUT':
context.recoveryAction = 'RETRY';
break;
case 'INSUFFICIENT_FUNDS':
context.recoveryAction = 'NOTIFY_CUSTOMER';
break;
case 'AUTHENTICATION_ERROR':
context.recoveryAction = 'REFRESH_TOKEN';
break;
default:
context.recoveryAction = 'ESCALATE';
}
}
// Access full error history
var errorHistory = context._errorHistory || [];
console.log('Total errors in this process:', errorHistory.length);
Best Practices for Service Error Handling
✅ Do This
Implementation details removed for security.
Contact support for implementation guidance.
⌠Don't Do This
Implementation details removed for security.
Contact support for implementation guidance.
Related Documentation
- JavaScript Error Throwing Guide - Complete BpmnError reference
- Script Task Error Handling - Script error patterns
- Error Recovery Patterns - Advanced recovery strategies
- Circuit Breaker Pattern - Service protection patterns
Next Steps
- Learn about ScriptTask for inline code execution
- Explore UserTask for human interactions
- See Complete Examples for full workflows
- Read Execution Modes for supervised execution
- Advanced Error Handling - Deep dive into error patterns