SpEL and Transactions
This page covers how transactions affect SpEL scripts — specifically how external calls behave within a transaction, how to use the {async: true} flag, and common pitfalls with #businessException. For general transaction concepts, see the Transactions overview.
The Problem: External Calls in SpEL Scripts
Every SpEL script runs inside a database transaction. All changes to tSM entities within the same microservice are part of this transaction and are committed or rolled back together.
However, when a SpEL script calls a different microservice, an external system — via @tsmRestClient, @tsmSoapClient, or any connector — that call happens immediately and is not part of the database transaction. If the script fails later (e.g., validation with #businessException), the database rolls back, but the external call cannot be undone.
Practical Example: JIRA + SAP Integration
Consider a script that must:
- Create a task in JIRA
- Add a stock record in SAP
- Validate and update the Order
The Naive Approach (Broken)
// Step 1: Call JIRA immediately (non-transactional!)
#jiraResponse = @tsmRestClient.post('jira', '/rest/api/2/issue')
.body({
fields: {
project: { key: 'PROJ' },
summary: 'Install equipment at ' + #order.address,
description: 'Order: ' + #order.code,
issuetype: { name: 'Task' }
}
})
.execute();
// Step 2: Call SAP immediately (non-transactional!)
@tsmRestClient.post('sap', '/sap/stock/create')
.body({
materialId: #order.productCode,
quantity: 1,
warehouse: 'WH-01'
})
.execute();
// Step 3: Validation — oops, this might fail!
#if (#order.customerSegment == 'BLOCKED')
.then(#businessException('VALIDATION_ERROR', 'Customer segment is blocked, cannot proceed.'));
// Step 4: Save result to Order
#order.status = 'Dispatched';
What Goes Wrong?
The user sees an error and the Order is correctly unchanged. But there is now a phantom JIRA task and a phantom stock record in SAP that nobody knows about. This is a data inconsistency bug.
Do not make non-transactional calls (REST, SOAP, etc.) before validation. If a #businessException (or any error) can occur after the call, the external effect cannot be undone.
The Correct Approach: {async: true} Flag
The {async: true} query-option is available on most tSM SpEL clients and on @tsmRestClient. When used, the call is not executed immediately — instead, a Kafka message is queued and only dispatched after the database transaction commits.
If the transaction rolls back (e.g., due to #businessException), the Kafka message is never sent — the external system is never called.
// Step 1: Validate FIRST (before any external calls!)
#if (#order.customerSegment == 'BLOCKED')
.then(#businessException('VALIDATION_ERROR', 'Customer segment is blocked, cannot proceed.'));
// Step 2: Call JIRA via async (queued for after commit)
@tsmRestClient.post('/rest/api/2/issue')
.connector('jira')
.body({
fields: {
project: { key: 'PROJ' },
summary: 'Install equipment at ' + #order.address,
description: 'Order: ' + #order.code,
issuetype: { name: 'Task' }
}
})
.async()
.execute();
// Step 3: Update stock asynchronously
@tsmRestClient.post('/sap/stock/create')
.connector('sap')
.body({
materialId: #order.productCode,
quantity: 1,
warehouse: 'WH-01'
})
.async()
.execute();
// Step 4: Save result to Order (transactional, in DB)
#order.status = 'Dispatched';
How It Works (Success)
How It Works (Validation Failure)
When to Use {async: true}
- You don't need the response from the external system (fire-and-forget).
- The external operation should only happen if the transaction succeeds.
- This is the recommended approach for most external integrations from SpEL scripts.
Since the call is fire-and-forget, you cannot read the response (e.g., the JIRA issue key). The call returns null immediately. If you need the response, use an External Task or Kafka Task in the process engine instead.
Using {async: true} with SpEL Clients
The same pattern works with tSM SpEL clients (not just @tsmRestClient). Pass {async: true} as a query-option:
// Create a comment — only sent after the transaction commits
@comment.comment.create({
ownerId: #order.id,
ownerType: 'Order',
comment: 'JIRA task created for installation.'
}, {async: true});
// Send a notification — only sent after commit
@notification.notification.send({
templateCode: 'ORDER_DISPATCHED',
ownerId: #order.id,
ownerType: 'Order'
}, {async: true});
// Cancel billing document — only sent after commit
@billing.billingDocument.patch(
#id,
{state: 'Cancelled'},
{async: true}
);
Rules of Thumb
| Situation | What to do |
|---|---|
| Validate input | Always validate before any external call |
| Create a comment / send a notification | Use {async: true} — fire-and-forget |
| Call an external REST API (don't need response) | Use .async().execute() on @tsmRestClient |
| Call an external REST API (need response) | Use an External Task in the process |
| Update a tSM entity (Order, Ticket, etc.) | Normal call — it's part of the database transaction |
| Multiple external calls in sequence | Use {async: true} for each, or use External Tasks for ordering guarantees |
See Also
- Transactions overview — general transaction concepts and strategies
- Process Engine Transactions — wait states, external tasks, SAGA pattern
- Script (reference) — script definition, types, parameter forms, and execution context
- SpEL Console — developing and debugging SpEL scripts interactively
- Event Bindings — synchronous vs. async event bindings and their transaction behavior
- REST Bindings — each request runs in its own transaction
- MCP Bindings — each tool call runs in its own transaction
- SpEL Clients — full reference on query-options including
{async: true} - SpEL Connectors —
@tsmRestClient,@tsmSoapClient,@tsmDatabaseClient