Create a node in Java
Scaffold a Java package and node with the Axiom CLI, implement the handler against AxiomContext, test it with Maven, and push it to the platform.
View as MarkdownA Java node is a single public static method on a class under nodes/: it
takes an AxiomContext and a typed input message, and returns a typed output
message. This guide takes you from an empty directory to a pushed package —
scaffold, implement, test, push.
Prerequisites
- The Axiom CLI installed and logged in (
axiom login) — see Installation. - JDK 21 and Maven (
mvn) on your PATH. The generatedpom.xmltargets Java 21 (maven.compiler.release21), andaxiom testrunsmvn test. - Docker — only needed for
axiom build(optional local image build).axiom testandaxiom pushdo not require it. - A git remote:
axiom pushrequires the current HEAD to be pushed to the remote.
Create the package and node
The shortest path — four commands:
# from any working directory
axiom init my-scope/order-tools --language java
cd order-tools
axiom create message OrderRequest --fields "order_id:string; amount:double"
axiom create message OrderConfirmation --fields "order_id:string; approved:bool"
axiom create node ProcessOrder --input OrderRequest --output OrderConfirmationWhat each step does:
axiom initcreates./order-tools/(the part after the last/) withaxiom.yaml,messages/messages.proto, a generated Mavenpom.xml(marked "DO NOT EDIT" — it wires protobuf code generation and the executable jar build), and a.gitignore.axiom create messageappends a message block tomessages/messages.proto— all messages live in that single file so they can reference each other. Comments written directly above a message or field are extracted at publish time and shown in the registry as documentation. Without--fields, the block is scaffolded with placeholder fields and syntax hints for you to edit.axiom create node(the name must be PascalCase) writesnodes/process_order.javaandnodes/process_order_test.java, generates theAxiomContextinterface atsrc/main/java/axiom/AxiomContext.java, and registers the node inaxiom.yaml. If--inputor--outputare omitted in a terminal, it prompts with a numbered list of available messages. In a terminal it also asks for the node type — press Enter to accept the default (unary), or pass--typeto skip the prompt.
Keep the snake_case filenames. axiom validate and the build pipeline
look up each node at nodes/<snake_name>.java; at build time the file is
relocated and renamed automatically to the Maven-conventional
src/main/java/nodes/ProcessOrder.java, so the working-tree name never
violates Java's class-per-file convention in the compiled layout.
Implement the handler
The validator requires this exact signature shape — a public static method
whose name is the camelCase form of the node name:
public static OrderConfirmation processOrder(AxiomContext ax, OrderRequest input)Local message types are nested under the generated gen.Messages outer
class, so each is imported by its fully qualified name and used by its
simple name. A complete handler:
// nodes/process_order.java
package nodes;
import axiom.AxiomContext;
import gen.Messages.OrderRequest;
import gen.Messages.OrderConfirmation;
import java.util.Map;
public class ProcessOrder {
/**
* Approves orders under 1000.00 and echoes the order id. This Javadoc is
* extracted at publish time and shown in the Axiom registry as the
* node's documentation — write the real description here.
*
* @param ax The AxiomContext: logging, secrets, agent memory, reflection, mutation.
* @param input The decoded OrderRequest for this invocation.
*/
public static OrderConfirmation processOrder(AxiomContext ax, OrderRequest input) {
ax.log().info("processOrder handling", Map.of("order_id", input.getOrderId()));
boolean approved = input.getAmount() < 1000.0;
return OrderConfirmation.newBuilder()
.setOrderId(input.getOrderId())
.setApproved(approved)
.build();
}
}Messages are standard protobuf-java classes: read fields with getters
(input.getOrderId()), build outputs with newBuilder() … build(). See
Type system for how messages define the
contract between nodes.
Reach the platform through AxiomContext
AxiomContext (the ax parameter) is the only way node code reaches the
platform — everything goes through the sidecar, never a direct call. The
interface lives in your package at src/main/java/axiom/AxiomContext.java:
ax.log()— structured logging:ax.log().info("msg", Map.of("k", "v")), plusdebug,warn,error, each with a single-argument overload.ax.secrets().get("NAME")— returnsOptional<String>; tenant-scoped secrets resolved by the platform. Declare the names your node needs underrequired_secretsinaxiom.yaml— see Manage secrets in a flow.ax.agent().memory()— durable agent memory:search(query, limit),write(content, importance), and per-session scope viasession("s1")with conversation history (history().last(20),history().append(role, content)). See Memory.ax.executionId(),ax.flowId(),ax.tenantId()— identifiers for the current execution.ax.reflection().flow()— read-only view of the running flow graph (nodes(),edges(),loopEdges(),position(),graphId()).ax.mutation().flow()— buffered, additive mutation of the running flow graph (addNode,addEdge).
Why the indirection: node code is sandboxed and the sidecar is the trust boundary — see Sandboxing and tenancy.
Java has no axiom generate step
For Java packages, protobuf code generation is deferred to the build tool:
the protobuf-maven-plugin declared in the generated pom.xml compiles
messages/messages.proto (plus the platform's node service proto) during
every mvn build. Running axiom generate in a Java package prints a
notice and generates nothing — there is no gen/ bindings directory to
commit.
Two naming rules follow from this:
- Local messages compile with
option java_package = "gen"andoption java_outer_classname = "Messages", so a local messageFoois the Java typegen.Messages.Foo. - Imported messages (types from another package, added with
axiom import) compile underimports.<proto_package>, so an imported messageBarfrom packageacme/billingisimports.acme.billing.Bar.axiom create nodewrites the correct import lines for whichever source each message comes from — see Import package types.
Test the node
axiom testaxiom test validates the package, then assembles the full Maven build
context (the same src/main + src/test layout axiom build compiles,
with proto codegen wired into the pom) and runs mvn test inside it. Java node tests cannot run in the bare package root — they compile
against the generated service, AxiomContext, and proto bindings that only
exist in the assembled context.
The scaffolded nodes/process_order_test.java contains a TestContext
class — a no-op AxiomContext implementation you can edit to drive a
specific scenario (its secrets return empty, memory returns empty,
reflection exposes an empty graph). Replace the generated testProcessOrder
method with real assertions, keeping the generated imports and the
TestContext class:
// nodes/process_order_test.java — replaces the generated testProcessOrder method
@Test
public void testProcessOrder() {
AxiomContext ax = new TestContext();
OrderRequest input = OrderRequest.newBuilder()
.setOrderId("ord-1")
.setAmount(250.0)
.build();
OrderConfirmation result = ProcessOrder.processOrder(ax, input);
assertEquals("ord-1", result.getOrderId());
assertTrue(result.getApproved());
}axiom validate warns — without blocking — when a node has no test, and
axiom test runs the suite with Maven. The push build does not execute
tests, so a green axiom test before pushing is the quality gate that
matters. Assert output fields meaningfully — not just null-checked.
Build and push
axiom validate # axiom.yaml schema, proto definitions, node signatures, node tests
axiom build # optional: local Docker image, identical to the publish pipeline
axiom push # deploy to the platform, tenant-privateaxiom validatechecksaxiom.yaml, compiles every proto inmessages/, and verifies each node's function signature matches the expected shape. It runs automatically insideaxiom buildand the publish pipeline.axiom buildproduces a local Docker image: the Java build compiles the package and packages it as an executable fat jar that runs on a JRE base image. All artifacts land in.axiom/for inspection.axiom pushvalidates, then pushes the package to the platform. Commit and push your changes first — the platform builds from the git remote, not your working tree. A pushed package is visible only to your own tenant, and you can push the same version repeatedly while iterating; runaxiom publish <package>@<version>(or use the Axiom UI) when you want an immutable public release.
Note: axiom dev supports Java via a rebuild-on-save-restart loop — each saved
change runs mvn package and restarts the service (compiled languages recompile
rather than hot-swap, so the first build is slower while Maven warms up; a failed
build leaves the previous server running). The HTTP bridge (default :8083) lets
you curl your nodes with JSON. axiom test and axiom build remain available.
Next: place the node in a flow (Build your first flow) and invoke it (Invoke a flow via the API).
Streaming pipeline nodes
Pass --type pipeline to scaffold a streaming node instead of a unary one:
axiom create node TransformFrames --input OrderRequest --output OrderConfirmation --type pipelineA pipeline node consumes an iterator of input frames and returns a stream of output frames:
public static Stream<OrderConfirmation> transformFrames(AxiomContext ax, Iterator<OrderRequest> inputs)When a pipeline node is the entry node of a flow, the iterator yields exactly one item. See Execution model for how pipeline mode changes flow execution.