Skip to main content
Version: 2.4

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:

  1. Create a task in JIRA
  2. Add a stock record in SAP
  3. 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.

Never Do This

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}

Best Practice
  • 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.
Limitation

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

SituationWhat to do
Validate inputAlways validate before any external call
Create a comment / send a notificationUse {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 sequenceUse {async: true} for each, or use External Tasks for ordering guarantees

See Also