Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

The type system

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.

View as Markdown

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.yamlinput and output — each referring to a message defined in the package's messages/messages.proto or imported from another package:

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

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

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:

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

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:

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

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: srcdst. 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 for the end-to-end canvas workflow and 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 for the request format, and Use the interactive API docs to explore a package's schemas in the browser.