Skip to main content

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 context to the command
  • Stores results in process variables
  • Supports resultVariable for named result storage
  • No scripting required (unlike ScriptTask)

Properties

Required Properties

  • ConnectorKey: The BankLingo command name to execute
  • TaskType: Must be "ServiceTask"
  • Name: Display name of the task

Optional Properties

  • ResultVariable: Variable name to store full service result
  • OutputMapping: JSON mapping to extract specific fields from result
  • InputMapping: Map process variables to service inputs
  • ServiceName: Descriptive service name
  • Endpoint: Service endpoint (for external HTTP services)
  • Method: HTTP method (GET, POST, etc.)
  • PayloadTemplate: Request payload template
  • Headers: HTTP headers
  • Retries: Number of retry attempts
  • TimeoutMs: Timeout in milliseconds
  • 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 lastServiceResult (default).

Result Handling

Three-Step Result Processing

ServiceTask uses the same result processing as ScriptTask:

  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 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="{
&quot;customerName&quot;: &quot;personalInfo.fullName&quot;,
&quot;email&quot;: &quot;personalInfo.email&quot;,
&quot;income&quot;: &quot;financials.monthlyIncome&quot;,
&quot;creditScore&quot;: &quot;financials.creditScore&quot;
}" />
<!-- Extracts only 4 fields, ignores rest -->

Scenario 3: Both (Full + Selective)

<custom:property name="ResultVariable" value="fullCustomerRecord" />
<custom:property name="OutputMapping" value="{
&quot;name&quot;: &quot;personalInfo.fullName&quot;,
&quot;score&quot;: &quot;financials.creditScore&quot;
}" />
<!-- 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's CustomerId property receives it
  • If context has loanAmount, your command's LoanAmount property 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!

<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

FeatureServiceTaskScriptTask
PurposeCall external services/commandsExecute inline code
ConfigurationConnectorKey onlyFull JavaScript code
ComplexitySimple, declarativeFlexible, programmatic
ReusabilityHigh (command is reusable)Low (script is inline)
MaintainabilityEasy (command in separate file)Harder (script in BPMN)
TestingEasy (test command separately)Harder (need process context)
Use WhenCalling existing servicesCustom logic needed
Context AccessAutomatic (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="{
&quot;customerName&quot;: &quot;personalInfo.fullName&quot;,
&quot;email&quot;: &quot;contact.email&quot;,
&quot;phone&quot;: &quot;contact.phone&quot;,
&quot;monthlyIncome&quot;: &quot;employment.income&quot;,
&quot;employmentStatus&quot;: &quot;employment.status&quot;
}" />
</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="{
&quot;creditScore&quot;: &quot;scores.fico&quot;,
&quot;ratingCode&quot;: &quot;rating&quot;,
&quot;hasBankruptcy&quot;: &quot;publicRecords.hasBankruptcy&quot;,
&quot;openCollections&quot;: &quot;collections.openCount&quot;,
&quot;inquiriesLast6Months&quot;: &quot;inquiries.sixMonthCount&quot;
}" />
</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="{
&quot;availableBalance&quot;: &quot;balances.available&quot;,
&quot;currentBalance&quot;: &quot;balances.current&quot;,
&quot;overdraftLimit&quot;: &quot;limits.overdraft&quot;,
&quot;accountStatus&quot;: &quot;status&quot;
}" />
</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="{
&quot;riskScore&quot;: &quot;assessment.totalScore&quot;,
&quot;riskCategory&quot;: &quot;assessment.category&quot;,
&quot;creditRisk&quot;: &quot;factors.creditRisk&quot;,
&quot;incomeRisk&quot;: &quot;factors.incomeRisk&quot;,
&quot;collateralRisk&quot;: &quot;factors.collateralRisk&quot;,
&quot;recommendedAction&quot;: &quot;recommendation.action&quot;,
&quot;requiredApprovalLevel&quot;: &quot;recommendation.approvalLevel&quot;
}" />
</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="{
&quot;allDocumentsValid&quot;: &quot;summary.allValid&quot;,
&quot;missingDocuments&quot;: &quot;summary.missingCount&quot;,
&quot;invalidDocuments&quot;: &quot;summary.invalidCount&quot;,
&quot;verificationStatus&quot;: &quot;summary.status&quot;
}" />
</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" and loanAmount: 50000
  • Command properties should be named CustomerId and LoanAmount (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:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Common Service Error Codes

Error CodeUse CaseRecovery Action
INVALID_REQUESTInvalid input parametersValidate and retry
AUTHENTICATION_ERRORAuth/credentials failedRefresh token and retry
AUTHORIZATION_ERRORInsufficient permissionsEscalate to admin
RESOURCE_NOT_FOUNDEntity doesn't existCreate or use fallback
DUPLICATE_RESOURCEResource already existsUse existing or merge
VALIDATION_ERRORBusiness validation failedNotify user, request correction
INSUFFICIENT_FUNDSAccount balance too lowNotify customer
GATEWAY_TIMEOUTExternal system timeoutRetry with backoff
GATEWAY_ERRORExternal system errorRetry or use fallback
RATE_LIMIT_EXCEEDEDToo many requestsWait and retry
SERVICE_UNAVAILABLEService downCircuit 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

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

❌ Don't Do This

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Next Steps