Create a node in Rust
Scaffold a Rust node package, implement the handler against the AxiomContext trait, run cargo-based tests with axiom test, and push it to the platform.
View as MarkdownThis guide takes you from an empty directory to a pushed Rust node: scaffold a package, define messages, implement the handler, test it, and push. If you have never built a node in any language, start with your first node; this page covers the Rust-specific workflow.
Prerequisites
- The Axiom CLI installed and logged in (
axiom login) — see installation. - A Rust toolchain with
cargoon your PATH. If you don't have one:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh. - For
axiom push: the package directory must be a git repository whose current HEAD is pushed to its remote.
You do not need protoc or any protoc plugin for Rust — proto compilation
happens inside the build via tonic-build (see
How Rust differs from other languages).
Create a Rust node in six commands
# from any directory — scaffolds ./order-tools/
# the scope before the "/" must be your Axiom handle — push rejects any other scope
axiom init your-handle/order-tools --language rust
cd order-tools
axiom create message OrderRequest --fields "order_id:string; total:double"
axiom create message OrderConfirmation --fields "order_id:string; confirmed:bool"
axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation
axiom testThen implement the handler in nodes/process_order.rs (next section), make the
generated test assert real output fields, and push:
# from the package root
axiom pushWhat each step did:
axiom init your-handle/order-tools --language rustcreated anorder-tools/subdirectory (the part of the package name after the last/) withaxiom.yaml, the standard layout (messages/,nodes/, and an emptygen/, which Rust leaves unused because codegen happens at build time), a.gitignore(ignoring.axiom/,gen/, andtarget/), and aCargo.toml. Package names must be scoped asyour-handle/package-name, where the scope matches your Axiom account handle. The crate is always namedservice— the Axiom package name lives inaxiom.yaml. The manifest pinstonic,prost,tokio,tokio-stream, andserde_jsonas dependencies andtonic-buildas a build dependency.axiom create messageappended a message tomessages/messages.proto— the single file where all of the package's messages live. The--fieldsflag accepts colon shorthand (name:string), proto3 syntax with or without field numbers, and the scalar typesstring,int32,int64,uint32,uint64,float,double,bool,bytes.axiom create node ProcessOrdercreatednodes/process_order.rsandnodes/process_order_test.rs(the node name must be PascalCase; the Rust function name is its snake_case form), and registered the node inaxiom.yaml. Omit--input/--outputto pick messages interactively.axiom testvalidated the package and rancargo test.axiom pushvalidated the package and pushed it tenant-private to the platform, ready to place in a flow.
Implement the node handler
A Rust node is a plain public function in nodes/<snake_name>.rs. It receives
the AxiomContext trait object and a typed input message, and returns a
Result with the typed output message:
// nodes/process_order.rs
use crate::axiom_context::AxiomContext;
use crate::gen::messages::{OrderRequest, OrderConfirmation};
use std::collections::HashMap;
/// Confirms an incoming order. This doc comment documents the handler for
/// anyone reading the node's source, which the registry captures at push time.
pub fn process_order(
ax: &dyn AxiomContext,
input: OrderRequest,
) -> Result<OrderConfirmation, Box<dyn std::error::Error>> {
ax.log().info(
"processing order",
&HashMap::from([("order_id", input.order_id.clone())]),
);
Ok(OrderConfirmation {
order_id: input.order_id,
confirmed: true,
})
}Key points:
- Messages are prost structs. Each protobuf message in
messages/messages.protobecomes a Rust struct undercrate::gen::messageswith snake_case fields and aDefaultimpl. Messages from imported packages are re-exported into the samegen::messagesmodule — see import package types and the type system. - The signature is enforced.
axiom validate(run automatically byaxiom test,axiom build, and the publish pipeline) checks that each node declared inaxiom.yamlhas a function of exactly this shape:pub fn <name>(ax: &dyn AxiomContext, input: <Input>) -> Result<<Output>, ...>. - The registry description comes from
axiom.yaml. Thedescriptionfield on the node's entry inaxiom.yaml(set with--descriptiononaxiom create node, or edited any time before pushing) is what the Axiom registry displays as the node's description. The node's source file — including its doc comment — is captured at push time and shown in the registry's source view, so edit the scaffolded placeholder too.
Use platform capabilities through AxiomContext
AxiomContext is the only way node code reaches the platform — there is no
direct network access from a node (see
sandboxing and tenancy). The trait
exposes:
ax.log()— structured logging:debug,info,warn,error, each taking a message and a&HashMap<&str, String>of attributes.ax.secrets()— read-only tenant secrets:ax.secrets().get(name)returns(String, bool)— the value and whether the secret exists.ax.agent().memory()— agent memory: session-scoped history and semantic search/write (see memory).ax.execution_id(),ax.flow_id(),ax.tenant_id()— identifiers for the current invocation.ax.reflection()andax.mutation()— read and append-only-extend the running flow's graph (advanced agent features).
Reading a secret:
// nodes/process_order.rs — inside the handler body
let (api_key, ok) = ax.secrets().get("STRIPE_API_KEY");
if !ok {
return Err("secret STRIPE_API_KEY is not registered".into());
}Declare every secret the node reads in axiom.yaml so it is recorded with
the node and shown in the marketplace — users then know which secrets to
register before running a flow that contains the node:
# axiom.yaml
nodes:
- name: ProcessOrder
input: OrderRequest
output: OrderConfirmation
required_secrets:
- STRIPE_API_KEYSee manage secrets in a flow for registering the secret value itself.
Test the node with axiom test
axiom create node scaffolds nodes/<snake_name>_test.rs with a mock
AxiomContext (test_context()) you edit to drive scenarios. Replace the
generated TODO with assertions on real output fields:
// nodes/process_order_test.rs — inside the generated #[cfg(test)] module
#[test]
fn test_process_order() {
let ax = test_context();
let input = OrderRequest { order_id: "ord-1".into(), total: 12.5 };
let out = process_order(&ax, input).unwrap();
assert_eq!(out.order_id, "ord-1");
assert!(out.confirmed);
}Run the suite from the package root:
# validates the package, then runs cargo test in the assembled build context
axiom test
# pass filters through to the cargo test binary after --
axiom test -- test_process_orderDo not run cargo test directly in the package root — it will fail to
compile. Rust node tests are #[cfg(test)] modules of the generated service
crate and reference crate:: paths that only exist once the build context is
assembled. axiom test assembles that build context and runs cargo test
inside it.
axiom validate (run by axiom test, axiom build, and axiom push) warns
when a node has no test file or the test file defines no test. For Rust the
push build compiles your crate but does not execute tests, so a green
axiom test run before pushing is the quality gate that matters. Make the
assertions meaningful: a test that only Ok-checks the result passes against
any implementation.
Write a streaming pipeline node
Pass --type pipeline to scaffold a streaming node instead of a unary one:
axiom create node StreamOrders --input OrderRequest --output OrderConfirmation --type pipelineA pipeline node receives an iterator of input frames and returns an iterator
of results (for the start node of a pipeline flow, inputs yields exactly one
item):
// nodes/stream_orders.rs
use crate::axiom_context::AxiomContext;
use crate::gen::messages::{OrderRequest, OrderConfirmation};
/// Emits one confirmation per incoming order frame.
pub fn stream_orders<I>(
ax: &dyn AxiomContext,
inputs: I,
) -> impl Iterator<Item = Result<OrderConfirmation, Box<dyn std::error::Error>>>
where
I: Iterator<Item = OrderRequest>,
{
let _ = ax;
inputs.map(|input| {
Ok(OrderConfirmation {
order_id: input.order_id,
confirmed: true,
})
})
}See the execution model for how pipeline mode differs from unary execution.
How Rust differs from other languages
Three Rust-specific behaviors to know, compared with the Python, Go, and TypeScript workflows:
- No
axiom generatestep. Rust defers all proto codegen to the build tool: a generatedbuild.rscompilesmessages/*.proto(plus any imported packages and the platform's node service proto) withtonic-buildatcargo build. Runningaxiom generatein a Rust package prints a note and does nothing. Noprotocor protoc plugin is required. axiom devsupports Rust via a rebuild-on-save-restart loop: each saved change runscargo buildand restarts the service (compiled languages recompile rather than hot-swap, so the first build is slower while the crate graph warms up; a failed build leaves the previous server running). The HTTP bridge (default:8083) converts JSON to and from Protobuf so you cancurlyour nodes — the same fast loop Go/Python/TypeScript get. You can still useaxiom buildto produce a local Docker image identical to the publish pipeline (all artifacts are written under.axiom/and are inspectable).- The crate compiles only in the assembled build context. The package root
has no
service.rs,build.rs, or generated bindings — those are injected whenaxiom testoraxiom buildassembles the context under.axiom/. Useaxiom testrather than barecargocommands.
Push and use the node
# from the package root, with HEAD pushed to your git remote
axiom pushaxiom push validates the package locally, then pushes it to the platform
tenant-private: it is visible only to your own tenant and does not appear
in the public marketplace. You can push the same version repeatedly — each
push overwrites the previous one — so you can iterate on deployed code. When
you are satisfied, run axiom publish <package>@<version> (or use the Axiom UI)
to make it an immutable public release.
Next steps:
- Place the node in a flow on the canvas — build your first flow.
- Call the flow over HTTP — invoke via API.
- Full command details — axiom init, axiom create node, axiom test, axiom build, axiom push.