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 MarkdownAxiom'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:
# axiom.yaml
name: my-org/orders
version: 0.1.0
language: python
nodes:
- name: ProcessOrder
input: OrderRequest
output: OrderConfirmationaxiom 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 messagerejects anything else. - Scalar field types:
string,int32,int64,uint32,uint64,float,double,bool,bytes. Userepeatedfor lists andoptionalfor 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.1The command requires a prior axiom login. It then:
-
Resolves the latest published version when
@versionis omitted. -
Downloads the package's
.protofiles and extracts them toimports/<package>/<version>/in your project (a/in a scoped package name is flattened to-in the directory name). -
Adds an entry under
imports:inaxiom.yamlrecording 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 -
Runs
axiom generateso 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, andLENGTHfor strings;MULTIPLYandADDfor numbers;DEFAULTto substitute a fallback when the value is empty;CASTto 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 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.