Skip to main content
Version: 2.3

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 typeTriggerTypical use-case
EventsInternal 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
RESTPOST /scripts/package/my-scriptExpose 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):

PropertyTypeRequiredDescription
entityTypestring✔Logical name of the entity that raises the event (e.g. Ticket, Order). Autocomplete comes from the built-in Entity-Type LOV widget.
eventNamestring✔Lifecycle event – one of CREATE, UPDATE, DELETE.
scriptstring✔Code of the SpEL script to execute, chosen via the Script LOV.
asyncboolean–Run async: dispatch the event after the DB transaction commits (good for heavy work). Default =false (synchronous).
privateEntityboolean–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​

  1. Register row – as above.

  2. 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​

PatternAdvice
Heavy IO (HTTP, mail)Mark binding async=true or enqueue work via @taskScheduler.
Long-running loopsBatch queries; aim for < 100 ms per event.
Private vs public entityPrefer 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​

StepWhat you doWhere
1Create a helper script
Code = Finance.Convert
Source = #amount * 22.0
Scripts register
2Create a binding row with:
fullName = finance.convert
script = Finance.Convert
Scripts / Bindings / Script Invocations
3Call it from any other script:
@script.finance.convert(100)
anywhere

Result: the caller receives 2200.

3.3 Register-row schema (Scripts.Bindings.ScriptInvocations)​

FieldTypeRequiredDefaultDescription
fullNamestring✔–The fully-qualified name you’ll type after @script.
scriptstring✔–LOV to the target script code in the Scripts register.
positionalArgsboolean–falseTurn ON if the caller should be allowed to invoke the binding positionally.
paramNamesarray<string>when positionalArgs=true–Ordered mapping from invocation arguments to named parameters passed to the target script.
addCallerContextboolean–trueMerge caller’s #variables into the callee’s context. Explicit binding arguments win 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: the binding translates the positional call into a named parameter map using paramNames.

Example:

{
"fullName": "ticket.addComment",
"script": "Ticket.AddComment",
"positionalArgs": true,
"paramNames": ["orderId", "comment"]
}
@script.ticket.addComment(#order.id, 'Created from gateway')

The callee receives the named parameters #orderId and #comment.

Return value = whatever the callee script’s last expression evaluates to.

3.5 Context variables inside the callee​

VariableAvailable whenContains
named params from the bindingpositionalArgs=truevalues mapped by paramNames, for example #orderId, #comment
keys from caller mappositionalArgs=falsewhatever the caller passed in the single map argument
(caller variables)addCallerContext=trueevery #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 Example with positional-to-named translation​

Helper script – Ticket.AddComment

#ticketService.addComment(#orderId, #comment)

Binding row

{
"fullName": "ticket.addComment",
"script": "Ticket.AddComment",
"positionalArgs": true,
"paramNames": ["orderId", "comment"]
}

Caller script

@script.ticket.addComment(#ticket.id, 'Validated by workflow')

The target script receives the named params #orderId and #comment.

3.8 Error handling​

ExceptionThrown whenTypical fix
ApiConfigurationExceptionno binding row matches fullNameAdd or correct the row in Script Invocations.
ApiConfigurationExceptionpositionalArgs=true but paramNames is missing or invalidDefine config.paramNames as a list of unique target param names.
ApiValidationExceptionargument style mismatch (too many positional params, wrong type)Pass a single map or set positionalArgs=true.
ApiValidationExceptionpositional call count does not match the number of configured paramNamesAdjust the call or the binding mapping.

3.9 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" }'