---
title: "Nodes, packages, and flows"
description: "Axiom's core object model — how messages, nodes, packages, flows, executions, and the entry and terminal nodes relate to each other."
category: concept
surfaces: [cli, canvas, sdk, http-api]
related: [getting-started/first-node, getting-started/first-flow, getting-started/invoke-via-api, concepts/type-system, concepts/execution-model, reference/glossary, reference/axiom-yaml]
last_reviewed: 2026-06-06
---

# Nodes, packages, and flows

Everything 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](../reference/glossary.md) is
the one-line authority for every term.

## The object model at a glance

| Object | What it is | Where it lives |
|---|---|---|
| [Message](../reference/glossary.md#message) | A Protocol Buffers message — the only data type that crosses a node boundary | `messages/messages.proto` in a package |
| [Node](../reference/glossary.md#node) | A typed handler function — the unit of compute | A source file in `nodes/`, declared in `axiom.yaml` |
| [Package](../reference/glossary.md#package) | A versioned bundle of nodes and message types — the unit of publishing | A directory with an `axiom.yaml` manifest |
| [Flow](../reference/glossary.md#flow) | A directed graph of nodes with type-checked edges | Composed in the canvas; compiled to an immutable artifact |
| [Execution](../reference/glossary.md#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:

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

After `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](../getting-started/first-node.md) and
[your first flow](../getting-started/first-flow.md) 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`:

```python
# 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:

```go
// 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](./execution-model.md).

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](./sandboxing-and-tenancy.md). Per-language guides:
[Python](../guides/create-a-node-python.md), [Go](../guides/create-a-node-go.md),
[TypeScript](../guides/create-a-node-typescript.md),
[Rust](../guides/create-a-node-rust.md), [Java](../guides/create-a-node-java.md),
[C#](../guides/create-a-node-csharp.md).

## 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:

```bash
# 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](./type-system.md) page covers contracts and compatibility;
[import types from another package](../guides/import-package-types.md) 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`.

```yaml
# axiom.yaml
name: acme/orders
version: 0.1.0
language: python
description: Order intake and confirmation nodes
nodes:
  - name: ProcessOrder
    input: OrderRequest
    output: OrderConfirmation
```

Each 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](../reference/axiom-yaml.md).

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](./type-system.md)). 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](../getting-started/invoke-via-api.md).

## 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:

```bash
# 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](./execution-model.md) explains unary versus pipeline
execution, durability, and replay. To watch an execution node by node, see
[debug a flow](../guides/debug-a-flow.md).
