Skip to main content

Workflow Data & Context Access

Overview

Understanding how to access and manipulate workflow data is crucial for building effective BPMN processes. This guide explains the data flow between tasks, forms, and the execution engine.

The Context Dictionary

All workflow data is stored in a central dictionary called state.Variables (server-side) or workflowData (client-side).

What's In the Context?

{
// Initial process variables (passed at start)
"customerName": "John Doe",
"loanAmount": 50000,

// Data from completed user tasks (signal data)
"creditScore": 720,
"reviewNotes": "Approved by manager",

// Results from script tasks
"lastScriptResult": {
"interestRate": 5.0,
"monthlyPayment": 1458.33
},

// Results from service tasks
"lastServiceResult": {
"creditBureauResponse": {...}
}
}

Access Patterns by Task Type

ScriptTask - Server-Side Access

<bpmn:scriptTask id="Task_Calculate" name="Calculate Payment">
<bpmn:script>
// Access any variable using 'context' object
var amount = context.loanAmount; // Initial variable
var score = context.creditScore; // From previous UserTask signal
var previous = context.lastScriptResult; // From previous ScriptTask

// Perform calculation
var rate = score > 700 ? 5.0 : 7.5;
var monthly = (amount * (1 + rate / 100)) / 36;

// Return result (stored as context.lastScriptResult)
return {
interestRate: rate,
monthlyPayment: monthly,
calculatedAt: new Date().toISOString()
};
</bpmn:script>
</bpmn:scriptTask>

Gateway - Server-Side Access

<bpmn:exclusiveGateway id="Gateway_Check" name="Credit Check">
<custom:property name="conditionalScript" value="
// Access context for decision logic
var score = context.creditScore;
var amount = context.loanAmount;

// Return the name of the flow to take
if (score > 700 && amount < 50000) {
return 'AutoApproved';
} else if (score > 650) {
return 'ManualReview';
} else {
return 'Rejected';
}
" />
</bpmn:exclusiveGateway>

UserTask - Client-Side Access

✅ FIXED: As of this version, workflowData is now passed to the client!

<bpmn:userTask id="Task_Review" name="Review Application">
<custom:property name="Form" value="loan-review-form" />
<custom:property name="ClientScript" value="
// Access workflow data on the client
var amount = workflowData.loanAmount;
var score = workflowData.creditScore;
var calculation = workflowData.lastScriptResult;

// Update UI based on data
if (amount > 100000 && score < 700) {
document.getElementById('warning').style.display = 'block';
document.getElementById('notes').required = true;
}

// Pre-fill fields
document.getElementById('displayAmount').textContent = amount;
document.getElementById('displayRate').textContent = calculation.interestRate + '%';
" />
</bpmn:userTask>

Implementation Details:

The API response now includes WorkflowData in each waiting task:

// In BpmnEngineInvoker.cs
response.Waiting = state.WaitingTasks.Select(t => new WaitingActivity
{
Id = t.TaskId,
Name = t.TaskName,
Form = t.Form,
ClientScript = t.ClientScript,
WorkflowData = state.Variables // ✅ ALL workflow variables included
}).ToList();

ServiceTask - Server-Side Access

<bpmn:serviceTask id="Task_CallAPI" name="Credit Bureau Check">
<custom:property name="Endpoint" value="/api/credit-check" />
<custom:property name="HttpMethod" value="POST" />
<!-- The entire context is sent as request body -->
<!-- API receives: { customerName: "...", loanAmount: ..., ...} -->
</bpmn:serviceTask>

Signal Data (UserTask Completion)

When a user completes a UserTask, they send a signal with form data. This data is merged into the context.

Client-Side: Sending Signal

