SpEL Bindings
The tSM SpEL binding layer connects your expressions to the outside world:
from reacting to live platform events to calling one script from another or publishing scripts as HTTP endpoints.
1 Essentials in a Nutshell
Binding type | Trigger | Typical use-case |
---|---|---|
Events | Internal lifecycle event (order created, ticket closed, timer fired …) | Automate business logic whenever something happens in the platform |
Script-to-script | @script.myPackage.myScript(params) | Re-use a SpEL script from inside another script |
REST | POST /scripts/package/my-script | Expose a script to external systems over HTTP |
2 Event Bindings
2.1 What is an Event Binding?
An event binding is a SpEL script that tSM automatically executes when a predefined platform event occurs. Think of it as a server-side trigger written in SpEL.
Key concept: The binding (“when to run which script”) lives in a system register, while the script code itself lives in the tSM Scripts catalogue.
2.2 Register-based Configuration
Bindings are configured in the register “Scripts / Bindings / Entity Events” (internal code Scripts.Bindings.EntityEvents
). Each row corresponds to one binding and is described by the following JSON schema (simplified):
Property | Type | Required | Description |
---|---|---|---|
entityType | string | ✔ | Logical name of the entity that raises the event (e.g. Ticket , Order ). Autocomplete comes from the built-in Entity-Type LOV widget. |
eventName | string | ✔ | Lifecycle event – one of CREATE , UPDATE , DELETE . |
script | string | ✔ | Code of the SpEL script to execute, chosen via the Script LOV. |
async | boolean | – | Run async: dispatch the event after the DB transaction commits (good for heavy work). Default =false (synchronous). |
privateEntity | boolean | – | Use internal model: pass the private (managed) entity instance to the script instead of the documented public model. Use only for low-level tweaks; the internal API may change without notice. |
Example binding (as stored in the register):
{
"entityType" : "Ticket",
"eventName" : "UPDATE",
"script" : "Ticket.Events.DescriptionToUppercase",
"async" : false
}
2.3 Minimal Working Example
-
Register row – as above.
-
Script – create a new entry in Scripts with code
Ticket.Events.DescriptionToUppercase
and source:#ticket.description = #ticket.description.uppercase()
Save both. The next time a Ticket UPDATE happens, the description is converted to uppercase.
2.4 Authoring the SpEL Script
The binding row only tells which script to run. The script itself lives in the tSM Scripts register (main menu → Scripts).
- Code – must match the value used in the binding.
- Source – the actual SpEL text.
Organization – keep event scripts short and focused. If you need more than ~50 lines, delegate into a service class or another SpEL helper.
2.5 Execution Context & Transactionality
- Scripts run inside the same DB transaction as the change that triggered them – unless
async=true
, in which case they execute right after commit (read-only snapshot; failures are retried × 3 with exponential back-off before landing in the dead-letter queue). - A context variable with the name of the entity type (e.g.
#ticket
) is available in the script. - All standard SpEL helpers work (
#now()
,#if()
,#with()
, …).
2.6 Context Variables
For every binding tSM injects one primary context variable whose name is the entity type with the first letter lower-cased:
Ticket
→#ticket
Order
→#order
ProcessInstance
→#processInstance
When the binding row has privateEntity=true
an additional variable with
a Private
suffix appears, pointing to the managed Hibernate instance:
Ticket
→#ticketPrivate
Order
→#orderPrivate
The public model (#ticket
, #order
, …) is still available even in
private-entity bindings, so you can mix safe DTO reads with occasional direct
manipulations.
You are free to read and write the context object; any changes are flushed
to the database unless the binding is asynchronous (async=true
) — in that
case the object is a detached, read-only snapshot and modifications are
silently ignored.
2.7 Debugging & Testing
-
Kibana – every execution is logged; search by the
traceId
MDC key to correlate the full flow. -
Manual log in SpEL:
@log.log.create({ logTypeCode : "Test.Log", request : "Ticket description changed to " + #ticket.description })
-
JUnit – use
EventBindingTestSupport
to fire events in isolation.
2.8 Performance Tips
Pattern | Advice |
---|---|
Heavy IO (HTTP, mail) | Mark binding async=true or enqueue work via @taskScheduler . |
Long-running loops | Batch queries; aim for < 100 ms per event. |
Private vs public entity | Prefer public DTO; set privateEntity=true only when you must bypass standard hooks. |
2.9 Advanced Patterns & Gotchas
2.9.1 Field-specific UPDATE bindings
Short-circuit early when the change is irrelevant:
#if(!#changedFields.containsKey('description')).then('skip')
2.9.2 Multiple bindings on the same event
All bindings matching (entityType
,eventName
) are executed.
Order is deterministic: ascending by the script
code.
2.9.3 Async caveat
Async bindings receive a detached, read-only snapshot. Direct modifications to the entity are ignored; use services instead.
3 Script-to-Script Binding (@script.…
)
3.1 Why you need it — the problem it solves
Large SpEL automations quickly end up copy-pasting common snippets (currency
conversion, e-mail templates, date math …).
The script-to-script binding lets you refactor that boiler-plate into a
single helper script and call it from anywhere with a one-liner:
@script.finance.convert(#amount, 'USD', 'CZK')
3.2 Quick start in 60 seconds
Step | What you do | Where |
---|---|---|
1 | Create a helper scriptCode = Finance.Convert Source = #amount * 22.0 | Scripts register |
2 | Create a binding row with:fullName = finance.convert script = Finance.Convert | Scripts / Bindings / Script Invocations |
3 | Call it from any other script:@script.finance.convert(100) | anywhere |
Result: the caller receives 2200
.
3.3 Register-row schema (Scripts.Bindings.ScriptInvocations
)
Field | Type | Required | Default | Description |
---|---|---|---|---|
fullName | string | ✔ | – | The fully-qualified name you’ll type after @script. |
script | string | ✔ | – | LOV to the target script code in the Scripts register. |
positionalArgs | boolean | – | false | Turn ON if the callee expects positional parameters (param1 , param2 , …). |
addCallerContext | boolean | – | true | Merge caller’s #variables into the callee’s context (caller wins on key clash). |
3.4 Call syntax
// named variables
@script.<package>.<code>({
key1: "value1",
key2: "value2",
})
// positional argumemts
@script.<package>.<code>( arg1, arg2, … )
- If
positionalArgs=false
: Pass one map –@script.util.slugify({ "text" : #title })
- If
positionalArgs=true
: Positional parameters land in the callee as#p0
,#p1
, … and an#args
array.
Return value = whatever the callee script’s last expression evaluates to.
3.5 Context variables inside the callee
Variable | Available when | Contains |
---|---|---|
#args | positionalArgs=true | full Object[] of arguments |
#p0 , #p1 , … | positionalArgs=true | individual positional parameters |
(caller variables) | addCallerContext=true | every #variable the caller had, unless shadowed by an explicit argument key |
3.6 Example with named-argument map
Helper script – String.Pad
#text.padEnd(#width, #char ?: ' ')
Binding row
{
"fullName": "string.pad",
"script": "String.Pad",
"positionalArgs": false
}
Caller script
#padded = @script.string.pad({
"text" : #name,
"width" : 20,
"char" : '.'
})
#padded
→"Invoice………………"
3.7 Error handling
Exception | Thrown when | Typical fix |
---|---|---|
ApiConfigurationException | no binding row matches fullName | Add or correct the row in Script Invocations. |
ApiValidationException | argument style mismatch (too many positional params, wrong type) | Pass a single map or set positionalArgs=true . |
3.8 Performance notes
- Bindings are cached for 5 minutes. Binding edits become visible after the cache expires.
- The callee runs in the same transaction / thread as the caller (no extra overhead).
4 REST Bindings (/scripts/…
) — TODO
- Auto-generated endpoint: POST /scripts/<package>/<code>
* Body → params map
* Response → JSON result of the script
- Auth: standard-bearer token (include X-Tsm-Tenant for multi-tenant).
- Example curl:
curl -X POST https://tsm.example.com/scripts/demo/hello \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{ "name": "world" }'