Process Variables and Context Access Guide
Overview
Understanding how variables work in BankLingo Process Engine is crucial for building robust workflows. This guide explains:
- How to access the
contextobject in scripts - How
resultVariableworks vs default storage - How variables flow between tasks
- Best practices for variable management
The Context Object
What is context?
context is a JavaScript object that contains ALL process variables at any point in the workflow execution:
// context contains:
{
// Initial variables (from runtimeContext)
customerId: "12345",
loanAmount: 50000,
// Results from previous tasks
customerData: { ... },
creditScore: { ... },
// Results from user tasks
approved: true,
approverComments: "..."
}
Where Context Comes From
The engine builds context from state.Variables:
// C# Implementation (BpmnExecutionEngine.cs)
var result = await _commandExecutor.ExecuteFormulaOnlyAsync(
task.Script,
state.Variables, // 👈 This becomes "context" in JavaScript
user);
Key Point: state.Variables (C#) = context (JavaScript)
Accessing Context in Different Task Types
1. ScriptTask
In ScriptTask, access context directly:
<bpmn:scriptTask id="Task_Calculate" name="Calculate Monthly Payment">
<bpmn:script><![CDATA[
// Access initial variables
const principal = context.loanAmount; // From runtimeContext
const rate = context.interestRate / 100;
const term = context.termMonths;
// Access previous task results
const creditScore = context.creditScoreData.score; // From previous task
// Perform calculation
const monthlyRate = rate / 12;
const numPayments = term;
const payment = principal *
(monthlyRate * Math.pow(1 + monthlyRate, numPayments)) /
(Math.pow(1 + monthlyRate, numPayments) - 1);
// Return result (will be stored in resultVariable or lastScriptResult)
return {
monthlyPayment: payment,
totalPayment: payment * numPayments,
totalInterest: (payment * numPayments) - principal
};
]]></bpmn:script>
</bpmn:scriptTask>
Available in context:
- All initial variables from
runtimeContext - All
resultVariablevalues from previous tasks - All
lastScriptResultfrom previous tasks (if no resultVariable) - All signal data from UserTasks
2. ServiceTask
In ServiceTask, the entire context is automatically passed to the command:
<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:serviceTask>
Behind the scenes:
// Engine generates this:
var commandScript = $"doCmd('FetchCustomerCommand', context)";
// context contains all process variables
var result = await _commandExecutor.ExecuteFormulaOnlyAsync(
commandScript,
state.Variables, // All variables passed as context
user);
Your command receives:
public class FetchCustomerCommand : IRequest<CustomerData>
{
public string CustomerId { get; set; } // Maps from context.customerId
public decimal LoanAmount { get; set; } // Maps from context.loanAmount
}
3. Gateway (Conditional)
In Gateway conditions, access context for decision-making:
<bpmn:exclusiveGateway id="Gateway_CheckScore" name="Check Credit Score">
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_Approve</bpmn:outgoing>
<bpmn:outgoing>Flow_Reject</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_Approve" name="Good Score"
sourceRef="Gateway_CheckScore"
targetRef="Task_Approve">
<bpmn:conditionExpression>
context.creditScoreData.score >= 700 && context.loanAmount <= 50000
</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_Reject" name="Poor Score"
sourceRef="Gateway_CheckScore"
targetRef="Task_Reject">
<bpmn:conditionExpression>
context.creditScoreData.score < 700 || context.loanAmount > 50000
</bpmn:conditionExpression>
</bpmn:sequenceFlow>
4. UserTask
UserTask receives context data for display and adds signal data to context:
<bpmn:userTask id="Task_ReviewApplication" name="Review Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Form" value="LoanReviewForm" />
<!-- Form can access context variables for display -->
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
When user completes the task:
{
"cmd": "SignalProcessInstanceCommand",
"data": {
"instanceId": "123",
"taskId": "Task_ReviewApplication",
"signalData": {
"approved": true,
"comments": "Customer has excellent payment history",
"approvedBy": "JohnDoe"
}
}
}
Signal data is merged into context:
// Before signal
context = {
customerId: "12345",
creditScoreData: { ... }
}
// After signal
context = {
customerId: "12345",
creditScoreData: { ... },
approved: true, // From signal data
comments: "...", // From signal data
approvedBy: "JohnDoe" // From signal data
}
ResultVariable vs Default Storage
The Problem with Default Storage
Without resultVariable, results get overwritten:
<!-- Task 1 -->
<bpmn:scriptTask id="Task1" name="Calculate Score">
<bpmn:script>
doCmd('calculateScore', { customerId: context.customerId })
</bpmn:script>
</bpmn:scriptTask>
<!-- Task 2 -->
<bpmn:scriptTask id="Task2" name="Check History">
<bpmn:script>
doCmd('checkHistory', { customerId: context.customerId })
</bpmn:script>
</bpmn:scriptTask>
What happens:
// After Task1
context.lastScriptResult = { score: 750, rating: "Good" }
// After Task2 - Task1 result is LOST!
context.lastScriptResult = { transactions: [...], balance: 15000 }
// ❌ Can't access score anymore!
context.lastScriptResult.score // undefined (overwritten!)
The Solution: Use ResultVariable
With resultVariable, results are preserved:
<!-- Task 1 -->
<bpmn:scriptTask id="Task1" name="Calculate Score"
camunda:resultVariable="creditScore">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ResultVariable" value="creditScore" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:script>
doCmd('calculateScore', { customerId: context.customerId })
</bpmn:script>
</bpmn:scriptTask>
<!-- Task 2 -->
<bpmn:scriptTask id="Task2" name="Check History"
camunda:resultVariable="customerHistory">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ResultVariable" value="customerHistory" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:script>
doCmd('checkHistory', { customerId: context.customerId })
</bpmn:script>
</bpmn:scriptTask>
<!-- Task 3 -->
<bpmn:scriptTask id="Task3" name="Make Decision">
<bpmn:script>
// ✅ Both results available!
const score = context.creditScore.score; // 750
const balance = context.customerHistory.balance; // 15000
doCmd('makeDecision', { score, balance });
</bpmn:script>
</bpmn:scriptTask>
What happens:
// After Task1
context.creditScore = { score: 750, rating: "Good" }
// After Task2 - Task1 result still available!
context.creditScore = { score: 750, rating: "Good" }
context.customerHistory = { transactions: [...], balance: 15000 }
// Task 3 can access both
context.creditScore.score // 750 ✅
context.customerHistory.balance // 15000 ✅
How ResultVariable Works
Implementation Details
// BpmnExecutionEngine.cs (ScriptTask)
private async Task ExecuteScriptTaskAsync(BpmnTask task, ...)
{
var result = await _commandExecutor.ExecuteFormulaOnlyAsync(
task.Script,
state.Variables,
user);
if (result != null)
{
// ✅ Use ResultVariable if specified
var variableName = !string.IsNullOrEmpty(task.ResultVariable)
? task.ResultVariable
: "lastScriptResult"; // Default fallback
state.Variables[variableName] = result;
}
}
Storage Behavior
| Scenario | Storage Location | Access Pattern |
|---|---|---|
| No ResultVariable | context.lastScriptResult | Gets overwritten each time |
| With ResultVariable | context.{ResultVariable} | Preserved permanently |
| ServiceTask default | context.lastServiceResult | Gets overwritten each time |
| ServiceTask with ResultVariable | context.{ResultVariable} | Preserved permanently |
Complete Variable Flow Example
Process Definition
<?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="LoanProcess" name="Loan Application Process">
<!-- Start -->
<bpmn:startEvent id="Start" />
<!-- Task 1: Fetch Customer (ServiceTask) -->
<bpmn:serviceTask id="Task_FetchCustomer" name="Fetch Customer"
camunda:resultVariable="customer">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FetchCustomerCommand" />
<custom:property name="ResultVariable" value="customer" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Task 2: Calculate Score (ScriptTask) -->
<bpmn:scriptTask id="Task_CalcScore" name="Calculate Credit Score"
camunda:resultVariable="creditScore">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ResultVariable" value="creditScore" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:script><![CDATA[
// Access customer from Task 1
const customerId = context.customer.id;
const accountBalance = context.customer.accountBalance;
// Access initial variables
const loanAmount = context.loanAmount;
// Call command
return doCmd('calculateCreditScore', {
customerId,
accountBalance,
loanAmount
});
]]></bpmn:script>
</bpmn:scriptTask>
<!-- Task 3: Review Application (UserTask) -->
<bpmn:userTask id="Task_Review" name="Review Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Form" value="ReviewForm" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Task 4: Make Decision (ScriptTask) -->
<bpmn:scriptTask id="Task_Decision" name="Make Final Decision"
camunda:resultVariable="finalDecision">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ResultVariable" value="finalDecision" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:script><![CDATA[
// Access ALL previous results
const score = context.creditScore.score;
const customerName = context.customer.name;
const approved = context.approved; // From UserTask signal
const comments = context.reviewComments; // From UserTask signal
// Make decision
return {
approved: approved && score >= 700,
reason: approved ? comments : "Rejected by reviewer",
approvedAmount: context.loanAmount,
interestRate: score >= 750 ? 7.5 : 9.0
};
]]></bpmn:script>
</bpmn:scriptTask>
<!-- End -->
<bpmn:endEvent id="End" />
</bpmn:process>
</bpmn:definitions>
Execution Timeline with Context Evolution
Step 1: Start Process
Request:
{
"cmd": "StartProcessInstanceCommand",
"data": {
"processDefinitionId": 15,
"runtimeContext": {
"customerId": "CUST123",
"loanAmount": 50000,
"term": 24
}
}
}
Context:
{
customerId: "CUST123",
loanAmount: 50000,
term: 24
}
Step 2: After Task_FetchCustomer
Task executed: ServiceTask with resultVariable="customer"
Context now:
{
// Initial variables (still available)
customerId: "CUST123",
loanAmount: 50000,
term: 24,
// Task 1 result (added)
customer: {
id: "CUST123",
name: "John Doe",
accountNumber: "ACC001",
accountBalance: 15000,
employmentStatus: "employed"
}
}
Step 3: After Task_CalcScore
Task executed: ScriptTask with resultVariable="creditScore"
Script accessed:
context.customer.id→ "CUST123"context.customer.accountBalance→ 15000context.loanAmount→ 50000
Context now:
{
customerId: "CUST123",
loanAmount: 50000,
term: 24,
customer: { ... }, // Still available
// Task 2 result (added)
creditScore: {
score: 750,
rating: "Good",
factors: [
{ name: "Payment History", value: "Excellent" },
{ name: "Account Balance", value: "Good" }
]
}
}
Step 4: UserTask Signal
Task waiting: UserTask Task_Review
Signal request:
{
"cmd": "SignalProcessInstanceCommand",
"data": {
"instanceId": "123",
"taskId": "Task_Review",
"signalData": {
"approved": true,
"reviewComments": "Customer has excellent history",
"reviewedBy": "Jane Smith",
"reviewDate": "2025-12-18"
}
}
}
Context now:
{
customerId: "CUST123",
loanAmount: 50000,
term: 24,
customer: { ... },
creditScore: { ... },
// UserTask signal data (merged)
approved: true,
reviewComments: "Customer has excellent history",
reviewedBy: "Jane Smith",
reviewDate: "2025-12-18"
}
Step 5: After Task_Decision
Task executed: ScriptTask with resultVariable="finalDecision"
Script accessed:
context.creditScore.score→ 750context.customer.name→ "John Doe"context.approved→ truecontext.reviewComments→ "Customer has excellent history"
Final Context:
{
// Initial variables
customerId: "CUST123",
loanAmount: 50000,
term: 24,
// Task 1 result
customer: {
id: "CUST123",
name: "John Doe",
accountNumber: "ACC001",
accountBalance: 15000,
employmentStatus: "employed"
},
// Task 2 result
creditScore: {
score: 750,
rating: "Good",
factors: [...]
},
// UserTask signal data
approved: true,
reviewComments: "Customer has excellent history",
reviewedBy: "Jane Smith",
reviewDate: "2025-12-18",
// Task 4 result (final)
finalDecision: {
approved: true,
reason: "Customer has excellent history",
approvedAmount: 50000,
interestRate: 7.5
}
}
Common Patterns
Pattern 1: Sequential Data Enrichment
// Task 1: Fetch basic data
context.customer = { id: "123", name: "John" }
// Task 2: Enrich with credit data
context.creditData = { score: 750, history: [...] }
// Task 3: Enrich with account data
context.accountData = { balance: 15000, transactions: [...] }
// Task 4: Use all enriched data
const decision = {
customer: context.customer,
credit: context.creditData,
account: context.accountData
};
Pattern 2: Conditional Processing
// Gateway condition
if (context.creditScore.score >= 700) {
// Route to auto-approval
} else {
// Route to manual review
}
Pattern 3: Accumulating Results
// Initialize array in first task
context.validationResults = [];
// Each validation task appends
context.validationResults.push({
validator: "CreditCheck",
passed: true,
message: "Credit score acceptable"
});
// Final task checks all validations
const allPassed = context.validationResults.every(r => r.passed);
Best Practices
1. Always Use ResultVariable
❌ Bad:
<bpmn:scriptTask id="Task1">
<bpmn:script>
return { score: 750 };
</bpmn:script>
</bpmn:scriptTask>
✅ Good:
<bpmn:scriptTask id="Task1" camunda:resultVariable="creditScore">
<bpmn:extensionElements>
<custom:property name="ResultVariable" value="creditScore" />
</bpmn:extensionElements>
<bpmn:script>
return { score: 750 };
</bpmn:script>
</bpmn:scriptTask>
2. Use Descriptive Variable Names
❌ Bad:
context.data1
context.result2
context.temp
✅ Good:
context.customerData
context.creditScore
context.riskAssessment
3. Check for Undefined Values
❌ Bad:
const score = context.creditScore.score; // May crash if undefined
✅ Good:
const score = context.creditScore?.score ?? 0; // Safe access with default
4. Document Your Variables
Add comments in your process definition:
<bpmn:documentation>
Process Variables:
- customerId: String - Customer unique identifier
- loanAmount: Number - Requested loan amount
- customer: Object - Customer details from Task_FetchCustomer
- creditScore: Object - Credit score data from Task_CalcScore
- score: Number (0-850)
- rating: String ("Excellent", "Good", "Fair", "Poor")
- approved: Boolean - From UserTask Task_Review
</bpmn:documentation>
5. Keep Context Clean
Don't pollute context with unnecessary data:
❌ Bad:
context.tempData = [...]; // Never used again
context.debugInfo = {...}; // Not needed
✅ Good:
// Only store what future tasks need
return {
score: calculatedScore,
rating: getRating(calculatedScore)
};
Troubleshooting
Issue: "context is undefined"
Cause: Trying to access context before process starts
Solution: Ensure initial variables are passed in runtimeContext
Issue: "context.variableName is undefined"
Cause: Variable not set by previous task
Solution: Check that previous task has resultVariable set
<!-- Fix: Add resultVariable -->
<custom:property name="ResultVariable" value="variableName" />
Issue: Result from Task 1 not available in Task 3
Cause: Task 2 overwrote lastScriptResult
Solution: Use unique resultVariable names
<!-- Task 1 -->
<custom:property name="ResultVariable" value="task1Result" />
<!-- Task 2 -->
<custom:property name="ResultVariable" value="task2Result" />
<!-- Task 3 can access both -->
<bpmn:script>
const result1 = context.task1Result;
const result2 = context.task2Result;
</bpmn:script>
Issue: Signal data not in context
Cause: Signal data keys don't match expected names
Solution: Verify signal data property names
// Sending signal
{
"signalData": {
"approved": true // Must match context.approved
}
}
Summary
Key Concepts
context= All process variables at current pointresultVariable= Named storage for task results- Default storage =
lastScriptResult/lastServiceResult(gets overwritten) - Variables persist = All context data flows through entire process
- Signal data merges = UserTask completions add to context
Memory Aid
context = {
...initialVariables,
...task1Result (if resultVariable set),
...task2Result (if resultVariable set),
...userTaskSignalData,
...taskNResult
}
Next Steps
- Learn about ScriptTask for inline code
- Explore ServiceTask for external services
- See Complete Examples for full workflows
- Read Execution Modes for supervised execution