async function completeUserTask(instanceId, formData) {
const signalData = {
userAction: 'approve', // User's decision
creditScore: 720, // Form input
reviewNotes: 'All documents verified',
approvedBy: currentUser.id, // Additional context
approvalDate: new Date().toISOString()
};

await fetch(`/api/process/${instanceId}/signal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signalData })
});
}

Server-Side: Signal Data Merged

BEFORE SIGNAL:
{
"customerName": "John Doe",
"loanAmount": 50000,
"lastScriptResult": {...}
}

SIGNAL RECEIVED:
{
"userAction": "approve",
"creditScore": 720,
"reviewNotes": "All documents verified",
"approvedBy": "manager123"
}

AFTER MERGE:
{
"customerName": "John Doe", // Original
"loanAmount": 50000, // Original
"lastScriptResult": {...}, // Original
"userAction": "approve", // ← New from signal
"creditScore": 720, // ← New from signal
"reviewNotes": "...", // ← New from signal
"approvedBy": "manager123" // ← New from signal
}

Next Task Can Access Signal Data

<!-- After UserTask completes and signal is received -->
<bpmn:scriptTask id="Task_ProcessDecision" name="Process Decision">
<bpmn:script>
// Access original data
var amount = context.loanAmount;

// Access signal data from UserTask
var decision = context.userAction;
var notes = context.reviewNotes;
var approver = context.approvedBy;

return {
processed: true,
decision: decision,
processedBy: 'system',
approvedBy: approver
};
</bpmn:script>
</bpmn:scriptTask>

Result Variables

Default Result Storage

  • ScriptTask results → context.lastScriptResult
  • ServiceTask results → context.lastServiceResult

Accessing Previous Results

<bpmn:scriptTask id="Task_Step1" name="Calculate Interest">
<bpmn:script>
return { interest: 5000 };
</bpmn:script>
</bpmn:scriptTask>

<bpmn:scriptTask id="Task_Step2" name="Calculate Fees">
<bpmn:script>
// Access previous result
var interest = context.lastScriptResult.interest;
var fees = interest * 0.05;

return {
fees: fees,
total: interest + fees
};
</bpmn:script>
</bpmn:scriptTask>

⚠️ Important: Result Overwriting

Problem: Multiple ScriptTasks overwrite lastScriptResult

<!-- Task 1 stores result -->
<bpmn:scriptTask id="Task_1" name="Calculate A">
<bpmn:script>
return { valueA: 100 };
// Stored in: context.lastScriptResult
</bpmn:script>
</bpmn:scriptTask>

<!-- Task 2 OVERWRITES previous result -->
<bpmn:scriptTask id="Task_2" name="Calculate B">
<bpmn:script>
return { valueB: 200 };
// Stored in: context.lastScriptResult
// ❌ Previous result (valueA) is LOST!
</bpmn:script>
</bpmn:scriptTask>

Solution: Store results with unique keys

<bpmn:scriptTask id="Task_1" name="Calculate A">
<bpmn:script>
context.resultA = { valueA: 100 }; // ✅ Store with unique key
return { valueA: 100 };
</bpmn:script>
</bpmn:scriptTask>

<bpmn:scriptTask id="Task_2" name="Calculate B">
<bpmn:script>
var previousA = context.resultA; // ✅ Access previous result
return { valueB: 200, previousA: previousA };
</bpmn:script>
</bpmn:scriptTask>

Form Data Access

React Component Example

import React, { useEffect, useState } from 'react';

function LoanReviewForm({ waitingTask }) {
const { workflowData, processContext } = waitingTask;

useEffect(() => {
// Execute ClientScript if provided
if (waitingTask.ClientScript) {
// ClientScript has access to workflowData
eval(waitingTask.ClientScript);
}
}, [waitingTask]);

const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);

// Prepare signal data
const signalData = {
userAction: formData.get('action'),
reviewNotes: formData.get('notes'),
reviewedBy: currentUser.id,
reviewedAt: new Date().toISOString()
};

// Send signal to resume workflow
await sendSignal(processContext.InstanceId, signalData);
};

return (
<form onSubmit={handleSubmit}>
<h2>Review Loan Application</h2>

{/* Display workflow data */}
<div className="summary">
<p>Applicant: {workflowData.customerName}</p>
<p>Amount: ${workflowData.loanAmount}</p>
<p>Credit Score: {workflowData.creditScore}</p>

{/* Show calculated data from previous tasks */}
{workflowData.lastScriptResult && (
<>
<p>Interest Rate: {workflowData.lastScriptResult.interestRate}%</p>
<p>Monthly Payment: ${workflowData.lastScriptResult.monthlyPayment}</p>
</>
)}
</div>

{/* Form inputs */}
<textarea name="notes" placeholder="Review notes..." required />

<select name="action">
<option value="approve">Approve</option>
<option value="reject">Reject</option>
<option value="request-info">Request More Info</option>
</select>

<button type="submit">Submit Review</button>
</form>
);
}

Vanilla JavaScript Example

<div id="loan-review-form">
<h2>Review Application</h2>

<!-- Display workflow data -->
<div class="summary">
<p>Applicant: <span id="displayCustomer"></span></p>
<p>Amount: $<span id="displayAmount"></span></p>
<p>Score: <span id="displayScore"></span></p>
</div>

<!-- Form -->
<textarea id="notes" placeholder="Review notes..."></textarea>
<select id="action">
<option value="approve">Approve</option>
<option value="reject">Reject</option>
</select>
<button onclick="submitReview()">Submit</button>
</div>

<script>
// Initialize form with workflow data
document.getElementById('displayCustomer').textContent = workflowData.customerName;
document.getElementById('displayAmount').textContent = workflowData.loanAmount;
document.getElementById('displayScore').textContent = workflowData.creditScore;

// Execute ClientScript from task definition
if (typeof waitingTask !== 'undefined' && waitingTask.ClientScript) {
eval(waitingTask.ClientScript);
}

async function submitReview() {
const signalData = {
userAction: document.getElementById('action').value,
reviewNotes: document.getElementById('notes').value,
reviewedBy: currentUser.id
};

await fetch(`/api/process/${processInstanceId}/signal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signalData })
});
}
</script>

