Script-to-Script Bindings
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')
Quick start in 60 seconds
| Step | What you do | Where |
|---|---|---|
| 1 | Create a helper scriptCode = Finance.ConvertSource = #amount * 22.0 | Scripts register (Script reference) |
| 2 | Create a binding row with:fullName = finance.convertscript = Finance.Convert | Scripts / Bindings / Script Invocations |
| 3 | Call it from any other script:@script.finance.convert(100) | anywhere |
Result: the caller receives 2200.
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 caller should be allowed to invoke the binding positionally. |
paramNames | array<string> | when positionalArgs=true | – | Ordered mapping from invocation arguments to named parameters passed to the target script. |
addCallerContext | boolean | – | true | Merge caller's #variables into the callee's context. Explicit binding arguments win on key clash. |
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 usingparamNames.
Example:
{
"fullName": "ticket.addComment",
"script": "Ticket.AddComment",
"positionalArgs": true,
"paramNames": ["orderId", "comment"]
}
@script.ticket.addComment(#order.id, 'Created from gateway')
The callee receives:
{
"orderId": "...",
"comment": "Created from gateway"
}
The target script then works only with named params such as #orderId and
#comment.
Return value = whatever the callee script's last expression evaluates to.
Context variables inside the callee
| Variable | Available when | Contains |
|---|---|---|
| named params from the binding | positionalArgs=true | values mapped by paramNames, for example #orderId, #comment |
| keys from caller map | positionalArgs=false | whatever the caller passed in the single map argument |
| (caller variables) | addCallerContext=true | every #variable the caller had, unless shadowed by an explicit argument key |
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………………"
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.
Error handling
| Exception | Thrown when | Typical fix |
|---|---|---|
ApiConfigurationException | no binding row matches fullName | Add or correct the row in Script Invocations. |
ApiConfigurationException | positionalArgs=true but paramNames is missing or invalid | Define config.paramNames as a list of unique target param names. |
ApiValidationException | argument style mismatch (too many positional params, wrong type) | Pass a single map or set positionalArgs=true. |
ApiValidationException | positional call count does not match the number of configured paramNames | Adjust the call or the binding mapping. |
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).
Since the callee shares the caller's transaction, any entity changes made by the callee are committed or rolled back together with the caller. For more on transaction boundaries, see SpEL and Transactions.
Using paramsFormCode and resultFormCode
When a target script defines paramsFormCode and/or resultFormCode, those tSM Forms act as a typed contract for the binding:
| Script field | Effect on script-to-script binding |
|---|---|
paramsFormCode | The JSON Schema embedded in the form validates the parameter map at call time. Callers get autocomplete and inline documentation in the SpEL Console. |
resultFormCode | Documents the return structure so callers know what to expect. Currently informational — no runtime enforcement. |
Because a tSM Form is simultaneously a JSON Schema (data model) and a UI definition, a single artefact covers three concerns at once:
- Validation — parameters are checked against the schema before execution; type mismatches surface immediately.
- Documentation — field labels, descriptions, and constraints serve as living API docs for anyone calling the script.
- UI — the SpEL Console (and BPMN modeler) renders the form so users can fill in parameters interactively instead of writing raw JSON.
Always define paramsFormCode on scripts that are bound as reusable helpers. It makes the binding self-documenting and prevents callers from passing invalid data.
See also
- Script (reference) — script definition, types, parameter forms, and attributes.
- SpEL Console — developing and debugging SpEL scripts interactively.
- Event Bindings — trigger scripts on entity lifecycle events.
- REST Bindings — expose scripts as HTTP endpoints.
- MCP Bindings — expose scripts as AI-callable tools.
- SpEL and Transactions — how transactions affect SpEL scripts.
- Script Binding in BPMN — calling scripts from BPMN processes.