SpEL Syntax
This page describes the user-facing syntax of tSM SpEL: how to write expressions, combine them into scripts, work with collections, call tSM services, and define lambdas.
SpEL in tSM is based on Spring Expression Language, but it runs as a safe, curated subset. In practice, that means you should rely on:
- literals, operators, and collection syntax
- built-in functions and type extensions
- context variables such as
#orderor#task - public tSM services exposed as
@service
Do not assume that arbitrary Java or Kotlin classes are directly available in scripts.
1 Running Scripts
1.1 Opening the SpEL Console
| Mode | How to open | When to use |
|---|---|---|
| Without context | Open the main menu search, type spel, and select SpEL Console. | Quick experiments and standalone expressions. |
| With context | Open an entity such as an order or ticket, open the top-right menu, and choose SpEL Console. | Scripts that depend on context variables such as #order, #ticket, or #task. |
1.2 Useful Shortcuts
| Shortcut | Action |
|---|---|
Ctrl + Space | Open autocomplete suggestions. |
Ctrl + Space twice | Show details for the selected suggestion. |
Ctrl + Enter | Evaluate the whole script. |
Select text + Ctrl + Enter | Evaluate only the selected expression. |
@ then Ctrl + Space | List available tSM services. |
# then Ctrl + Space | List built-in functions, variables, and flow-control helpers. |
Enter inside () | Insert example arguments for the selected method. |
Most examples on this page can be pasted directly into the SpEL Console.
1.3 Multi-expression Scripts
A script can contain multiple expressions. Expressions may be separated by:
- a semicolon
; - a line break
- other whitespace
The value of the last expression is the result of the whole script.
#orderTotal = 250
#discount = 50
#finalTotal = #orderTotal - #discount
'Final total: ' + #finalTotal
Using semicolons is optional, but it is usually clearer when several expressions share one line:
#a = 10; #b = 20; #a + #b
Inside helper constructs that already take a list of expressions, use commas, not semicolons. This applies for example to #do(...), #with(...).do(...), and branches such as .then(...) or .else(...).
#if(#orderTotal > 100)
.then(#discount = 10, #orderTotal - #discount)
.else(#orderTotal)
2 Core Expressions
2.1 Literals
'Hello World' // string
"Hello World" // string
1234 // integer
123.45 // decimal number
0xFF // hexadecimal number
6.0221415E+23 // scientific notation
true // boolean
false // boolean
null // null
2.1.1 String Interpolation
Double-quoted strings support interpolation with #{...}. The content inside #{...} is a normal SpEL expression.
#name = 'John'
"Hello #{#name}" // "Hello John"
#price = 100
#vat = 0.21
"Total #{#price * (1 + #vat)}" // "Total 121.0"
Interpolation is supported only in double-quoted strings. Single-quoted strings stay literal:
#name = 'John'
'Hello #{#name}' // "Hello #{#name}"
Under the hood, interpolation behaves like ordinary string concatenation. That means the embedded expression is evaluated normally and then combined into the resulting string.
2.2 Variables and Assignment
Variables use the #name form. Assign with =.
#message = 'Hello'
#count = 42
#currentUserId = #currentUser()
Once assigned, the variable can be reused later in the same script.
#name = 'John Doe'
'Hello, ' + #name + '!'
2.3 Common Data Shapes
#text = 'Hello world'
#number = 123
#flag = true
#list = ['order', 'ticket', 'process']
#map = {'customerName': 'Alpha', 'employees': 100}
#date = #now()
#uuid = #randomUUID()
#nothing = null
Under the hood, many values are ordinary Java or Kotlin runtime objects such as strings, numbers, lists, maps, dates, and DTOs returned from tSM services. You can use their allowed methods and the documented tSM extensions, but not arbitrary platform classes.
2.4 Operators
2.4.1 Arithmetic
Operators: +, -, *, / or div, % or mod, ^
1 + 2 // 3
5 - 2 // 3
3 * 4 // 12
10 / 2 // 5
10 div 2 // 5
10 % 3 // 1
10 mod 3 // 1
2 ^ 3 // 8
2.4.2 Comparison
Operators: == or eq, != or ne, < or lt, > or gt, <= or le, >= or ge
1 == 1
1 != 2
1 < 2
2 > 1
1 <= 1
2 >= 1
'abc' == 'abc'
2.4.3 Logical
Operators: && or and, || or or, ! or not
true && false
true || false
!true
not false
2.5 Accessing Data
2.5.1 Object Properties and Map Keys
Use dot access for properties and map-like keys, or bracket access when the key is dynamic.
#customer = {
'name': 'Your Company',
'address': {'street': 'Na padesatem', 'city': 'Prague'}
}
#customer.name
#customer.address.street
#customer['name']
#customer['address']['city']
2.5.2 List Items
Use zero-based indexing with [index].
#items = ['order', 'task', 'process']
#items[0] // 'order'
2.5.3 Selection and Projection
Use selection to filter a collection and projection to transform it.
- selection:
.?[condition] - projection:
.![expression]
Inside selection or projection, the current item is available as #this.
#services = [
{'name': 'Internet', 'price': 10},
{'name': 'TV', 'price': 15},
{'name': 'Phone', 'price': 5}
]
#expensive = #services.?[#this.price > 9]
#names = #services.![#this.name]
#expensiveNames = #services.?[#this.price > 9].![#this.name]
3 Null Handling and Control Flow
3.1 Safe Navigation
Use ?. when a value may be null.
#order = {'customer': null}
#order.customer?.name // null
3.2 Elvis Operator
Use ?: to provide a fallback value when the left side is null.
#customerName = null
#customerName ?: 'Unknown Customer'
3.3 Ternary Operator
Use condition ? whenTrue : whenFalse for short conditional expressions.
#orderTotal = 150
#orderTotal > 100 ? 'Large Order' : 'Small Order'
3.4 Condition-based Flow Helpers
tSM SpEL also provides fluent helpers for multi-step conditions:
#if(condition).then(expr...).elseif(condition).then(expr...).else(expr...)#match(value).when(expected, expr...).else(expr...)#case().when(condition, expr...).else(expr...)
#total = 99
#if(#total > 200)
.then(#discount = 0.20)
.elseif(#total > 100)
.then(#discount = 0.10)
.else(#discount = 0.05)
#technology = '5G'
#match(#technology)
.when('5G', 'mobile')
.else('fixed')
3.5 Local Blocks
Use these helpers when you want several expressions to share the same local scope:
#do(expr1, expr2, ..., exprN)evaluates expressions in order and returns the last result#with(expr1, expr2, ...).do(exprN)creates a local block and then evaluates the final expression
#with(
#orderTotal = 250,
#discountRate = 0.10,
#discountedTotal = #orderTotal - (#orderTotal * #discountRate)
).do(
#discountedTotal > 100 ? 'Large Order' : 'Small Order'
)
3.6 Error Handling
Use #try(...).catch(...) to recover from runtime errors. Inside .catch(...), the thrown exception is available as #error.
#try(
#uuid('not-a-uuid')
).catch(
'Invalid input: ' + #error.message
)
4 Collections, Iteration, and Lambdas
4.1 forEach
forEach is a list helper with this shape:
#list.forEach(#item, expr1, expr2, ..., exprN)
Rules:
- the first argument must be a variable reference such as
#item - every following expression is evaluated for each element
- the value of the last expression becomes the output value for that element
#indexcontains the current zero-based position
[1, 2, 3, 4].forEach(#n, #n * #n) // [1, 4, 9, 16]
['apple', 'banana'].forEach(
#item,
'Item ' + #index + ': ' + #item
)
4.2 Lambda Expressions
tSM SpEL supports first-class lambdas.
Syntax
(#param1, #param2, ..., #paramN) -> expression
Examples:
(#x) -> #x * 2
() -> 'Hello'
(#a, #b) -> #a + #b
A lambda body is a single expression. If you need several sequential expressions, wrap them in #do(...).
#net = (#amount, #rate) -> #do(
#gross = #amount * (1 + #rate),
#discount = #gross > 1000 ? 50 : 0,
#gross - #discount
)
Invocation
Assign the lambda to a variable and call it with normal function-call syntax:
#double = (#x) -> #x * 2
#double(10) // 20
The call must provide the same number of arguments as the lambda declares.
Lambdas can also be stored in collections, returned from other lambdas, or passed as arguments.
#makeAdder = (#x) -> (#y) -> #x + #y
#addFive = #makeAdder(5)
#addFive(7) // 12
#apply = (#fn, #value) -> #fn(#value)
#square = (#x) -> #x * #x
#apply(#square, 6) // 36
Captured Variables
Lambdas in tSM SpEL capture surrounding variables as live bindings. If an outer variable changes later, the lambda sees the updated value.
#factor = 2
#mul = (#x) -> #x * #factor
#factor = 4
#mul(3) // 12
In this example, #mul uses the current value of #factor when it is invoked.
If you create another lambda after changing the variable, both lambdas still read the same live outer binding:
#factor = 2
#mulByTwo = (#x) -> #x * #factor
#factor = 4
#mulByFour = (#x) -> #x * #factor
#do(
#old = #mulByTwo(3),
#new = #mulByFour(3),
'old=' + #old + ', new=' + #new
) // 'old=12, new=12'
The same rule applies to nested lambdas. An inner lambda keeps access to variables from the outer lambda scope:
#makeCounter = () -> #do(
#count = 0,
(#delta) -> #do(
#count = #count + #delta,
#count
)
)
#counter = #makeCounter()
#first = #counter(2)
#second = #counter(3)
'first=' + #first + ', second=' + #second // 'first=2, second=5'
Variable Assignment Inside Lambdas
When you assign a variable inside a lambda, SpEL checks whether that name already exists in the surrounding code:
- Variable already exists outside — the assignment updates the existing variable. The change is visible both inside and outside the lambda.
- Variable is new — the assignment creates a private variable that only exists inside the lambda. Code outside the lambda cannot see it.
This works the same way whether the outer variable was defined in another lambda, at the top level of your script, or comes from the platform context (e.g., #order, #task).
Example — updating an existing variable:
#total = 0
#add = (#value) -> #do(
#total = #total + #value,
#total
)
#add(2)
#add(3)
#total // 5
#total was defined before the lambda, so the lambda updates it directly.
Example — creating a private variable:
#compute = (#value) -> #do(
#tmp = #value + 1,
#tmp
)
#compute(4) // 5
#tmp // null
#tmp did not exist before, so it stays private to the lambda and is not visible outside.
Lambda parameters always take priority over outer variables with the same name:
#value = 10
#identity = (#value) -> #value
#identity(3) // 3
Lambdas with Collection Operations
Lambdas are useful when you want to name a transformation or predicate and reuse it:
#square = (#n) -> #n * #n
#isEven = (#n) -> #n % 2 == 0
#squares = [1, 2, 3, 4].![#square(#this)]
#squares.?[#isEven(#this)] // [4, 16]
5 Runtime-specific Features
5.1 Built-in Functions and Type Extensions
tSM provides two kinds of reusable helpers:
- type extensions such as
'Hello'.uppercase() - standalone functions such as
#now()or#randomUUID()
See SpEL Built-in Functions for the full catalogue.
5.2 Context Variables
Context variables expose the current runtime state. The exact variables depend on where the script runs.
#order.productCodes()
#task.complete()
#execution.processInstance
#ticket.status
5.3 tSM Services
tSM services are exposed with the @serviceName syntax. They provide access to platform APIs and business operations.
@user.user.get(#currentUser()).name
@crm.customer.find({'key': 'CUST-25'}).first()
Use autocomplete in the SpEL Console or the public API documentation to explore available services and methods.
6 Practical Guidance
- Prefer short, composable expressions over large monolithic scripts.
- Use
#do(...)when an expression needs several sequential steps. - Use lambdas when a transformation or predicate needs to be reused.
- Use semicolons when script structure would otherwise be ambiguous.
- Prefer built-in functions and tSM services over assumptions about underlying Java classes.
For more cookbook-style examples, see SpEL Examples.