Data Stores
A DataStore lets one process read variables from instances of another process — directly in the BPMN diagram, without writing a service task or HTTP call.
The dependency is visible to anyone reading the diagram: a DataStoreReference symbol connects the two processes explicitly. There is nothing hidden in code.
These constructs (itemDefinition, dataObject/dataObjectReference, dataStore/dataStoreReference)
are parsed only from BPMN 2.0 XML definitions. JSON/YAML process definitions express a subset and
cannot declare typed data objects, data stores, or inline types. See the
Processes guide for JSON/YAML format limitations.
When to Use a DataStore
| Situation | DataStore? |
|---|---|
| A renewal process needs data from the original application | ✅ |
| An invoice process needs the order details | ✅ |
| A dashboard aggregates data across many instances | ✅ |
| You need to write data to another process | ❌ (read-only) |
| Data comes from an external REST API | ❌ (use a serviceTask) |
Two BPMN Elements
<bpmn:dataStore> — declared at <bpmn:definitions> level. Defines what data source this store represents.
<bpmn:dataStoreReference> — placed inside <bpmn:process>. Defines how to query the store from this process.
Declaring a DataStore
<bpmn:dataStore id="LoanStore" name="Loan Applications" itemSubjectRef="LoanSnapshotType">
<bpmn:extensionElements>
<stateway:storeConfig
backingType="processInstances"
definitionKey="loan-application"
identityVariable="loan_id" />
</bpmn:extensionElements>
</bpmn:dataStore>
storeConfig Attributes
| Attribute | Required | Description |
|---|---|---|
backingType | ✅ | processInstances — look up variables from instances of another process definition |
definitionKey | ✅ | The key of the process definition whose instances to query |
identityVariable | ✅ | The variable name in those instances that serves as the lookup key (e.g., loan_id) |
definitionVersion | — | latest (default) or a specific version number |
Placing a DataStoreReference
<bpmn:dataStoreReference id="LoanStoreRef" dataStoreRef="LoanStore">
<bpmn:extensionElements>
<stateway:storeAccess
mode="snapshot"
keyExpression="{{ variables.original_loan_id }}"
stateFilter="instance.status == 'completed' and variables.loan_status == 'disbursed'"
projection="amount, applicant, approved_at, interest_rate"
cardinality="single"
onNotFound="error" />
</bpmn:extensionElements>
</bpmn:dataStoreReference>
storeAccess Attributes
| Attribute | Default | Description |
|---|---|---|
keyExpression | required | Expression that evaluates to the identityVariable value to look up |
mode | snapshot | snapshot: resolve once and copy values; live: re-resolve on every read |
cardinality | single | single: expect exactly 1 result; optional: 0 or 1; list: all results |
onNotFound | error | error: fire ValidationError; null: set target variable to null |
stateFilter | (none) | Optional filter over instance.status and variables.* of the source instances |
projection | (all) | Comma-separated list of variable names to include in the result |
Key Expression
The keyExpression is evaluated against the current instance's variables at runtime:
{{ variables.original_loan_id }} — simple variable reference
{{ variables.order.id }} — nested property
The resulting string is matched against the identityVariable of instances in the target definition.
State Filter
stateFilter restricts which source instances are considered valid:
instance.status == 'completed'
instance.status == 'completed' and variables.loan_status == 'disbursed'
instance.status != 'error'
Supported operators: ==, !=, and, or, not.
Cardinality
| Value | Behavior |
|---|---|
single | Expects exactly 1 matching instance. 0 or 2+ triggers onNotFound behavior or an error |
optional | 0 or 1 matching instances. 0 returns null or fires onNotFound; 1 resolves normally |
list | All matching instances returned as an array |
onNotFound
| Value | Behavior |
|---|---|
error | Fires a ValidationError event. Catch it with a boundary event, or the instance moves to error state |
null | Sets the target variable to null. Execution continues normally |
snapshot vs live
| Mode | When Resolved | Cost |
|---|---|---|
snapshot | Once, when the token first reaches the DataStoreReference element | One DB query, low cost |
live | Every time the variable is accessed during execution | Repeated queries, higher cost |
Use snapshot for stable data (a completed original loan). Use live when the source instance can change while the current process runs and you need the latest value.
live mode executes a database query on every access. A performance warning is logged when a live resolution takes more than 500ms. Configure the threshold with the DATASTORE_LIVE_MODE_THRESHOLD_MS environment variable.
Projection
projection limits which variables are copied from the source instance. Use it to avoid unintentionally copying large or sensitive fields:
projection="amount, applicant, approved_at"
Only amount, applicant, and approved_at are included in the resolved data. All other variables in the source instance are excluded.
Wiring a DataStore into a Task
After declaring the dataStoreReference, wire it to a process variable with a dataInputAssociation on the consuming task:
<!-- Process-level typed variable to hold the resolved data -->
<bpmn:dataObject id="OriginalLoanData" name="originalLoan" itemSubjectRef="LoanSnapshotType" />
<bpmn:dataObjectReference id="OriginalLoanRef" dataObjectRef="OriginalLoanData" />
<bpmn:serviceTask id="enrich" name="Enrich with Original Loan">
<bpmn:dataInputAssociation>
<bpmn:sourceRef>LoanStoreRef</bpmn:sourceRef>
<bpmn:targetRef>OriginalLoanRef</bpmn:targetRef>
</bpmn:dataInputAssociation>
</bpmn:serviceTask>
Before enrich executes, the engine resolves the DataStore, copies the projected variables into originalLoan, and validates them against LoanSnapshotType.
Tenant Isolation
DataStore queries are always scoped to the current tenant. A process belonging to tenant A can never read instances from tenant B, even if it somehow obtains their IDs. Isolation is enforced in the SQL query and cannot be overridden by the process definition.
Observability
Every DataStore resolution is written to the instance audit log. Use the history endpoint to inspect resolutions and measure latency:
curl https://api.stateway.io/v1/instances/inst_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": 12
},
"createdAt": "2026-06-12T10:30:00.000Z"
}
]
}
When resolution fails (onNotFound=error), the action is datastore.not_found with the same payload structure and cardinalityFound: 0.
Complete Example
See the Cross-Process Reference recipe for a full loan-renewal BPMN that uses a DataStore to look up the original loan, with boundary error handling for the not-found case.