---
title: "Create a node in Java"
description: "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."
category: guide
surfaces: [cli, sdk]
languages: [java]
related: [getting-started/first-node, getting-started/invoke-via-api, concepts/nodes-packages-flows, concepts/type-system, guides/import-package-types, guides/manage-secrets, reference/axiom-yaml, reference/cli/axiom-init, reference/cli/axiom-create-node, reference/cli/axiom-test, reference/cli/axiom-push]
last_reviewed: 2026-06-06
---

# Create a node in Java

A 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](../getting-started/installation.md).
- **JDK 21 and Maven (`mvn`)** on your PATH. The generated `pom.xml` targets
  Java 21 (`maven.compiler.release` 21), and `axiom test` runs `mvn test`.
- **Docker** — only needed for `axiom build` (optional local image build).
  `axiom test` and `axiom push` do not require it.
- A **git remote**: `axiom push` requires the current HEAD to be pushed to
  the remote.

## Create the package and node

The shortest path — four commands:

```bash
# 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 OrderConfirmation
```

What each step does:

- `axiom init` creates `./order-tools/` (the part after the last `/`) with
  `axiom.yaml`, `messages/messages.proto`, a generated Maven `pom.xml`
  (marked "DO NOT EDIT" — it wires protobuf code generation and the
  executable jar build), and a `.gitignore`.
- `axiom create message` appends a message block to
  `messages/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) writes
  `nodes/process_order.java` and `nodes/process_order_test.java`, generates
  the `AxiomContext` interface at `src/main/java/axiom/AxiomContext.java`,
  and registers the node in `axiom.yaml`. If `--input` or `--output` are
  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 `--type` to 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:

```java
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:

```java
// 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](../concepts/type-system.md) 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"))`,
  plus `debug`, `warn`, `error`, each with a single-argument overload.
- `ax.secrets().get("NAME")` — returns `Optional<String>`; tenant-scoped
  secrets resolved by the platform. Declare the names your node needs under
  `required_secrets` in `axiom.yaml` — see
  [Manage secrets in a flow](manage-secrets.md).
- `ax.agent().memory()` — durable agent memory:
  `search(query, limit)`, `write(content, importance)`, and per-session
  scope via `session("s1")` with conversation history
  (`history().last(20)`, `history().append(role, content)`). See
  [Memory](../concepts/memory.md).
- `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](../concepts/sandboxing-and-tenancy.md).

## 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"` and
  `option java_outer_classname = "Messages"`, so a local message `Foo` is
  the Java type `gen.Messages.Foo`.
- **Imported messages** (types from another package, added with
  `axiom import`) compile under `imports.<proto_package>`, so an imported
  message `Bar` from package `acme/billing` is
  `imports.acme.billing.Bar`. `axiom create node` writes the correct
  import lines for whichever source each message comes from — see
  [Import package types](import-package-types.md).

## Test the node

```bash
axiom test
```

`axiom 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:

```java
// 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

```bash
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-private
```

- `axiom validate` checks `axiom.yaml`, compiles every proto in
  `messages/`, and verifies each node's function signature matches the
  expected shape. It runs automatically inside `axiom build` and the
  publish pipeline.
- `axiom build` produces 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 push` validates, 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;
  run `axiom 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](../getting-started/first-flow.md)) and invoke it
([Invoke a flow via the API](../getting-started/invoke-via-api.md)).

## Streaming pipeline nodes

Pass `--type pipeline` to scaffold a streaming node instead of a unary one:

```bash
axiom create node TransformFrames --input OrderRequest --output OrderConfirmation --type pipeline
```

A pipeline node consumes an iterator of input frames and returns a stream of
output frames:

```java
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](../concepts/execution-model.md) for
how pipeline mode changes flow execution.
