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

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

  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