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 service result (default:lastServiceResult)InputMapping: Map process variables to service inputsOutputMapping: Map service outputs to process variablesServiceName: 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
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:
// Internal implementation
var commandScript = $"doCmd('{task.ConnectorKey}', context)";
var result = await _commandExecutor.ExecuteFormulaOnlyAsync(
commandScript,
state.Variables, // 👈 This is "context" in the script
user);
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"
camunda: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"
camunda: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"
camunda: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"
camunda: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:
// Your command implementation
public class CalculateCreditScoreCommand : IRequest<CreditScoreResult>
{
public string CustomerId { get; set; }
public decimal LoanAmount { get; set; }
// Command executor will map context properties to these
}
public class CalculateCreditScoreCommandHandler
: IRequestHandler<CalculateCreditScoreCommand, CreditScoreResult>
{
public async Task<CreditScoreResult> Handle(
CalculateCreditScoreCommand request,
CancellationToken cancellationToken)
{
// request.CustomerId comes from context.customerId
// request.LoanAmount comes from context.loanAmount
var score = await _creditService.CalculateScore(
request.CustomerId,
request.LoanAmount);
return new CreditScoreResult
{
Score = score,
Rating = GetRating(score)
};
}
}
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"
camunda: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"
camunda: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:camunda="http://camunda.org/schema/1.0/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"
camunda: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"
camunda: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"
camunda: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"
camunda: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"
camunda:resultVariable="customer">
<custom:property name="ConnectorKey" value="GetCustomerCommand" />
<custom:property name="ResultVariable" value="customer" />
</bpmn:serviceTask>
2. Use Descriptive ResultVariable Names
❌ Bad:
camunda:resultVariable="result1"
camunda:resultVariable="data"
camunda:resultVariable="temp"
✅ Good:
camunda:resultVariable="customerData"
camunda:resultVariable="creditScore"
camunda: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:
public async Task<CustomerData> Handle(
FetchCustomerCommand request,
CancellationToken cancellationToken)
{
try
{
var customer = await _repository.GetCustomerAsync(request.CustomerId);
if (customer == null)
{
throw new Exception($"Customer {request.CustomerId} not found");
}
return customer;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch customer {CustomerId}", request.CustomerId);
throw;
}
}
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 camunda:resultVariable="accountBalance">
<custom:property name="ConnectorKey" value="FetchAccountBalanceCommand" />
</bpmn:serviceTask>
2. External API Calls
<bpmn:serviceTask camunda:resultVariable="creditBureauReport">
<custom:property name="ConnectorKey" value="CallCreditBureauAPICommand" />
</bpmn:serviceTask>
3. Business Rule Execution
<bpmn:serviceTask camunda:resultVariable="loanEligibility">
<custom:property name="ConnectorKey" value="EvaluateLoanEligibilityCommand" />
</bpmn:serviceTask>
4. Data Transformation
<bpmn:serviceTask camunda:resultVariable="formattedReport">
<custom:property name="ConnectorKey" value="FormatLoanReportCommand" />
</bpmn:serviceTask>
5. Notification Services
<bpmn:serviceTask camunda: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
// Context has: { customerId: "12345", loanAmount: 50000 }
// Command should have:
public class MyCommand : IRequest<Result>
{
public string CustomerId { get; set; } // Matches context.customerId
public decimal LoanAmount { get; set; } // Matches context.loanAmount
}
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