Complete Data Flow Example

<bpmn:process id="LoanProcess" name="Loan Approval">

<!-- START: Initial variables -->
<bpmn:startEvent id="Start" name="Start">
<!-- Variables: { customerName: "John", loanAmount: 50000 } -->
</bpmn:startEvent>

<!-- SCRIPT: Access initial variables -->
<bpmn:scriptTask id="Task_Calculate" name="Calculate">
<bpmn:script>
var amount = context.loanAmount; // Access initial variable
return { calculated: amount * 1.05 };
</bpmn:script>
</bpmn:scriptTask>
<!-- After: context.lastScriptResult = { calculated: 52500 } -->

<!-- USER TASK: Form displays workflow data -->
<bpmn:userTask id="Task_Review" name="Review">
<custom:property name="Form" value="review-form" />
<custom:property name="ClientScript" value="
// Display: workflowData.customerName, workflowData.loanAmount
// Display: workflowData.lastScriptResult.calculated
" />
</bpmn:userTask>
<!-- WAIT: Process stops until signal received -->

<!-- User submits: { userAction: 'approve', notes: '...' } -->
<!-- Signal merges data into context -->

<!-- SCRIPT: Access all accumulated data -->
<bpmn:scriptTask id="Task_Process" name="Process">
<bpmn:script>
var original = context.loanAmount; // Initial
var calculated = context.lastScriptResult; // From Task_Calculate
var decision = context.userAction; // From signal
var notes = context.notes; // From signal

return {
finalAmount: calculated.calculated,
approved: decision === 'approve'
};
</bpmn:script>
</bpmn:scriptTask>

