Nodes, packages, and flows
Axiom's core object model — how messages, nodes, packages, flows, executions, and the entry and terminal nodes relate to each other.
View as MarkdownEverything you build on Axiom is made of six objects: messages define the data, nodes compute over it, packages publish nodes, flows compose nodes into a graph, and invoking a flow creates an execution that runs from the flow's entry node to its terminal node. This page defines each object and how they fit together; the glossary is the one-line authority for every term.
The object model at a glance
| Object | What it is | Where it lives |
|---|---|---|
| Message | A Protocol Buffers message — the only data type that crosses a node boundary | messages/messages.proto in a package |
| Node | A typed handler function — the unit of compute | A source file in nodes/, declared in axiom.yaml |
| Package | A versioned bundle of nodes and message types — the unit of publishing | A directory with an axiom.yaml manifest |
| Flow | A directed graph of nodes with type-checked edges | Composed in the canvas; compiled to an immutable artifact |
| Execution | One run of a flow, from invocation to completion | Created each time a flow is invoked |
The whole lifecycle is driven by the CLI plus the canvas:
# From an empty directory: package → messages → node → deployed
axiom init acme/orders --language python
cd orders
axiom create message OrderRequest --fields "order_id:string; total:double"
axiom create message OrderConfirmation --fields "order_id:string; accepted:bool"
axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation
axiom pushAfter axiom push, the package appears in the editor's Marketplace panel
(visible only to your tenant); importing it there adds its nodes to the open
flow's Library panel, ready to be dragged onto the canvas and composed into a
flow. See
your first node and
your first flow for the full walkthrough.
Node: the unit of compute
A node is a plain function with a typed input message and a typed output
message. There is no SDK in the business logic — platform capabilities
(logging, secrets, memory) arrive through a single injected context argument,
AxiomContext:
# nodes/process_order.py
from gen.messages_pb2 import OrderRequest, OrderConfirmation
from gen.axiom_context import AxiomContext
def process_order(ax: AxiomContext, input: OrderRequest) -> OrderConfirmation:
"""Accepts an order request and returns a confirmation for it."""
return OrderConfirmation(order_id=input.order_id, accepted=True)The same shape holds in every supported language — in a Go package
(axiom init acme/orders --language go), the same node looks like this:
// nodes/process_order.go
package nodes
import (
"context"
"acme/orders/axiom"
gen "acme/orders/gen"
)
// ProcessOrder accepts an order request and returns a confirmation for it.
func ProcessOrder(ctx context.Context, ax axiom.Context, input *gen.OrderRequest) (*gen.OrderConfirmation, error) {
return &gen.OrderConfirmation{OrderId: input.OrderId, Accepted: true}, nil
}axiom create node <Name> scaffolds the implementation file and a matching
test file in nodes/, and adds the node to axiom.yaml. The doc comment
attached to the function (the Python docstring, or the Go comment directly
above func) is extracted at publish time and shown in the registry as the
node's description.
By default a node is unary: one input message in, one output message out per
invocation. Declaring type: pipeline in axiom.yaml makes it a streaming
generator that emits a sequence of output frames instead — see the
execution model.
Node code is sandboxed: it runs in a container, talks to the platform only through the sidecar, and cannot reach other tenants' data — see sandboxing and tenancy. Per-language guides: Python, Go, TypeScript, Rust, Java, C#.
Message: the data contract between nodes
Every node input, node output, and edge payload is a Protocol Buffers message.
A package's messages all live in one file, messages/messages.proto, so they
can reference each other without import statements. Scaffold one with the CLI:
# Run inside the package directory (where axiom.yaml lives)
axiom create message OrderRequest --fields "order_id:string; total:double"--fields accepts canonical proto3 (string order_id = 1), proto3 without
field numbers, or name:type shorthand; field numbers are auto-assigned when
omitted. After adding the message, axiom generate runs automatically to
produce language bindings in gen/. Comments written directly above a message
or field (no blank line between) are extracted at publish time and shown in
the registry as documentation.
Messages can be shared across packages: a package declares imports in
axiom.yaml and pulls the proto definitions down with axiom import. The
type system page covers contracts and compatibility;
import types from another package covers
the workflow.
Package: the unit of publishing
A package is a versioned, single-language bundle of nodes and message types,
described by an axiom.yaml manifest. axiom init <name> creates the package
directory (named after the part of the package name following the last /)
with axiom.yaml, messages/, nodes/, and gen/. Supported languages:
go, python, rust, java, typescript, csharp.
# axiom.yaml
name: acme/orders
version: 0.1.0
language: python
description: Order intake and confirmation nodes
nodes:
- name: ProcessOrder
input: OrderRequest
output: OrderConfirmationEach node entry names its input and output messages; optional fields include
type: pipeline (streaming node) and required_secrets (the secret names the
node reads at runtime, displayed in the marketplace). The full schema is in
the axiom.yaml reference.
A package with no nodes (or with type: proto-only) is a proto-only
package: it carries only message types for other packages to import, and
publishing it skips the container build and deployment entirely.
Deployment is two-stage. axiom push validates the package and pushes it
tenant-private: only your tenant sees it, and pushing the same version
again overwrites the previous push, so you can iterate. Publishing — with
axiom publish <package>@<version> (or from the Axiom UI) when you are ready —
turns it into an immutable versioned release visible to others in the
marketplace.
Flow: a typed graph of nodes
A flow is a directed graph of nodes, composed in the canvas: create one with the New Flow dialog, import packages from the Marketplace panel into the flow's Library, drag nodes from the Library panel onto the canvas, and connect them with edges. An edge from node A to node B means "A's output message becomes B's input message" — the canvas checks type compatibility on every edge as you build, and an edge whose message types differ needs a field mapping before the flow can run (see the type system). A flow can also place another flow as a subflow node.
Compiling a flow produces a compiled artifact: an immutable, optimized representation that workers execute. Invoking a flow always means executing an artifact, never an editable draft; editing the flow and recompiling produces a new artifact. Compilation enforces the flow's shape: the graph must have exactly one node with no incoming edges and exactly one node with no outgoing edges, or the compile is rejected.
To run a flow, use the canvas Run button, or compile and invoke it over
HTTP — the Use via API dialog generates a ready-to-paste curl command.
See invoke a flow via the API.
Entry node and terminal node
The entry node is where an execution starts: the flow's single node with
no incoming edges. Its input message defines the flow's input schema — the
JSON input object a caller sends when invoking the flow is converted to that
message. (Compile errors call this the start node, as in "graph must have
exactly one start node".)
The terminal node is where an execution ends: the flow's single node with no outgoing edges. Its output message defines the flow's result schema, and an execution completes when the terminal node finishes.
Both are determined by graph shape — there is no flag to set. Compilation fails with an error if zero or multiple candidates exist for either role, so a compiled flow always has exactly one of each.
Execution: one run of a flow
An execution is a single run of a flow from invocation to completion. The platform assigns an execution ID when the request is accepted, and that ID is threaded through every hop — traces, debug events, and results all reference it, and the editor's Executions list shows the history.
The simplest invocation is one HTTP call that waits for the result:
# Invoke a compiled flow and wait for the terminal node's output
curl -X POST 'https://<your-axiom-host>/invocations/v1/flows/invoke' \
-H "Authorization: Bearer $AXIOM_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"graph_id": "<artifact-id>", "input": {"order_id": "A-100", "total": 42.5}, "wait": true}'The input object must match the entry node's input message; the response
carries the terminal node's output. Flows containing pipeline nodes run in
pipeline mode instead, streaming results progressively via
POST /invocations/v1/flows/invoke/stream — the
execution model explains unary versus pipeline
execution, durability, and replay. To watch an execution node by node, see
debug a flow.