Script Processor
Description
The Script Processor component lets you transform data using code you write directly in the scenario. The code is executed on GraalVM polyglot. A single code editor is used, with a language selector. Currently JavaScript and Python are supported.
The value the script returns (via a return statement) is written to the output variable. Its type is inferred
by running the script once when the scenario is validated: objects become records (with typed fields), arrays
become lists, and primitives become their concrete type (Long/Double/String/Boolean). When the type
cannot be determined without a node validation error, the output falls back to Unknown.
Parameters and configuration
| Name | Description |
|---|---|
| Script | The source code to run. Pick the language with the in-editor selector; the editor highlights the chosen language. |
| Output | The name of the output variable that will hold the value returned by the script (type inferred from the script). |
Example
Selecting JavaScript and entering:
return { greeting: "hello", answer: 6 * 7 };
makes the output variable hold a record equivalent to the following JSON:
{
"greeting": "hello",
"answer": 42
}
JavaScript values are converted to plain JVM values: numbers become Long/Double, strings become String,
arrays become List, and objects become Map.
Selecting Python and entering:
return {"greeting": "hello", "answer": 6 * 7}
returns the same shape. Python values are converted to plain JVM values: integers become Long, floats become
Double, strings become String, lists become List, and dictionaries become Map.
Reading scenario data from the script
Scenario data and helper APIs are exposed under a single nu namespace object. To read a scenario variable
(such as the input record or an upstream node's output), use the SpEL-like nu.vars.<name> accessor:
const input = nu.vars.input;
return { doubled: input.amount * 2, who: input.customerId };
nu.vars.<name> reads the variable by name (e.g. nu.vars.input); for names that are not valid identifiers use
the subscript form nu.vars["<name>"]. Records expose their fields both as properties and by key, in both
languages, so input.field (dot) and input["field"] (subscript) work in JavaScript and Python alike.
Arrays are navigated like JavaScript arrays / Python lists (items[0], items.length / len(items)).
return {"doubled": nu.vars.input.amount * 2, "who": nu.vars.input.customerId}
Scenario numbers reach the script as host values. Python operators (+, *, **, comparisons) accept them,
but C-level standard-library functions such as math.pow do not — coerce them first with int(...)/float(...),
e.g. math.pow(float(nu.vars.input.amount), 2). This does not affect JavaScript.
For JavaScript, the editor uses a TypeScript language service (running in a web worker) for semantic completion,
hover and type checking: completion includes members of built-in types (e.g. new Date(). → getDate),
and type mismatches or unknown fields are reported inline. The nu namespace is typed from the scenario's
variables, so nu.vars.input. completes the fields of the input record and flags accesses that do not match
the input's type. Python currently has syntax highlighting only; backend
validation still runs the script to catch syntax errors and always-occurring runtime errors.
This is backed by an extensible layer (ScriptApiProvider): each provider contributes one member of nu.
New capabilities (logging, HTTP, etc.) are added by registering a new provider — without changing the
component itself — and become available as nu.<member>.
Supported JavaScript
The script runs on GraalVM's JavaScript engine, so modern ECMAScript is available: let/const, arrow
functions, classes, template literals, destructuring, spread, closures, try/catch, and the standard
built-ins — JSON, Math, RegExp, Date, Map/Set/WeakMap, Symbol, BigInt, Reflect, Proxy,
typed arrays (Int32Array, ArrayBuffer, …), Intl, parseInt/parseFloat, encodeURIComponent, and the
usual String/Number/Array/Object methods (map, filter, reduce, Object.keys, …). Your code is
wrapped in a function, so it must return its result; without a return the result is undefined
(→ null).
Nussknacker-provided APIs live under the nu namespace:
nu.vars.<name>— read a scenario variable (see above).nu.log— logging:nu.log.info("...")(alsodebug/warn/error) writes to the Nussknacker logs. Use it instead of the engine'sconsole.log/print, which go to the engine's stdout rather than the logs.nu.meta— scenario metadata, mirroring SpEL's#meta:nu.meta.processName(scenario name) andnu.meta.properties(scenario properties).
const input = nu.vars.input;
nu.log.info("processing order " + input.id + " in " + nu.meta.processName);
return input.amount * 2;
Supported Python
Python scripts run on GraalPy through GraalVM polyglot. Your code is wrapped in a generated function, so it
must return its result; without a return the result is None (→ null).
Nussknacker-provided APIs live under the same nu namespace:
nu.vars.<name>— read a scenario variable. Records support both dot (nu.vars.input.field) and dictionary (nu.vars.input["field"]) access.nu.log— logging:nu.log.info("...")(alsodebug/warn/error) writes to the Nussknacker logs.nu.meta— scenario metadata:nu.meta.processNameandnu.meta.properties.
nu.log.info("processing order " + nu.vars.input.id + " in " + nu.meta.processName)
return nu.vars.input.amount * 2
What you can return
The returned value is converted to a plain JVM value:
| Python value | Result in Nussknacker |
|---|---|
| integer | Long |
| float | Double |
| string | String |
| boolean | Boolean |
dict { … } | record / Map |
list [ … ] | List |
None | null |
value read via nu | keeps its structure (record / list) |
JavaScript-specific values and limitations
What JavaScript can return
The returned value is converted to a plain JVM value:
| JavaScript value | Result in Nussknacker |
|---|---|
| integer number | Long |
| fractional number | Double |
| string | String |
| boolean | Boolean |
object { … } | record / Map |
array [ … ] | List |
null / undefined | null |
value read via nu | keeps its structure (record / list) |
Anything else (a function, a symbol, a raw Date, an unresolved Promise, …) has no meaningful conversion
and ends up as its toString form or an empty object — see below.
JavaScript dates
A Date object has no own properties, so returning a Date directly gives an empty object ({}). Convert
it to a value first:
return new Date().toISOString(); // -> String, e.g. "2024-01-31T12:00:00.000Z"
return new Date().getTime(); // -> Long (epoch millis)
return Date.now(); // -> Long (epoch millis)
JSON.stringify also turns dates into ISO strings, so building a JSON string is another option.
new Date() / Date.now() read the real wall-clock time of the engine at the moment the script runs
(per record). Local-time methods and toString() use the time zone of the engine's JVM (its default zone),
while toISOString() is always UTC. There is no fixed/deterministic clock.
JavaScript async / await / Promises — not supported
The script is executed synchronously and there is no event loop, so asynchronous JavaScript does not work:
- There are no timers —
setTimeout/setIntervalare not defined. - Top-level
awaitis a syntax error (the script body is not anasyncfunction), so you cannotawait. - Returning a
Promise— including the result of calling anasyncfunction — yields an empty object; the resolved value is lost. .then(...)/ microtask callbacks run only after the script has already returned, so they cannot influence the result.
In short: write synchronous code and return a plain value. Do not use async/await, Promise, or
callbacks that defer work.
Sandbox — not available
The script is sandboxed. It cannot:
- access the JVM/host — no
Java.type, nojava.*, no reflection, and no calling methods on values obtained fromnu(only field/index reads: a record reads like an object, a list like an array); - do any I/O or networking — no files, no
fetch/XMLHttpRequest, norequire/npm modules; - log (
console.logis not a usable logging API), spawn threads, or accessprocess.
Configuration
A runaway script (e.g. an infinite loop) is guarded by a wall-clock timeout and a GraalVM statement limit.
The value returned by the script is also bounded during conversion to JVM values. These settings are configured
on the base component provider in the model configuration:
components.base.scriptProcessor {
runtimeTimeout: 5 seconds # bounds a single script invocation on the engine (default 5 seconds)
validationTimeout: 2 seconds # bounds the script run during scenario validation (default 2 seconds)
runtimeStatementLimit: 10000000 # bounds guest-language statements per runtime invocation
validationStatementLimit: 1000000 # bounds guest-language statements during scenario validation
resultLimits {
maxDepth: 64 # maximum nesting depth of the returned value
maxArrayElements: 10000 # maximum elements in one returned array/iterator
maxObjectFields: 1000 # maximum fields/entries in one returned object/map
maxStringLength: 1000000 # maximum returned string length
maxTotalConvertedValues: 100000 # maximum total values converted from one script result
}
}
The validation timeout is kept shorter so an editing mistake never hangs the designer. Validation timeouts, statement-limit violations and oversized returned values are reported as node validation errors.
The result limits prevent copying very large returned structures into the JVM/NU data model. They do not bound memory allocated by JavaScript before the value is returned. GraalVM Community Edition cannot enforce a hard per-context memory limit, so a single very large allocation can still exhaust the worker.
Additional considerations
- During scenario validation the backend runs the script over sample data synthesized from the input
types. Syntax errors and errors that always occur — e.g. calling a misspelled
nu.getVar3iable(not a function) — are reported as a node error and block deployment, like SpEL. Values whose type cannot be synthesized are treated permissively, so valid scripts are not false-flagged (and an error that only happens for specific real data is not caught here — it surfaces at runtime). - Type-level hints (type mismatches, unknown fields) are additionally shown inline in the editor by the TypeScript language service; they are advisory and do not block deployment (JavaScript types cannot be enforced on the engine).
- The output type is inferred best-effort: at validation time the script is run over sample data
synthesized from the input variable types (so reading
nu.vars.inputis typed too). For a branching script the inferred type reflects the branch taken for the sample; when the type cannot be determined without a validation error it falls back toUnknown. Convert the output to the expected type when you need a guarantee.