<!-- GATEWAY: Route based on all data -->
<bpmn:exclusiveGateway id="Gateway" name="Route">
<custom:property name="conditionalScript" value="
return context.userAction === 'approve' ? 'Approved' : 'Rejected';
" />
</bpmn:exclusiveGateway>

<bpmn:endEvent id="End" name="End" />
</bpmn:process>

Best Practices

✅ DO: Use Descriptive Variable Names

// GOOD
{
"customerFirstName": "John",
"customerLastName": "Doe",
"loanRequestedAmount": 50000,
"creditBureauScore": 720
}

// BAD
{
"fname": "John",
"lname": "Doe",
"amt": 50000,
"score": 720
}

✅ DO: Structure Complex Data

// GOOD: Structured
{
"customer": {
"firstName": "John",
"lastName": "Doe",
"ssn": "123-45-6789"
},
"loan": {
"amount": 50000,
"term": 36,
"purpose": "home-improvement"
}
}

✅ DO: Preserve Original Values

<bpmn:scriptTask id="Task" name="Adjust Amount">
<bpmn:script>
var original = context.loanAmount;
var adjusted = original * 1.05;

return {
originalAmount: original, // ✅ Preserve original
adjustedAmount: adjusted,
adjustmentFactor: 1.05
};
</bpmn:script>
</bpmn:scriptTask>

✅ DO: Handle Missing Data

<bpmn:scriptTask id="Task" name="Safe Calculation">
<bpmn:script>
var amount = context.loanAmount;
var score = context.creditScore;

// ✅ Validate data exists
if (!amount || !score) {
return {
error: true,
message: 'Missing required data',
missing: [
!amount && 'loanAmount',
!score && 'creditScore'
].filter(Boolean)
};
}

// Proceed with calculation
return { rate: score > 700 ? 5.0 : 7.5 };
</bpmn:script>
</bpmn:scriptTask>

❌ DON'T: Overwrite Without Preserving

// BAD: Lost original value
context.amount = context.amount * 1.05;

// GOOD: Preserve original
context.adjustedAmount = context.amount * 1.05;

❌ DON'T: Use Ambiguous Result Names

<!-- BAD: What does "result" mean? -->
<bpmn:scriptTask id="Task1">
<bpmn:script>
return { result: 100 };
</bpmn:script>
</bpmn:scriptTask>

<!-- GOOD: Clear, descriptive -->
<bpmn:scriptTask id="Task1">
<bpmn:script>
return {
calculatedMonthlyPayment: 100,
calculationDate: new Date().toISOString()
};
</bpmn:script>
</bpmn:scriptTask>

Troubleshooting

Issue: "Cannot read property 'X' of undefined"

Cause: Variable doesn't exist in context

Solution: Check if variable exists before accessing

// BAD
var value = context.someValue.nested.property;

// GOOD
var value = context.someValue?.nested?.property || defaultValue;

// OR
if (context.someValue && context.someValue.nested) {
var value = context.someValue.nested.property;
}

Issue: "Previous script result lost"

Cause: Multiple ScriptTasks overwriting lastScriptResult

Solution: Store with unique keys

// Store with unique key
context.step1Result = { value: 100 };
return { value: 100 };

Issue: "Form cannot access workflow data"

Cause: Bug in current implementation (see WORKFLOW_DATA_ACCESS_GUIDE.md)

Workaround: Pass essential data through task properties

<bpmn:userTask id="Task" name="Review">
<custom:property name="Form" value="review" />
<custom:property name="customerName" value="${customerName}" />
<custom:property name="loanAmount" value="${loanAmount}" />
</bpmn:userTask>

Summary

  • All data lives in context (state.Variables server-side, workflowData client-side)
  • ScriptTasks, ServiceTasks, Gateways access context via context.variableName
  • UserTasks receive workflowData object for client-side access
  • Signal data from UserTask forms is merged into context
  • Result variables default to lastScriptResult / lastServiceResult
  • Best practice: Use descriptive names, preserve originals, structure complex data