Skip to main content
Version: Current

Script Processor

Mode: Streaming Mode: Request-Response

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

NameDescription
ScriptThe source code to run. Pick the language with the in-editor selector; the editor highlights the chosen language.
OutputThe 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}
note

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("...") (also debug/warn/error) writes to the Nussknacker logs. Use it instead of the engine's console.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) and nu.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("...") (also debug/warn/error) writes to the Nussknacker logs.
  • nu.meta — scenario metadata: nu.meta.processName and nu.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 valueResult in Nussknacker
integerLong
floatDouble
stringString
booleanBoolean
dict { … }record / Map
list [ … ]List
Nonenull
value read via nukeeps its structure (record / list)

JavaScript-specific values and limitations

What JavaScript can return

The returned value is converted to a plain JVM value:

JavaScript valueResult in Nussknacker
integer numberLong
fractional numberDouble
stringString
booleanBoolean
object { … }record / Map
array [ … ]List
null / undefinednull
value read via nukeeps 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 timerssetTimeout/setInterval are not defined.
  • Top-level await is a syntax error (the script body is not an async function), so you cannot await.
  • Returning a Promise — including the result of calling an async function — 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, no java.*, no reflection, and no calling methods on values obtained from nu (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, no require/npm modules;
  • log (console.log is not a usable logging API), spawn threads, or access process.

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.input is 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 to Unknown. Convert the output to the expected type when you need a guarantee.