Skip to main content

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
entityTypestringLogical name of the entity that raises the event (e.g. Ticket, Order). Autocomplete comes from the built-in Entity-Type LOV widget.
eventNamestringLifecycle event – one of CREATE, UPDATE, DELETE.
scriptstringCode of the SpEL script to execute, chosen via the Script LOV.
asyncbooleanRun async: dispatch the event after the DB transaction commits (good for heavy work). Default =false (synchronous).
privateEntitybooleanUse 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
fullNamestringThe fully-qualified name you’ll type after @script.
scriptstringLOV to the target script code in the Scripts register.
positionalArgsbooleanfalseTurn ON if the callee expects positional parameters (param1, param2, …).
addCallerContextbooleantrueMerge 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

VariableAvailable whenContains
#argspositionalArgs=truefull Object[] of arguments
#p0, #p1, …positionalArgs=trueindividual positional parameters
(caller variables)addCallerContext=trueevery #variable the caller had, unless shadowed by an explicit argument key

3.6 Example with named-argument map

Helper scriptString.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

ExceptionThrown whenTypical fix
ApiConfigurationExceptionno binding row matches fullNameAdd or correct the row in Script Invocations.
ApiValidationExceptionargument 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" }'