Skip to main content
Expressions are sandboxed: T(...) resolves only a short allowlist of safe types and new is blocked (Calling Java). Your own code enters through one door, a registered function: a public static Java method the console loads at startup and every expression can call as #name(args). This page is the worked example, from the class to the container, with what happens when a step is wrong.
Everything here was run against the published alginte/alginte:0.11.0 image, and the two behaviours a function can have, a value and an exception, are rows in the fidelity corpus that asserts the editor preview, the validator and the deployed runtime agree.

1. Write the class

No dependency on Alginte, no annotation, no interface. A class with a public static method:
Compile it for the console’s Java and pack it:
Three rules the registry enforces at startup, each with its own message (see When it cannot resolve):
  • The method is public static. Instance methods are not callable.
  • The name is unambiguous in its class: no overloads. Two maskEmail methods with different parameter lists abort startup, even if only one would ever match.
  • Arguments arrive as what the expression evaluates to (a String from value.get('email') on a JSON Schema or Avro record, boxed numbers, maps, lists), and the return value is what the expression continues with. Kafka Streams calls the function on a stream thread, once per record, so it must be thread-safe and should not block.

2. Register it

One property per function, the name you will call it by on the left and class#method on the right:
The function name is a map key, which decides how you pass it. A JVM system property keeps its case:
An environment variable does not: Spring lower-cases map keys bound from the environment, so KAFKA_STREAMS_TRANSFORMATION_FUNCTIONS_MASKEMAIL registers #maskemail. Either name works; use the one you registered. A mounted properties file passed with spring.config.additional-location keeps the case too (Configuration).

3. Put the jar on the classpath

The Docker image runs Spring Boot’s launcher over the exploded application in /app, and the launcher lists /app/BOOT-INF/lib/ at start. So a jar file mounted there is on the classpath with nothing else changed:
Mount the file, not a directory: /app is owned by root and the launcher does not descend into subdirectories of lib/. For a directory of jars, switch to the PropertiesLauncher and point loader.path at the mount; the application’s own classes and libraries stay on the classpath:
-Dloader.path under the image’s default entrypoint does nothing: the default launcher ignores it, and startup aborts with class not found on the classpath, the same message a missing mount produces. Read that message as “the jar is not on the classpath”, not as a typo in the class name.
The console starts only when every registered function resolved, so a healthy /actuator/health after startup is the proof the jar was found.

4. Call it

In any expression, on any operator:
The sample preview evaluates it against the real record like any other expression, so john.doe@example.com on the in line reads j***@example.com on the out line before anything is deployed. The validator knows the name: a call to a function that is not registered is reported while you type, as an unknown function. Completion offers the generic #fn() snippet rather than your function’s name. A function that throws fails the record the same way everywhere: the preview reports the exception on the step, the validator marks the expression, and the deployed stream fails the record’s transformation with the same exception, as it does for any expression error. There is no path on which the throw is swallowed.

When it cannot resolve

Resolution is fail-fast: the console does not start, and the message names the function and the reason. The four cases:

What the sandbox does and does not cover

The expression sandbox restricts what an expression can reach: the T(...) allowlist and the blocked new operator keep a one-line expression from calling into the process. A registered function is outside that boundary by design. It is your Java, running in the console’s process with the console’s rights, and nothing inspects what it does. Register only code you would run in the console anyway, and keep the trusted network boundary the console assumes (Production): whoever can author expressions can call every registered function with any argument.