---
title: "The type system"
description: "How Protocol Buffers define every contract in Axiom: messages as node inputs and outputs, importing types from other packages with axiom import, and how the canvas keeps edges type-safe."
category: concept
surfaces: [cli, canvas, http-api]
related: [concepts/nodes-packages-flows, guides/import-package-types, getting-started/first-flow, getting-started/invoke-via-api, reference/axiom-yaml, reference/glossary]
last_reviewed: 2026-06-06
---

# The type system

Axiom's type system is Protocol Buffers. A message is the only data type
that crosses a node boundary: every node declares a typed input message and
a typed output message, every edge in a flow carries a message, and the
platform checks that contract at every stage — when you validate the package
locally, when you connect nodes in the canvas, and when a caller invokes the
flow over HTTP.

## Messages are the node contract

A node's contract is two message names in `axiom.yaml` — `input` and
`output` — each referring to a message defined in the package's
`messages/messages.proto` or imported from another package:

```yaml
# axiom.yaml
name: my-org/orders
version: 0.1.0
language: python
nodes:
  - name: ProcessOrder
    input: OrderRequest
    output: OrderConfirmation
```

`axiom generate` compiles every `.proto` file in `messages/` into language
bindings under `gen/`, so the handler is written against generated types
rather than untyped dictionaries or JSON. In Python the generated scaffold
looks like this:

```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:
    """Validates an order request and returns a confirmation."""
    return OrderConfirmation()
```

The contract is enforced locally by `axiom validate`, which checks three
layers: the `axiom.yaml` schema (including that every node's `input` and
`output` reference a defined message), that all `.proto` files in
`messages/` compile, and that each node's implementation has the expected
function signature. `axiom validate` runs automatically as part of
`axiom build`, and `axiom generate` runs automatically as part of
`axiom dev`, `axiom test`, and `axiom build`.

## Defining messages

`axiom create message <Name>` appends a message block to
`messages/messages.proto` and then runs `axiom generate` to rebuild the
language bindings (pass `--no-generate` to skip that step when creating
several messages in a row):

```bash
axiom create message OrderRequest --fields "customer_id:string; item_count:int32"
```

`--fields` is a semicolon-separated list that accepts canonical proto3
(`string name = 1`), proto3 without field numbers (`string name`), or colon
shorthand (`name:string`); field numbers are auto-assigned when omitted.
Without `--fields`, the command scaffolds placeholder fields plus a HINTS
comment block explaining the syntax.

All of a package's messages live in that single file, so they can reference
each other without any import statements:

```proto
// messages/messages.proto
syntax = "proto3";

package my_org.orders;

// A request to place a new order.
message OrderRequest {
  string customer_id = 1;      // Who is placing the order
  repeated LineItem items = 2; // The items being ordered
}

// One item within an order.
message LineItem {
  string sku = 1;
  int32 quantity = 2;
}
```

Rules that matter when writing messages:

- Message names are PascalCase — letters and digits only, starting with an
  uppercase letter. `axiom create message` rejects anything else.
- Scalar field types: `string`, `int32`, `int64`, `uint32`, `uint64`,
  `float`, `double`, `bool`, `bytes`. Use `repeated` for lists and
  `optional` for optional fields.
- Field numbers must be unique within a message and never reused once the
  package is published. Numbers 1–15 encode in one byte on the wire — prefer
  them for your most common fields.
- A comment written directly above a message or field (no blank line in
  between) is extracted at publish time and shown in the Axiom registry as
  that message's or field's documentation. A detached comment (blank line
  before the declaration) is ignored.

## Importing types from another package

`axiom import <package>[@version]` makes a published package's message types
available as node inputs and outputs in your package:

```bash
axiom import my-org/payments@2.0.1
```

The command requires a prior `axiom login`. It then:

1. Resolves the latest published version when `@version` is omitted.
2. Downloads the package's `.proto` files and extracts them to
   `imports/<package>/<version>/` in your project (a `/` in a scoped package
   name is flattened to `-` in the directory name).
3. Adds an entry under `imports:` in `axiom.yaml` recording the package,
   version, and imported message names:

   ```yaml
   # axiom.yaml — entry added by axiom import
   imports:
     - package: my-org/payments
       version: 2.0.1
       messages:
         - PaymentRequest
         - PaymentResult
   ```

4. Runs `axiom generate` so the bindings and your IDE immediately see the
   new types.

If an imported message has the same name as one in your local `messages/`
directory, the import fails with a collision error — rename one of them
first.

To find types worth importing, search the marketplace with
`axiom search --type messages <query>`, and list a specific package's
messages with `axiom info <package>`. A package that defines only messages
and no nodes is a *proto-only* package: publishing it skips the build and
deployment entirely, which makes it the natural way to share a type contract
between teams.

For the full workflow, see
[Import message types from another package](/docs/guides/import-package-types).

## Type-safe edges in the canvas

An edge from node A to node B means A's output message becomes B's input
message. When the two message types are identical, fields pass through
automatically; when they differ, the canvas requires a field mapping before
the flow can run.

Selecting an edge opens the **Edge Plan** panel in the inspector, headed
with the edge's `source message → destination message` pair:

- **Same message type** — the panel shows "Types match — fields pass through
  automatically." You can still click "Configure field mapping anyway" to
  reshape fields explicitly.
- **Different message types** — the panel shows a field mapping editor.
  Each destination field is bound to a source field, optionally through a
  pipeline of one or more transform steps: `UPPER`, `LOWER`, `TRIM`,
  `PREFIX`, `SUFFIX`, `REPLACE`, `REGEX_REPLACE`, `SLICE`, `SPLIT`, and
  `LENGTH` for strings; `MULTIPLY` and `ADD` for numbers; `DEFAULT` to
  substitute a fallback when the value is empty; `CAST` to convert between
  kinds.

Field-level compatibility follows these rules: identical types always match;
`bytes → string` is safe; any numeric type can map to any other numeric type
(narrowing may lose precision but is allowed). Any other pairing is flagged:
"Type mismatch: `src` → `dst`. Add a transform to convert the value."

An edge with an unresolved type mismatch — or with a required (non-optional,
non-repeated) destination field left unmapped — renders amber on the canvas,
and the flow's Run control is disabled with the tooltip "Type warnings must
be resolved before running." Click the amber edge and fix the mapping to
unblock the run.

One structural rule sits alongside type checking: an edge leaving a pipeline
node may only connect to another pipeline node. See
[Compose your first flow](/docs/getting-started/first-flow) for the
end-to-end canvas workflow and
[Execution model](/docs/concepts/execution-model) for what pipeline nodes
are.

## Types at the API boundary

The entry node's input message defines the flow's input schema, and the
terminal node's output message defines its result schema. Callers never
hand-encode protobuf: you send a JSON body, and the platform transcodes it
into the entry node's input message — field names and types must match the
message definition — then transcodes the output message back to JSON in the
response.

Every published package also exposes a generated OpenAPI spec describing its
nodes' message schemas. `axiom info <package>` prints both URLs: the
machine-readable spec at `/packages/<name>@<version>/openapi.json` and the
interactive documentation at `/packages/<name>@<version>/docs`.

See [Invoke a flow via the API](/docs/getting-started/invoke-via-api) for
the request format, and
[Use the interactive API docs](/docs/guides/use-interactive-api-docs) to
explore a package's schemas in the browser.
