Skip to main content

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 context object in scripts
  • How resultVariable works 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 the process's stored variables:

Key Point: When a script executes, the engine automatically passes all process variables as the context object. You don't need to explicitly request or configure thisβ€”it happens automatically.


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 resultVariable values from previous tasks
  • All lastScriptResult from 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" 
custom: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:

The engine automatically generates the command invocation: doCmd('FetchCustomerCommand', context) where context contains all process variables.

Your command receives:

The command handler will receive properties mapped from the context. For example, if context contains customerId and loanAmount, those values are automatically mapped to your command's properties.


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"
custom: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"
custom: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​

When a script or command returns a value, the engine stores it using the specified ResultVariable name. If no ResultVariable is specified, the result is stored in a default variable called lastScriptResult.

Storage Logic:

  1. If ResultVariable is defined β†’ Use that name
  2. If ResultVariable is empty β†’ Use "lastScriptResult" as fallback
  3. Result is saved to the process state and available to subsequent tasks

Storage Behavior​

ScenarioStorage LocationAccess Pattern
No ResultVariablecontext.lastScriptResultGets overwritten each time
With ResultVariablecontext.{ResultVariable}Preserved permanently
ServiceTask defaultcontext.lastServiceResultGets overwritten each time
ServiceTask with ResultVariablecontext.{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:custom="http://banklingo.app/schema/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"
custom: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"
custom: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"
custom: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 β†’ 15000
  • context.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 β†’ 750
  • context.customer.name β†’ "John Doe"
  • context.approved β†’ true
  • context.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" custom: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​

  1. context = All process variables at current point
  2. resultVariable = Named storage for task results
  3. Default storage = lastScriptResult / lastServiceResult (gets overwritten)
  4. Variables persist = All context data flows through entire process
  5. Signal data merges = UserTask completions add to context

Memory Aid​

context = {
...initialVariables,
...task1Result (if resultVariable set),
...task2Result (if resultVariable set),
...userTaskSignalData,
...taskNResult
}

Next Steps​