Recipe: Cross-Process Reference
This recipe shows how a loan renewal process reads data from the original loan application process using a DataStore — no service task, no HTTP call, no hidden dependency.
Processes Involved
loan-application— runs first; produces loan data and stores a business key (loan_id) in its variables.loan-renewal— runs later; needsamount,applicant, andinterest_ratefrom the original loan.
The dependency is declared in the diagram — anyone reading the BPMN can see that loan-renewal reads from loan-application.
Step 1: Ensure the Source Process Stores the Key
The source process must have loan_id in its variables. This is the identity key used for lookup. Create a source instance:
curl -X POST https://api.stateway.io/v1/instances \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"definition_key": "loan-application",
"variables": {
"loan_id": "LOAN-2024-001",
"applicant": { "fullName": "Ana Lima", "email": "ana@example.com" },
"amount": 75000,
"interest_rate": 0.12
}
}'
Run the application process to completion so it reaches status: completed.
Step 2: The Renewal Process BPMN
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:stateway="https://stateway.io/schema/bpmn/1.0"
targetNamespace="https://stateway.io/processes/loan-renewal">
<bpmn:error id="ValidationError" name="Validation Error" errorCode="ValidationError" />
<!-- Type for the snapshot we read from the original loan -->
<bpmn:itemDefinition id="OriginalLoanSnapshot" structureRef="stateway:inline">
<bpmn:extensionElements>
<stateway:schema kind="object"><![CDATA[
{
"type": "object",
"properties": {
"amount": { "type": "number" },
"applicant": { "type": "object" },
"interest_rate": { "type": "number" }
}
}
]]></stateway:schema>
</bpmn:extensionElements>
</bpmn:itemDefinition>
<!-- DataStore: query completed loan-application instances by loan_id -->
<bpmn:dataStore id="LoanStore" name="Loan Applications" itemSubjectRef="OriginalLoanSnapshot">
<bpmn:extensionElements>
<stateway:storeConfig
backingType="processInstances"
definitionKey="loan-application"
identityVariable="loan_id" />
</bpmn:extensionElements>
</bpmn:dataStore>
<bpmn:process id="loan-renewal" isExecutable="true">
<!-- Local variable to hold the resolved original loan data -->
<bpmn:dataObject id="OriginalLoanData" name="originalLoan" itemSubjectRef="OriginalLoanSnapshot" />
<bpmn:dataObjectReference id="OriginalLoanRef" dataObjectRef="OriginalLoanData" />
<!-- DataStoreReference: how to query — snapshot, by original_loan_id variable -->
<bpmn:dataStoreReference id="LoanStoreRef" dataStoreRef="LoanStore">
<bpmn:extensionElements>
<stateway:storeAccess
mode="snapshot"
keyExpression="{{ variables.original_loan_id }}"
stateFilter="instance.status == 'completed'"
projection="amount, applicant, interest_rate"
cardinality="single"
onNotFound="error" />
</bpmn:extensionElements>
</bpmn:dataStoreReference>
<bpmn:startEvent id="start">
<bpmn:outgoing>flow1</bpmn:outgoing>
</bpmn:startEvent>
<!-- This task triggers the DataStore lookup before executing -->
<bpmn:serviceTask id="enrichWithLoan" name="Load Original Loan">
<bpmn:incoming>flow1</bpmn:incoming>
<bpmn:outgoing>flow2</bpmn:outgoing>
<bpmn:dataInputAssociation>
<bpmn:sourceRef>LoanStoreRef</bpmn:sourceRef>
<bpmn:targetRef>OriginalLoanRef</bpmn:targetRef>
</bpmn:dataInputAssociation>
</bpmn:serviceTask>
<!-- Boundary: original loan not found -->
<bpmn:boundaryEvent id="loanNotFound" attachedToRef="enrichWithLoan" cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
<bpmn:outgoing>flow_reject</bpmn:outgoing>
</bpmn:boundaryEvent>
<bpmn:userTask id="reviewRenewal" name="Review Renewal Terms">
<bpmn:incoming>flow2</bpmn:incoming>
<bpmn:outgoing>flow3</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="end">
<bpmn:incoming>flow3</bpmn:incoming>
</bpmn:endEvent>
<bpmn:endEvent id="rejected" name="Loan Not Found">
<bpmn:incoming>flow_reject</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow1" sourceRef="start" targetRef="enrichWithLoan" />
<bpmn:sequenceFlow id="flow2" sourceRef="enrichWithLoan" targetRef="reviewRenewal" />
<bpmn:sequenceFlow id="flow3" sourceRef="reviewRenewal" targetRef="end" />
<bpmn:sequenceFlow id="flow_reject" sourceRef="loanNotFound" targetRef="rejected" />
</bpmn:process>
</bpmn:definitions>
Step 3: Start the Renewal
curl -X POST https://api.stateway.io/v1/instances \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"definition_key": "loan-renewal",
"variables": {
"original_loan_id": "LOAN-2024-001"
}
}'
What happens:
- The engine evaluates
{{ variables.original_loan_id }}→"LOAN-2024-001". - It queries
loan-applicationinstances whereloan_id == "LOAN-2024-001"andstatus == 'completed'. - It copies
amount,applicant, andinterest_rate(the projection) intoinstance.variables.originalLoan. - The
reviewRenewaltask becomes active withoriginalLoanavailable in its snapshot.
Step 4: See the Resolution in the History Log
curl https://api.stateway.io/v1/instances/inst_renewal_01.../history \
-H "X-API-Key: sw_live_your_key"
{
"data": [
{
"id": "evt_01...",
"action": "datastore.resolved",
"payload": {
"storeId": "LoanStore",
"dataStoreRef": "LoanStoreRef",
"definitionKey": "loan-application",
"mode": "snapshot",
"keyValue": "LOAN-2024-001",
"cardinalityFound": 1,
"durationMs": 8
},
"createdAt": "2026-06-12T10:30:00.000Z"
}
]
}
Step 5: Test the Not-Found Path
Start a renewal with an ID that doesn't match any completed application:
curl -X POST https://api.stateway.io/v1/instances \
-H "X-API-Key: sw_live_your_key" \
-d '{
"definition_key": "loan-renewal",
"variables": { "original_loan_id": "LOAN-DOES-NOT-EXIST" }
}'
The DataStore resolution fails, the boundary event loanNotFound fires, and the token routes to the rejected end event. The history log records:
{
"action": "datastore.not_found",
"payload": {
"storeId": "LoanStore",
"keyValue": "LOAN-DOES-NOT-EXIST",
"cardinalityFound": 0,
"mode": "snapshot"
}
}
Key Points
- Isolation: The lookup is automatically scoped to your tenant. A renewal process can never read loans from another tenant.
- Snapshot semantics: The data is copied once at the moment the
enrichWithLoantask executes. Later changes to the original loan's variables don't affect the renewal. - Projection: Only
amount,applicant, andinterest_rateare copied — other variables in the source instance (e.g., internal tracking fields) are excluded.