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 service result (default: lastServiceResult)
  • InputMapping: Map process variables to service inputs
  • OutputMapping: Map service outputs to process variables
  • 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

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!

<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

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" 
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