## Getting started > Install the Axiom CLI {#getting-started/installation} > Install the axiom CLI with Homebrew or the curl install script, authenticate with axiom login, and confirm your identity and tenant with axiom whoami. # Install the Axiom CLI The `axiom` CLI is how you create packages, develop nodes locally, and push them to the Axiom platform. This page installs the CLI, logs you in, and verifies your identity — about five minutes end to end. ## Install Pick the method for your platform. Both drop a prebuilt `axiom` binary on your `PATH`; you do **not** need a Go toolchain to use the CLI. **Homebrew (macOS / Linux):** ```bash brew install axiomide/tap/axiom ``` **Install script (macOS / Linux):** ```bash curl -fsSL https://raw.githubusercontent.com/AxiomIDE/axiom-releases/main/install.sh | sh ``` The script detects your OS and architecture (Linux/macOS, amd64/arm64), downloads the matching release binary, and installs it (to `/usr/local/bin`, or `~/.local/bin` if that is not writable). Pin a version with `AXIOM_VERSION=v1.2.3`, or change the target with `AXIOM_INSTALL_DIR`. **Windows:** download the `.zip` for `windows/amd64` from the [Releases page](https://github.com/AxiomIDE/axiom-releases/releases) and put `axiom.exe` on your `PATH`. The pure-CLI commands (`init`, `validate`, `create`, `push`, `client`, `login`) run natively on Windows; the Docker-backed commands (`axiom build`) and the local dev server (`axiom dev`, which uses a Unix socket) are supported **under [WSL2](https://learn.microsoft.com/windows/wsl/)**. Released binaries ship from the public [AxiomIDE/axiom-releases](https://github.com/AxiomIDE/axiom-releases) repo — prebuilt binaries and the install script only; the platform source is not published there. Verify the binary is on your `PATH`: ```bash axiom version ``` This prints the CLI version, for example: ```text axiom version 0.2.0 ``` Run `axiom --help` at any time to list every command, or `axiom --help` for one command's flags. The full command reference is at [reference/cli](../reference/cli/axiom.md). ## Set up language toolchains Authoring nodes shells out to a few per-language tools — `protoc` and language plugins for proto codegen, plus each language's test runner. To see what you're missing without installing anything, run: ```bash axiom doctor ``` Bare `axiom doctor` only **checks**: it reports which toolchain pieces your packages need and prints copy-paste install hints for the ones that are absent — it changes nothing. Run inside a package, `axiom doctor --fix` performs the **project-local** installs it can do safely — `go mod download`, `go install` of the proto plugins, `npm install`, or `pip install` into an *active* virtualenv — and only **advises** (never installs) system-level tools like `protoc`. It never uses `sudo` or touches system packages. (`axiom doctor` outside a package surveys every language and exits 0 — purely informational. Inside a package it checks just that package's language and exits non-zero when a required tool is missing, so it can gate CI.) When you scaffold a package, `axiom init --install` runs the language's dependency install (`pip install -r requirements.txt`, `npm install`, `go mod download`, …) right after writing the manifests, so the generated code compiles and tests run without a separate setup step. You can also install the per-language tools yourself when you get to a guide; each [Create a node](../guides/create-a-node-python.md) page lists exactly what it needs. ## Log in with axiom login `axiom login` authenticates you with the Axiom platform using the OAuth Device Flow: ```bash axiom login ``` The command prints a verification URL and a one-time user code, then opens the URL in your default browser. Complete sign-in there via GitHub or Google and enter the code when prompted. Meanwhile the CLI polls the platform until you approve; on success it prints your identity: ```text ✓ Logged in as (tenant: ) ``` The resulting API key is stored in `~/.axiom/credentials` (a JSON file, written with owner-only `0600` permissions). Every authenticated CLI command reads the key from there — you log in once per machine, not per command. Two things to know: - **The device code expires** (typically after 15 minutes). If you see `device code expired`, run `axiom login` again. - **In CI environments**, set the `AXIOM_API_KEY` environment variable before running `axiom login` — the CLI saves that key directly and skips the browser flow entirely. You can create API keys in the account console; see [Create and manage API keys](../guides/api-keys.md). ## Verify with axiom whoami `axiom whoami` shows the user and tenant associated with your stored API key: ```bash axiom whoami ``` ```text Email: you@example.com Tenant: ``` The tenant is the isolation boundary for everything you push: your packages, flows, and secrets are scoped to it (see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)). If you have not logged in yet, `axiom whoami` prints `Not logged in. Run "axiom login" to authenticate.` and exits with a non-zero status — run [axiom login](#log-in-with-axiom-login) first. ## Advanced: pointing the CLI at a different deployment A released `axiom` binary already knows where to find the Axiom platform — the deployment endpoint is baked in at build time. There is nothing to configure: install, then run [`axiom login`](#log-in-with-axiom-login). Two environment variables exist as an **undocumented-by-default escape hatch** for self-hosters and platform developers who need to point the CLI somewhere other than the baked default: | Variable | Purpose | |---|---| | `AXIOM_API_URL` | Platform API base — login, push, search (include the `/api` path) | | `AXIOM_INGRESS_URL` | Flow invocation base — include the `/invocations` path when pointing at a gateway | ```bash # only needed for self-hosted deployments or local development export AXIOM_API_URL="https://api.axiomide.com/api" export AXIOM_INGRESS_URL="https://api.axiomide.com/invocations" ``` The CLI uses each value exactly as given. When you point `AXIOM_INGRESS_URL` at an Axiom gateway, keep the `/invocations` prefix as shown above; only when you point it straight at an ingress service (for example a local port-forward) do you drop the prefix and use the bare origin. When you do override the endpoint, the stored credential is just the API key — so switching `AXIOM_API_URL` to a different deployment requires running `axiom login` again against that deployment (the override takes effect on the next login, not retroactively). If you're building the CLI from source (e.g. contributing to Axiom itself), the source build has no baked endpoint and defaults to `http://localhost:8082` / `http://localhost:8080` for local dev clusters, so these variables matter more often in that workflow. ## Next steps You have a working, authenticated CLI. Continue the tutorial spine: - [Create your first node](./first-node.md) — `axiom init`, `axiom create node`, `axiom dev`, `axiom test`. - [Build your first flow](./first-flow.md) — push your package, compose a flow in the canvas, and `axiom publish` it to the marketplace. - [Invoke a flow via API](./invoke-via-api.md) — call your flow with curl or a generated client. For background on what you'll be building, read [Nodes, packages, and flows](../concepts/nodes-packages-flows.md). --- ## Getting started > Write your first node {#getting-started/first-node} > Scaffold a package with axiom init, create a Python node, and iterate locally with axiom validate, axiom test, and the axiom dev hot-reload server. # Write your first node A node is the single unit of compute in Axiom: a plain function with a typed input message and a typed output message. This tutorial scaffolds a Python package, writes a `Greet` node, and exercises the full local loop — `axiom validate`, `axiom test`, and the `axiom dev` hot-reload server — without deploying anything. ## Prerequisites - The Axiom CLI installed and on your PATH — see [Install the Axiom CLI](/docs/getting-started/installation). - Python 3.10 or newer. - The Python tooling the local loop shells out to: ```bash # grpcio-tools compiles .proto files (axiom generate); pytest runs axiom test pip install grpcio-tools pytest ``` If `grpcio-tools` is not installed, `axiom generate` falls back to a standalone `protoc` binary on your PATH; either one works. To let the CLI help instead, run `axiom doctor` (checks the toolchain and prints install hints; `axiom doctor --fix`, run inside a package, installs the project-local pieces) or scaffold with `axiom init --install` (see below). ## The fast path The whole scaffold is five commands. Each step is explained in the sections below. ```bash axiom init my-org/greeter --language python cd greeter axiom create message GreetRequest --fields "name:string" axiom create message GreetReply --fields "greeting:string" axiom create node Greet --input GreetRequest --output GreetReply --type unary ``` After these commands you have a buildable package; the sections [Implement the node](#implement-the-node) onward fill in the function body and run it. ## Create a package `axiom init` creates a package: the unit of publishing that holds your message types and nodes. ```bash axiom init my-org/greeter --language python cd greeter ``` The part of the name after the last `/` becomes the directory name, so this creates `./greeter/` containing: - `axiom.yaml` — the package manifest (name, version, language, nodes). See [axiom.yaml reference](/docs/reference/axiom-yaml). - `messages/messages.proto` — the single file where all of the package's message types are defined. - `nodes/` — node implementations and their tests (plus a generated `conftest.py` so `pytest` resolves imports from the package root). - `gen/` — generated Protocol Buffers bindings (created by `axiom generate`). - `.gitignore` — excludes `.axiom/` build artifacts and Python caches. The default language is Go, so pass `--language python` explicitly. Optional flags: `--version` (defaults to `0.1.0`) and `--description`. For a Python package `axiom init` also writes a `requirements.txt` — where a Python package lists its pip dependencies — because `axiom validate` requires that file. It starts empty (valid when your nodes have no dependencies); add your dependencies as you need them. (Pass `--install` to `axiom init` to run the language's dependency install right after scaffolding.) ## Define the input and output messages Every node input and output is a message — a Protocol Buffers type defined in `messages/messages.proto`. `axiom create message` appends a message to that file; all messages live in one file so they can reference each other without import statements. ```bash # Run inside the package directory (where axiom.yaml is). axiom create message GreetRequest --fields "name:string" axiom create message GreetReply --fields "greeting:string" ``` `--fields` is a semicolon-separated list and accepts canonical proto3 (`string name = 1`), proto3 without field numbers (`string name`), or the colon shorthand used above (`name:string`). Field numbers are auto-assigned when omitted. After each message is added, `axiom generate` runs automatically and compiles the bindings Python imports from `gen/`. Run `axiom create message ` without `--fields` to get placeholder fields plus a HINTS comment block explaining proto syntax; edit the fields in your editor, then run `axiom generate` to rebuild `gen/`. Comments written directly above a message or field (no blank line in between) are extracted at publish time and shown in the Axiom registry as documentation. For how messages type-check across a flow, see [the type system](/docs/concepts/type-system). ## Scaffold the node `axiom create node` writes the implementation file and a test file, and registers the node in `axiom.yaml`: ```bash # Run inside the package directory (where axiom.yaml is). axiom create node Greet --input GreetRequest --output GreetReply --type unary ``` This creates `nodes/greet.py` and `nodes/greet_test.py` (the file name is the snake_case of the node name) and appends the node entry to `axiom.yaml`. Rules and behavior: - The node name must be PascalCase (letters and digits only). - `--input` and `--output` must name messages defined in `messages/messages.proto` (or available from an imported package — see [import package types](/docs/guides/import-package-types)). - If you omit `--input`, `--output`, or `--type` at an interactive terminal, the command prompts with a numbered list of available messages and a node type prompt (press Enter to accept the default, `unary`). In non-interactive runs (scripts, CI), `--input` and `--output` are required and an omitted `--type` defaults to `unary`. - `--type` is `unary` (one input in, one output out) or `pipeline` (streaming) — see [the execution model](/docs/concepts/execution-model). ## Implement the node Replace the generated stub in `nodes/greet.py` with a working body: ```python # nodes/greet.py from gen.messages_pb2 import GreetRequest, GreetReply from gen.axiom_context import AxiomContext def greet(ax: AxiomContext, input: GreetRequest) -> GreetReply: """Returns a personalized greeting for the caller's name.""" ax.log.info("greeting requested", name=input.name) return GreetReply(greeting=f"Hello, {input.name}!") ``` Two things to know about this signature: - **The docstring is your registry description.** The first string literal inside the function is extracted when the package is published (`axiom push`) and shown in the Axiom registry as the node's documentation — write a real description before publishing. - **`ax` is `AxiomContext`, the single injection point for every platform capability**: `ax.log` for structured logging (use it instead of `print()` — in production each line carries the execution and trace IDs), and `ax.secrets.get(name)` for tenant secrets (returns a `(value, found)` tuple — see [manage secrets](/docs/guides/manage-secrets)). Full API: [Python SDK reference](/docs/reference/sdk/python). ## Validate the package `axiom validate` checks the package and prints a per-check report: ```bash # Run inside the package directory (where axiom.yaml is). axiom validate ``` 1. **axiom.yaml schema** — required fields, semver format, language, message references. 2. **Proto definitions** — every `.proto` file in `messages/` compiles. 3. **Node signatures** — each node declared in `axiom.yaml` has an implementation with the expected function signature. 4. **Node tests** — every node has a test file that declares at least one test (reported as a warning; this check does not block). 5. **Language files** — the language's required manifest exists; for Python that is `requirements.txt` (an empty file passes). Pass `--json` for structured output in scripts. You rarely run it alone: `axiom test` runs the same validation and stops before testing if any check fails, and `axiom dev` runs it at startup and reports issues as warnings. ## Run the tests `axiom test` regenerates proto bindings, validates the package, then runs the language-native test runner — `pytest nodes/` for Python — streaming its output live. The exit code mirrors the test runner's exit code. `axiom create node` already generated `nodes/greet_test.py` with a `_TestContext` stub (a minimal `AxiomContext` for unit tests) and one test. Replace the generated `test_greet` function at the bottom of that file so it asserts real output values: ```python # nodes/greet_test.py — replace the generated test_greet function with this def test_greet(): ax = _TestContext() result = greet(ax, GreetRequest(name="Ada")) assert isinstance(result, GreetReply) assert result.greeting == "Hello, Ada!" ``` ```bash axiom test ``` Arguments after `--` go straight to pytest, e.g. `axiom test -- -k test_greet` to filter by name. Tests run again inside the push build: the platform's image build runs pytest, and a failing test fails the push. `axiom validate` warns — without blocking — when a node has no test, because a node with zero tests passes silently. Assert output field values — not just types — so the gate actually catches regressions. If your node needs a secret, construct the stub as `_TestContext(secrets_map={"MY_KEY": "test-value"})`. ## Iterate live with axiom dev `axiom dev` is the local loop: it generates the same gRPC service the publish pipeline produces, compiles and runs it natively, and starts an HTTP bridge (default port 8083; change with `--port`) that converts JSON to and from Protocol Buffers so you can test with curl: ```bash axiom dev ``` ```bash # In a second terminal: curl localhost:8083/nodes/Greet -d '{"name": "Ada"}' # → {"greeting":"Hello, Ada!"} ``` The dev server also serves: - `GET http://localhost:8083/docs` — an interactive API reference for your nodes, no client setup needed. - `GET http://localhost:8083/openapi.json` — an OpenAPI spec you can import into Postman, Insomnia, or Bruno. It watches `nodes/`, `messages/`, and `axiom.yaml`, and recompiles and restarts automatically on every change. A failed build leaves the previous service running, so you always have a working server while you fix the error. Stop it with Ctrl-C. ## Next steps - [Push the package and build your first flow](/docs/getting-started/first-flow) — publish `my-org/greeter` and compose its node in the canvas. - [Invoke a flow via the API](/docs/getting-started/invoke-via-api). - [Nodes, packages, and flows](/docs/concepts/nodes-packages-flows) — how the pieces relate. - [Create a node in Python](/docs/guides/create-a-node-python) — the per-language guide with imports, secrets, and pipeline nodes in depth. - Writing in another language? The same commands work for Go, TypeScript, Rust, Java, and C# — start from [Create a node in Go](/docs/guides/create-a-node-go). --- ## Getting started > Run your first flow {#getting-started/first-flow} > Push your package with axiom push, import its nodes into the canvas editor, connect them into a flow, and run it from the editor. # Run your first flow This tutorial takes the package you built in [Write your first node](./first-node.md) and turns it into a running flow: push the package to the platform, import its nodes in the canvas editor, connect them, and run the flow — all without writing any infrastructure code. ## Before you start You need three things: - **A working package.** Complete [Write your first node](./first-node.md) first — you should have a package directory containing an `axiom.yaml` manifest and at least one node that passes `axiom test`. - **A login session.** Run `axiom login` once; `axiom push` refuses to run without it. - **A git remote.** Your package directory must be a git repository with a remote named `origin`, and your current HEAD commit must be pushed to that remote. The platform builds your package from the remote repository, not from your local working tree — uncommitted or unpushed changes are not included. ## Push your package to the platform From your package directory, run: ```bash # Run from your package directory (the one containing axiom.yaml). axiom push ``` `axiom push` does three things: 1. **Validates the package locally.** The same checks as `axiom validate` run first; blocking failures abort the push before anything is sent. 2. **Builds and deploys on the platform.** The CLI sends your repository URL and HEAD commit; the platform builds every node in the package and streams build progress back to your terminal. 3. **Prints your live endpoints.** On success you get the package URL, a `POST` endpoint plus a ready-to-copy `curl` command for every node, and a link to the package's interactive API docs. A pushed package is **private to your tenant** — it does not appear in the public marketplace. You can push the same version repeatedly to iterate — each re-push refreshes the code, descriptions, and OpenAPI spec. (Once a node is deployed, its input/output wire signature is frozen for that version, so a wire-signature change is rejected until you bump the version.) When you are happy with it, promote it to a public, immutable marketplace release with `axiom publish`: ```bash axiom publish my-org/greeter@0.1.0 ``` `axiom publish` performs the private→public transition for a version you have already pushed. Because a published version is **immutable** (it can never be overwritten, only superseded by a new version), the command prompts for a `[y/N]` confirmation before publishing. You can also publish from the Axiom UI. For non-interactive use (CI), pass `--yes` (`-y`) to skip the confirmation prompt — without it a scripted publish blocks waiting on stdin: ```bash axiom publish my-org/greeter@0.1.0 --yes ``` For scripting, `axiom push --json` emits a single JSON result object instead of human-readable progress. Full flag reference: [axiom push](../reference/cli/axiom-push.md). ## Create a flow in the canvas editor Open the Axiom app in your browser and sign in — the canvas editor lives at `/editor` on your deployment's app origin. **What is the app origin?** It is the web host the editor and `/console/*` pages hang off — distinct from the CLI's API endpoint. It is the deployment's app URL (`https://app.axiomide.com`); the CLI already knows this value and prints the editor URL for you after `axiom push`/`axiom login`. Open `https://app.axiomide.com/editor`. Then: 1. Choose **File → New Flow**. 2. Enter a name in the **New Flow** dialog and click **Create**. Save at any point with **File → Save**; a "Saved" confirmation appears when the flow is persisted. ## Import your pushed package Pushed nodes reach the canvas through the Marketplace panel: 1. Click the **Marketplace** toggle (store icon) in the header to open the Marketplace panel. 2. Open the filter dropdown and choose **In Development**. This lists the packages you have pushed — they are visible only to your tenant. If a fresh push is missing, click the refresh button next to the Packages/Flows tabs. 3. Click **+ Import** on your package's card. Importing adds the package to the open flow's library. Switch to the **Library** panel (boxes icon in the header) to see every imported package and its nodes. The library is per-flow: each flow keeps its own list of imported packages. ## Connect nodes into a flow Drag a node card from the Library panel and drop it anywhere on the canvas. Repeat for a second node, then connect them: drag from the **output handle** on the right edge of the first node to the **input handle** on the left edge of the second. ![The canvas editor with a two-node flow: an Echo node connected by an edge to an Identity node, the Library panel listing the package's nodes on the left, the flow inspector on the right, and the Run button below the canvas](../assets/screenshots/editor-canvas-first-flow.png) *The screenshot above is an illustrative two-node flow (Echo → Identity) used to show the canvas layout — the Library panel, inspector, and Run button. The `greeter` package you built in the previous tutorial has a single `Greet` node, which is already a valid flow on its own (it is both the entry and terminal node); you do not need a second node to run it.* An edge means "the source node's output message becomes the target node's input message". The editor checks type compatibility as you connect: - If the output message's fields don't line up with the input message's fields, the edge turns **amber** — a type warning. Click the amber edge to map source fields to destination fields. Type warnings must be resolved before the flow can run. - A pipeline node can only connect to another pipeline node; the editor refuses the connection otherwise. Before a run is allowed, the flow must also have valid topology: - exactly one **entry node** (no incoming edges) — its input message defines what you send when running the flow; - at least one **terminal node** (no outgoing edges) — in a linear flow like this one, the last node's output message is the flow's result; - no cycles, and every node reachable from the entry node. A single node on the canvas is already a valid flow: it is both the entry node and the terminal node. ## Run the flow from the editor Click **Run** at the bottom-center of the canvas. The button is disabled while topology errors or unresolved type warnings remain — hover it to see exactly what to fix. In the **Run Graph** dialog: 1. **Fill in the input form.** It shows one control per field of the entry node's input message. 2. **Leave Debug Mode on** (the default for unary flows) to watch execution node-by-node on the canvas; switch it off to just wait for the result. 3. Click **▶ Run**. The dialog closes and execution plays out on the canvas. The result appears in the result panel docked below the canvas — its **Output** tab shows the terminal node's output message, and errors surface there too. Two warnings can appear in the dialog before you run: a topology error, and a list of secrets that nodes in the flow require but you haven't registered yet — see [Manage secrets in a flow](../guides/manage-secrets.md). The dialog also shows the flow type (**Unary** — single request/response — or **⚡ Pipeline** — streams frames via SSE), a **Debug** button that runs with breakpoints ([Debug a flow](../guides/debug-a-flow.md)), and **Use via API**, which generates a `curl` command for the same run ([Invoke a flow via the API](./invoke-via-api.md)). ## What happens when you run Clicking Run compiles and invokes the flow in one step. The platform compiles your flow into a **compiled artifact** — validating every edge's type compatibility and resolving every node placement — and then starts an **execution** of that artifact. Each run is an execution with its own execution ID; past executions are listed on the **Executions** page (history icon in the header). See [Execution model](../concepts/execution-model.md) for unary vs pipeline execution and durability, and [The type system](../concepts/type-system.md) for how edge compatibility is checked. ## Next steps - [Invoke a flow via the API](./invoke-via-api.md) — call the flow you just built with `curl` or a generated client. - [Nodes, packages, and flows](../concepts/nodes-packages-flows.md) — the mental model behind what you just did. - [Debug a flow](../guides/debug-a-flow.md) — breakpoints, stepping, and live state inspection in the editor. --- ## Getting started > Invoke a flow via API {#getting-started/invoke-via-api} > Create an API key, copy the ready-made curl command from the Use via API dialog, and call your compiled flow over HTTP — one-shot or as an SSE stream. # Invoke a flow via API Every flow you build in the canvas can be invoked over HTTP. This page takes the flow from [Build your first flow](./first-flow.md) and calls it from outside the editor: create an API key, copy a ready-made `curl` command from the **Use via API** dialog, run it, and read the result — about ten minutes. ## Prerequisites - A saved flow in the editor — finish [Build your first flow](./first-flow.md) first. - An account you can sign in to the editor with. The API key you create must belong to the same account that owns the flow. - `curl` on your machine. ## Create an API key API requests authenticate with a bearer API key. To create one: 1. In the app, go to **Console → API Keys** (`/console/api-keys`). 2. Click **Create key**, fill in **Name** (for example `my-laptop`), and click **Create key** in the dialog. 3. The **Save your API key** dialog shows the raw key — a 64-character hex string — **exactly once**. Copy it now and click **Done**; the key is never shown or recoverable again (the platform stores only its SHA-256 hash). Export it so the commands below can use it: ```bash export AXIOM_API_KEY="" ``` After the dialog closes, the key list shows only the name and a masked form (first 8 characters … last 4). To revoke a key, click **Revoke <name>** in its row and confirm with **Revoke key**; revocation takes effect on the key's very next use. Full management details: [Create and manage API keys](../guides/api-keys.md). The key authorizes requests as your tenant: it can invoke flows your account owns and nothing belonging to other tenants — see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md). ## Copy the curl command from the Use via API dialog The editor generates the invoke command for you, two ways: - Open your flow, click **Run** at the bottom of the canvas, then click **Use via API** in the run dialog's footer. Any input values you filled into the run form are pre-filled into the command. - Or open the flow inspector's **API** section and click **Use via API (curl)**. The same section shows the invoke URL and an **Open interactive docs** button — see [Use the interactive API docs](../guides/use-interactive-api-docs.md). The dialog compiles the current canvas into a compiled artifact and shows a complete `curl` command containing: - the invoke URL — `https://api.axiomide.com/invocations/v1/flows/invoke`, or `https://api.axiomide.com/invocations/v1/flows/invoke/stream` when the flow runs in pipeline mode; - a JSON body whose `graph_id` is the compiled artifact's 26-character id and whose `input` is the entry node's input message as a JSON object. Click the copy button, then replace `$AXIOM_API_KEY` with your key — or keep the variable and change the quotes around the `Authorization` header to double quotes so your shell expands it. The `graph_id` is pinned to this version of the flow: if you edit the flow, reopen the dialog to get an updated command. ## Invoke the flow and read the result The copied command has this shape (your origin, `graph_id`, and input are filled in by the dialog; the header is quoted here so `$AXIOM_API_KEY` expands): ```bash curl -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD", "input": { "text": "hello" }, "wait": true }' ``` `"wait": true` makes the request block until the execution completes — up to 30 seconds by default; set `"timeout_seconds"` to wait longer. On success the response is HTTP 202 with the result inline: ```json { "accepted": true, "execution_id": "4bf92f3577b34da6a3ce929d0e0e4736", "result": { "success": true, "output": { "text": "hello" }, "completed_at": 1765432100000 } } ``` - `execution_id` identifies this execution everywhere — execution history, debug events, and traces all reference it ([Debug a flow](../guides/debug-a-flow.md)). - `result.output` is the terminal node's output message, decoded to JSON. The request body accepts these fields: | Field | Required | Meaning | |---|---|---| | `graph_id` | yes | The compiled artifact to execute. | | `input` | yes | The entry node's input message as a JSON object; the platform converts it to the typed message. | | `wait` | no | Block until the execution completes and include `result` in the response. Without it the response returns immediately with just `accepted` and `execution_id`. | | `timeout_seconds` | no | How long to wait for completion (default 30). | | `config_id` | no | Named flow config profile to apply — see [Flow configs](../guides/flow-configs.md). | A missing, invalid, or revoked key gets HTTP 401 with `{"error":"unauthorized"}`. Other failures return a structured error body — see the [error catalog](../reference/error-catalog.md) and the full [HTTP API reference](../reference/http-api.md). ## Stream a pipeline flow over SSE A flow in pipeline mode emits a sequence of result frames instead of a single response, so it uses the streaming endpoint `/invocations/v1/flows/invoke/stream`. The **Use via API** dialog detects pipeline mode and generates this variant automatically: ```bash curl -N -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke/stream' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD", "input": { "text": "hello" } }' ``` `-N` disables curl's output buffering so frames print as they arrive. The response is Server-Sent Events (`Content-Type: text/event-stream`); each frame is one `data:` line of JSON: ```text data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":0,"payload":{"text":"chunk 1"},"is_final":false} data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":1,"payload":{"text":"chunk 2"},"is_final":false} data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":2,"payload":{"text":"chunk 3"},"is_final":false} data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":3,"is_final":true,"success":true} ``` - `payload` is the terminal node's output message for that frame, decoded to JSON. Payload-carrying frames always have `"is_final": false`. - The stream ends with a separate final frame — `"is_final": true` and no `payload`; its `success` reports whether the execution succeeded, and on failure `error` carries the message. - The stream times out after 30 seconds by default; set `"timeout_seconds"` in the body to extend it. A timeout ends the stream with a final frame whose `error` is `"timeout waiting for pipeline result"`. - `wait` does not apply here — streaming always delivers frames as they are produced. A flow that contains a pipeline node always runs in pipeline mode; for other flows, the flow type is shown in the run dialog and can be switched there — see [Execution model](../concepts/execution-model.md). ## Next steps Your flow is now an endpoint any program can call. From here: - [Use the interactive API docs](../guides/use-interactive-api-docs.md) — browse and try your flow's generated OpenAPI docs from the inspector. - [Flow configs](../guides/flow-configs.md) — run the same flow with named parameter profiles via `config_id`. - [Debug a flow](../guides/debug-a-flow.md) — inspect any `execution_id` you got back from the API. - [HTTP API reference](../reference/http-api.md) — every endpoint, field, and error shape. --- ## Concepts > Nodes, packages, and flows {#concepts/nodes-packages-flows} > Axiom's core object model — how messages, nodes, packages, flows, executions, and the entry and terminal nodes relate to each other. # 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) 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 ` 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). ### Generic node and Instance: typed at use time, not at publish time Most nodes have a fixed, nominal input/output shape decided when the author writes them. A **Generic node** (`kind: generic` in `axiom.yaml`) is different: it's a real, published, deployed connector — a database query, an HTTP call — whose port(s) are deliberately *generalized*, because the author can't know the exact table columns or API response shape any one flow will need at publish time. A Generic node cannot be dragged onto the canvas and placed by itself. An **Instance** binds a Generic node's port(s) to a real, specific type via a field mapping — created with `axiom instance create` or the marketplace's Generic Nodes tab, never hand-authored in `axiom.yaml`. It is effectively a first-class node in its own right: it gets its own registry endpoint (`POST /v1/instances`, no build, no Knative deploy — it shares the Generic node's already-deployed image), its own **shell package** row so it shows up in the marketplace and Library panel like any other package, and once created it's placed in a flow exactly like a concrete node — same `package`/`node` shape, fully-typed ports, no residual generic typing. A facade field can itself be another message, defined inline and recursively, with `repeated`/`optional` modifiers — see [Configure an Instance from a Generic node](../guides/configure-an-instance.md) for the full authoring walkthrough (console and CLI) and [the `axiom.yaml` reference](../reference/axiom-yaml.md#generic-and-instance-nodes) for the schema. ## 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 ` 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 refreshes the code, descriptions, and OpenAPI spec so you can iterate (a deployed node's input/output wire signature is frozen — a wire change needs a version bump). Publishing — with `axiom publish @` (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://api.axiomide.com/invocations/v1/flows/invoke' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"graph_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). --- ## Concepts > The type system {#concepts/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. # 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 ` 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 [@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///` 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. An imported message is usable **two** ways: as a node's whole input/output type (reference it by simple name in `axiom.yaml`), and as a **field** of a message you declare locally. To embed one as a field, add an import of the package's stable flat proto filename to your `messages/messages.proto` — `__messages.proto`, the package name lowercased with every `/` and `-` turned into `_` (so `my-org/payments` → `my_org_payments_messages.proto`) — then write the field type as the imported message's proto-qualified name (the `package …;` declared in the downloaded `.proto` plus the message name): ```proto // messages/messages.proto import "my_org_payments_messages.proto"; message Checkout { string order_id = 1; my_org.payments.PaymentResult payment = 2; // the canonical imported type } ``` Embedding reuses the one canonical published type instead of duplicating a structurally-identical local copy, so both sides of a flow edge stay the same protobuf type. This is fully wired for Go and Python; the worked example and per-language support are in [Import message types from another package](/docs/guides/import-package-types). To find types worth importing, search the marketplace with `axiom search --type messages `, and list a specific package's messages with `axiom info `. 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 ` prints both URLs: the machine-readable spec at `/packages/@/openapi.json` and the interactive documentation at `/packages/@/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. --- ## Concepts > Execution model {#concepts/execution-model} > How Axiom runs a flow: immutable compiled artifacts, unary vs pipeline (SSE) invocation, the Executions list and detail pages, and durable resume after worker failure. # Execution model Every run of a flow is an **execution**: the platform compiles the flow into an immutable compiled artifact, assigns an execution ID when the request is accepted, and records durable checkpoints as each node completes. A flow runs in one of two modes — **unary** (one input message in, one result out) or **pipeline** (a stream of frames delivered over Server-Sent Events) — and either way the run survives infrastructure failure: if a worker dies mid-run, a replacement resumes from the last checkpoint instead of starting over. ## Flows run as compiled artifacts Invoking a flow never executes an editable draft — it always executes a [compiled artifact](/docs/reference/glossary#compiled-artifact). Compilation validates that every edge connects type-compatible messages, resolves every node placement, fixes the [entry node](/docs/reference/glossary#entry-node) and [terminal node](/docs/reference/glossary#terminal-node), and freezes the execution mode (unary or pipeline) into the artifact. An artifact is immutable: editing the flow and compiling again produces a new artifact with a new ID. The artifact ID is the `graph_id` you pass when invoking the flow over HTTP. In the editor, the flow inspector's **API** section shows the flow's invoke endpoint, and its **Use via API (curl)** action compiles the current canvas and produces a ready-to-paste curl command with the compiled `graph_id` filled in. That `graph_id` is pinned to the version of the flow that was compiled — after editing the flow, reopen the dialog to get an updated command. The same section's **Open interactive docs** button opens a live API reference generated for that exact artifact (see [Use the interactive API docs](/docs/guides/use-interactive-api-docs)). Workers cache compiled artifacts by ID, so repeat invocations skip graph resolution entirely. ## Unary invocation A unary flow handles one input message and produces one result per invocation. This is the default mode for flows that contain no pipeline nodes. Invoke it with `POST /invocations/v1/flows/invoke` on your deployment's origin, authenticated with an API key created under **Console → API Keys** (the key must belong to the same account that owns the flow): ```bash # Replace the origin and graph_id with the values from "Use via API (curl)" curl -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"graph_id": "01JX3Z9KQ4WV8RT2M5XB7CDEFG", "input": {"name": "Ada"}, "wait": true}' ``` The request body fields: - `input` — a JSON object matching the entry node's input message. The platform converts it to protobuf for you; callers never deal with binary serialization. - `wait` — when `true`, the call blocks until the flow completes and the response carries `result`, with the terminal node's output decoded back to JSON in `result.output`. The default wait timeout is 30 seconds; set `timeout_seconds` to override it. When `false`, the flow runs asynchronously and only `execution_id` is returned. - `webhook` — **not currently supported.** Asynchronous webhook delivery was removed along with the egress-delivery service, so a request that includes a non-empty `webhook.url` is rejected with HTTP 400 (`webhook delivery not supported`) rather than silently accepted and then dropped. For asynchronous runs, omit `webhook` and poll the execution result (or use `wait: true`). - `config_id` — optional [flow config](/docs/reference/glossary#flow-config) profile; when omitted the default hierarchy applies (tenant default → flow default). See [Flow configs](/docs/guides/flow-configs). A successful `wait: true` response (HTTP 202): ```json { "accepted": true, "execution_id": "0af7651916cd43dd8448eb211c80319c", "result": { "output": { "greeting": "Hello, Ada!" }, "success": true, "completed_at": 1780000000 } } ``` The `execution_id` identifies this run everywhere — the Executions pages, the events API, and distributed traces all reference it. ## Pipeline invocation streams frames over SSE A pipeline node is a streaming generator: declared with `type: pipeline` in `axiom.yaml`, it emits a sequence of output frames instead of a single output message. A flow that contains any pipeline node always runs in pipeline mode — frames flow continuously between nodes, and results stream back to the caller progressively. Pipeline flows compile to a pipeline artifact and must be invoked on the SSE endpoint — `wait` applies only to unary invokes: ```bash # -N disables curl's buffering so frames render as they arrive curl -N -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke/stream' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"graph_id": "01JX3ZAB2PIPE8RT2M5XB7CDEF", "input": {"name": "Ada"}}' ``` The response is a Server-Sent Events stream (`Content-Type: text/event-stream`). Each event is one frame: ```text data: {"execution_id":"0af7651916cd43dd8448eb211c80319c","frame_index":0,"payload":{"greeting":"Hello, Ada!"},"is_final":false,"success":true} data: {"execution_id":"0af7651916cd43dd8448eb211c80319c","frame_index":1,"is_final":true,"success":true} ``` `payload` is the flow output for that frame, decoded to JSON using the terminal node's output message. The stream ends when a frame arrives with `is_final: true`; on failure the final frame carries an `error` string instead of a payload. If no final frame arrives within the timeout (30 seconds by default, `timeout_seconds` to override), the stream closes with a final error frame. ## Running a flow from the editor The **Run** button sits at the bottom-center of the editor canvas and opens the **Run Graph** dialog. The dialog shows the flow's type — "Unary — single request / response" or "⚡ Pipeline — streams multiple frames via SSE" — with a **Switch** action to toggle between them. Switching is disabled when the flow contains a pipeline node, because a pipeline node's streaming frames cannot be delivered through a unary run. Fill in the entry node's input fields and press **▶ Run**. Unary runs also offer a **Debug Mode** toggle to watch the execution in real time node by node (see [Debug a flow](/docs/guides/debug-a-flow)). Every run started from the editor is recorded as an execution, exactly like an API-triggered run. ## The executions list Click **Executions** in the top header to open `/executions`: one row per execution, newest first, with **Status**, **Started**, **Duration**, **Flow**, and **Execution** columns. Filter by status, by flow, or by date range. The same data is available over HTTP at `GET /invocations/v1/executions`. An execution's status is one of: | Status | Meaning | |---|---| | Queued | Accepted, not yet picked up by a worker | | Running | A worker is executing nodes | | Paused (HITL) | Waiting on a human-in-the-loop approval | | Waiting on gate | Waiting for parallel branches to converge at a gate | | Debug paused | Stopped at a breakpoint in a debug session | | Completed | Terminal node finished; result available | | Failed | A node failed and the flow did not complete | | Compensating | Running compensation steps after a failure | | Compensated | Compensation finished draining; the flow ended rolled back | | Cancelled | Stopped by an explicit cancel request | A running execution can be cancelled with `DELETE /invocations/v1/flows/{execution_id}`; cancellation takes effect immediately, even if the flow is blocked inside a long-running node. ## Execution detail Selecting an execution opens `/executions/`: a read-only canvas that replays the run on the flow's topology, a timeline scrubber to step through it, and a header showing the status plus Started, Updated, Completed, and Duration timestamps. Six tabs break the run down: - **Timeline** — the run's events in time order, driving the scrubber. - **Events** — the persisted per-node event log, filterable by node, including `NODE_RETRY_SCHEDULED`/`STARTED`/`EXHAUSTED` events with an "attempt N/M" chip on the row. Also available at `GET /invocations/v1/executions/{id}/events`. - **Checkpoints** — the durable checkpoint written after each completed node, including its recorded output. Also available at `GET /invocations/v1/executions/{id}/checkpoints`. - **Pauses** — human-in-the-loop pauses and how each was resumed. - **Branches** — gate activity for parallel branches. - **Output** — the flow's final output (or failure), decoded against the terminal node's output message. When any node in the run retried, a **retry rollup banner** appears below the header, above the tabs: a per-node summary such as "3 attempts, succeeded on #3" or "5 attempts, failed after exhausting retries," derived from the run's own event log. This is a run-level summary distinct from the per-event "attempt N/M" chip on individual rows in the Events tab above — the banner tells you at a glance which nodes retried and how the retries resolved without opening the Events tab and reading the log; it's hidden entirely when nothing in the run retried. The detail page is fed entirely from persisted history, so it renders the same run identically on every reload — including after the run finished. ## Durable resume After every completed node, the platform durably records a checkpoint containing that node's output before moving on. Checkpoints are what make an execution survive infrastructure failure: if the worker executing a flow dies mid-run, the platform redelivers the run to a replacement worker, which loads the checkpoint history, skips every node that already completed, and re-runs only the node that was in flight when the worker died. The execution keeps its ID and completes normally — callers waiting on a result or an SSE stream do not need to retry. Checkpoints are retained for 30 days by default and are visible on the **Checkpoints** tab of the execution detail page. Large node outputs are stored out-of-band and rehydrated transparently when a resumed run (or the detail page) needs them. Pauses are durable the same way: an execution in **Paused (HITL)** or **Waiting on gate** holds its state indefinitely — across worker restarts — until it is resumed or times out per its configured policy. ## Retry and backoff A node can carry a **retry policy** (see [Configure per-node retry and backoff](/docs/guides/flow-configs#configure-per-node-retry-and-backoff)): on a retryable failure, instead of ending the execution the platform durably schedules another attempt and releases the worker slot in the meantime — the wait never blocks a worker, which is what lets a retry back off for minutes at a time without starving other flows. A node with a retry policy carries a 🔄 badge on the canvas, and a node currently backing off shows a live **retrying** state with a countdown to its next attempt. ### Attempts, backoff, and timeouts Each attempt is durably counted against the node's `(execution_id, step_number)` — the same identity a resumed execution replays after a worker crash, so a crash mid-backoff picks the attempt count back up rather than resetting it. The wait between attempts grows exponentially up to a configured ceiling, bounded by two independent timeouts: a per-attempt timeout (how long any single attempt may run) and an overall timeout across every attempt (ScheduleToClose) — once the overall budget is spent, retries stop even with attempts remaining. ### Retryable vs. non-retryable failures Not every failure is worth retrying. A transport-level failure (a dropped connection, a timeout) is retryable by default; a deterministic business failure — the node ran and decided the input was invalid — is non-retryable by default, because retrying it just reproduces the same failure. A policy can narrow this further with an explicit non-retryable list, but it can never widen it to force a retry of a business failure. ### Scope Retry applies to **unary** nodes only in this release. Pipeline (streaming) nodes and heartbeat-based liveness checks are explicit non-goals. ## Failure edges and typed errors A node's terminal failure no longer has to end the execution. If the node has an outgoing **failure edge** (an edge authored to fire only on failure, after any retry policy exhausts its attempts), the worker builds a typed error payload — a closed error code, a safe message, and structured detail — and routes it down that edge exactly like a normal output, **without** cancelling the flow. The receiving node runs as usual, and if its own output reaches the flow's terminal node, the execution **completes** instead of failing. On the canvas, a node with a failure edge shows a second, red output handle so the branch is visible next to its normal outputs. Ordering is fixed and deterministic: a retry policy's attempts exhaust first, then a failure edge (if one exists) gets a chance to route the error, then join-tolerance on any downstream fan-in gate, and only then does the platform fall back to cancelling the flow. A failure a failure edge handles is never also recorded as a failed join input — the two mechanisms never double-handle the same failure. ## Compensation (saga rollback) Some nodes declare a paired **compensation node** — the undo for whatever that node did. The platform tracks these as a stack, pushed just before a node with a compensation link runs and popped once it succeeds. When an execution reaches a terminal failure with a non-empty stack, the platform sets its status to **Compensating** and unwinds the stack **LIFO** — most recently succeeded step compensated first — dispatching each compensation node in turn and popping its frame only after that node itself succeeds. Draining the whole stack moves the execution to the **Compensated** terminal status; a compensation node that itself fails stops the drain and the execution ends **Failed** instead, leaving the remaining frames recorded for inspection. Compensation survives a worker crash the same way ordinary execution does: a replacement worker reloads the stack and resumes the drain from the exact frame that was in flight, never re-running a compensation that already succeeded. An author can also force a rollback explicitly by routing a failure edge into one marked as rollback-forcing — useful when a node's failure should undo prior work even though the author is otherwise handling that failure on the canvas. --- ## Concepts > Sandboxing and tenancy {#concepts/sandboxing-and-tenancy} > What node code can and cannot do at runtime: the sidecar is a node's only channel to the platform, and tenant isolation is enforced by the platform — never by your code. # Sandboxing and tenancy Node code runs in an isolated container with no direct access to the platform. Everything a node does beyond pure computation — logging, reading secrets, inspecting the running flow — goes through the sidecar, and the sidecar enforces tenant isolation. You never write isolation code yourself, and node code cannot opt out of it. This page explains the boundary: how the sandbox works, what node code can and cannot do, and which guarantees you can build on. ## How node code is sandboxed Every published package runs as two containers side by side in one auto-scaling service: - **user-node** — the image built from your package. It runs the generated service wrapper around your handler functions and listens on a Unix socket at `/tmp/axiom.sock`, on a volume shared only with its sidecar. - **sidecar** — the platform-owned proxy. It is the only container that receives execution traffic, and it relays each invocation to the user-node over the shared socket. Node code has no platform endpoint to call. All platform capabilities reach node code exclusively through `ax` on `AxiomContext`: the sidecar relays every `ax.log` and `ax.secrets` call on the node's behalf, and there is no separate address a node can dial to reach the platform directly. Each invocation delivers exactly this to your handler: the serialized input message, the execution ID, the flow ID, the tenant identity, a variables map holding resolved flow config values and secrets, and a read-only structural view of the running flow (`ax.reflection.flow`). Nothing else about the platform is visible. Log lines written with `ax.log` travel back through the same sidecar channel and are stamped with the execution ID and trace context, so they are searchable per execution in production. The variables map is for flow config values and secrets only — it is not working memory or agent state. Within one execution, state belongs in your typed messages. Cross-execution agent memory is out of scope for the beta. ## What node code can do A node handler is a plain function: it receives `AxiomContext` (`ax`) and a typed input message, and returns a typed output message. Through `ax` it can: - **Log** — `ax.log.debug/info/warn/error`, structured, per invocation. - **Read secrets** — `ax.secrets.get(name)` returns the plaintext value of any secret the calling tenant has registered, plus a found flag. - **Inspect the running flow** — `ax.reflection.flow` exposes the nodes, edges, and current position, read-only. - **Mutate the running flow** — `ax.mutation.flow.add_node` and `add_edge`, available only to nodes declared `mutation_capable: true` in `axiom.yaml`; every mutation is validated by the platform. Node code can also make ordinary outbound network calls. Calling an external API — an LLM provider, a database you host — is the normal use of `ax.secrets`: ```python # nodes/call_model.py — scaffolded by: # axiom create message Prompt --fields "text:string" # axiom create message Reply --fields "text:string" # axiom create node CallModel --input Prompt --output Reply from gen.messages_pb2 import Prompt, Reply from gen.axiom_context import AxiomContext def call_model(ax: AxiomContext, input: Prompt) -> Reply: api_key, ok = ax.secrets.get("OPENAI_KEY") if not ok: ax.log.error("secret not registered", name="OPENAI_KEY") return Reply() ax.log.info("calling model", prompt_chars=len(input.text)) # Use api_key with any HTTP client here — outbound calls are allowed. return Reply() ``` ## What node code cannot do - **Call platform services directly.** No platform service address is exposed to the node container. Every platform capability — logging, secrets — is accessed through `ax`. Node code that attempts to dial platform endpoints directly will not reach them. - **Read another tenant's secrets.** The platform resolves secrets for the tenant invoking the flow before any node code runs; the variables map a node receives only ever contains that tenant's secrets. - **See engine internals.** Flow reflection is a read-only structural mirror; compiled adapters and engine-internal types are not exposed. When the platform rejects a mutation, the node sees only a human-readable error message — structured engine error codes never cross the boundary. - **Mutate a flow that does not allow it.** `ax.mutation.flow` calls are buffered and validated by the platform after the handler returns; they are accepted only when the compiled flow contains a node declared `mutation_capable: true`, and every accepted mutation is type-checked. ## How tenant isolation is enforced Tenant isolation happens at two checkpoints, neither of them in node code: 1. **At the platform edge.** Every invocation is authenticated, and the tenant identity is resolved from the caller's credentials before any node code runs (see [Create and manage API keys](../guides/api-keys.md)). That identity travels with the execution through every hop. 2. **At secret resolution.** When an execution starts, the platform fetches the calling tenant's secrets — stored encrypted at rest — and injects the plaintext values into the invocation's variables map. A node can only read what was injected for its caller's tenant (see [Manage secrets in a flow](../guides/manage-secrets.md)). Because enforcement lives in the platform and the sidecar, a bug in node code — yours or a marketplace package's — cannot widen its own access. > Cross-execution agent memory would add a third checkpoint, but it is out of > scope for the beta SDK — there is no node-side memory call to isolate at this > layer today. ## What you can rely on, and what you own You can rely on: - Secrets stored on the **Secrets** page of the console (`/console/secrets`) are encrypted at rest and readable only by executions invoked under your tenant. - One tenant's executions never observe another tenant's payloads or secrets. - New platform capabilities arrive as new properties on `ax` — node function signatures never change when the platform grows. You still own: - **Where your data goes.** The sandbox does not restrict outbound network calls; what a node sends to external services is your code's responsibility. - **Secret hygiene in your own code.** `ax.secrets.get` returns the plaintext value — do not write it to `ax.log` or copy it into an output message. - **Your dependencies.** Third-party libraries in your package run inside your node container with the same access your code has. --- ## Guides > Create a node in Python {#guides/create-a-node-python} > Scaffold a Python package, define protobuf messages, implement a typed node function with AxiomContext, test it with pytest, and push it to Axiom. # Create a node in Python A node is a plain Python function with a typed input message and a typed output message. This guide takes one node from empty directory to a pushed package: scaffold, messages, implementation, tests, push. ## The five commands The whole path, end to end. Each step is explained in the sections below. ```bash # Run from the directory that will contain your package axiom init your-handle/order-tools --language python --description "Order processing utilities" cd order-tools touch requirements.txt # axiom validate requires this file for Python packages axiom create message OrderRequest --fields "order_id:string; quantity:int32; unit_price_cents:int64" axiom create message OrderConfirmation --fields "order_id:string; total_cents:int64; accepted:bool" axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation # ...implement nodes/process_order.py, edit nodes/process_order_test.py... axiom test axiom push ``` ## Before you start - Install the Axiom CLI and authenticate with `axiom login` — see [Installation](../getting-started/installation.md). `axiom login` uses the OAuth Device Flow and stores the resulting API key in `~/.axiom/credentials`; in CI, set `AXIOM_API_KEY` instead. - Python 3.10 or newer (the generated code uses `dict | None` syntax). - `pip install grpcio-tools pytest` — `axiom generate` compiles your `.proto` files with `python -m grpc_tools.protoc` (falling back to a standalone `protoc` if installed), and `axiom test` runs your tests with pytest. - For `axiom push`: the package must live in a git repository with an `origin` remote, because the platform builds from the remote at your HEAD commit — not from your working tree. ## Create the package and its messages `axiom init / --language python` creates a subdirectory named after the package (the part after the last `/`) containing `axiom.yaml`, `messages/messages.proto`, `nodes/conftest.py`, and a `.gitignore`. Package names must be scoped (`your-handle/package-name`); pushing rejects unscoped names. Use `--description` to set the package description and `--version` to override the default `0.1.0`. Every node input and output is a [message](../reference/glossary.md#message) defined in `messages/messages.proto`. All messages live in that single file so they can reference each other without imports. Scaffold them with `axiom create message`: ```bash # Run inside the package directory (where axiom.yaml lives) axiom create message OrderRequest --fields "order_id:string; quantity:int32; unit_price_cents:int64" axiom create message OrderConfirmation --fields "order_id:string; total_cents:int64; accepted:bool" ``` `--fields` takes semicolon-separated definitions in colon shorthand (`name:string`), proto3 without field numbers (`string name`), or canonical proto3 (`string name = 1`); field numbers are auto-assigned when omitted. Omit `--fields` to get a placeholder block with syntax hints to edit by hand. After appending the message, the command runs `axiom generate` automatically to compile bindings into `gen/` (skip with `--no-generate` when creating several messages at once). The result in `messages/messages.proto`: ```protobuf // messages/messages.proto message OrderRequest { string order_id = 1; int32 quantity = 2; int64 unit_price_cents = 3; } message OrderConfirmation { string order_id = 1; int64 total_cents = 2; bool accepted = 3; } ``` Comments written directly above a message or field declaration (with no blank line in between) are extracted at publish time and shown in the Axiom registry as documentation; a comment separated by a blank line is ignored. See [The type system](../concepts/type-system.md) for how messages type-check flow edges, and [Import message types from another package](import-package-types.md) to reuse messages published by others. ## Scaffold the node `axiom create node` writes the implementation file and a test file into `nodes/`, then records the node in `axiom.yaml`: ```bash # Run inside the package directory axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation ``` The node name must be PascalCase. Input and output messages must be defined in `messages/messages.proto` or available from an imported package. If you omit `--input` or `--output` in an interactive terminal, the command prompts with a numbered list of available messages. The default node type is `unary` (one input message in, one output message out per invocation); pass `--type pipeline` for a streaming node instead. This creates `nodes/process_order.py` and `nodes/process_order_test.py`, generates `gen/messages_pb2.py` (your message bindings) and `gen/axiom_context.py` (the `AxiomContext` type), and updates `axiom.yaml`: ```yaml # axiom.yaml name: your-handle/order-tools version: 0.1.0 language: python description: Order processing utilities nodes: - name: ProcessOrder input: OrderRequest output: OrderConfirmation ``` See the [axiom.yaml reference](../reference/axiom-yaml.md) for every manifest field. ## Implement the node function A Python node is a function whose first parameter is the `AxiomContext` (`ax`) and whose second parameter is the input message; it returns the output message. Declare it as `async def` if you need `await` other async platform APIs. ```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 with the calculated total. Orders with zero quantity are rejected. """ if input.quantity <= 0: ax.log.warn("rejected order", order_id=input.order_id) return OrderConfirmation(order_id=input.order_id, accepted=False) total = input.quantity * input.unit_price_cents ax.log.info("order accepted", order_id=input.order_id, total_cents=total) return OrderConfirmation( order_id=input.order_id, total_cents=total, accepted=True, ) ``` The docstring directly inside the function is extracted at publish time and shown in the Axiom registry as the node's documentation — write a plain description of what the node does. `ax` is the single injection point for every platform capability: - `ax.log` — structured logger with `debug` / `info` / `warn` / `error`, taking keyword attributes. Use it instead of `print()`: in local development it writes plain text; in production it writes JSON with `trace_id`, `span_id`, and `execution_id` baked in. - `ax.secrets.get(name)` — returns a `(value, found)` tuple. Use it instead of environment variables for API keys; list the secret names a node reads under `required_secrets` in `axiom.yaml` so they are validated during publish. See [Manage secrets in a flow](manage-secrets.md). - `ax.execution_id`, `ax.flow_id`, `ax.tenant_id` — identifiers for the current [execution](../reference/glossary.md#execution), the flow, and the tenant. The full surface is documented in the [Python SDK reference](../reference/sdk/python.md). ## Test the node `axiom create node` generated `nodes/process_order_test.py` containing a `_TestContext` class — a minimal `AxiomContext` implementation with a no-op logger and a secrets map you can populate (`_TestContext(secrets_map={"OPENAI_KEY": "sk-test"})`). Replace the generated placeholder test with assertions on real output fields: ```python # nodes/process_order_test.py — replaces the generated placeholder test; # keep the generated _TestContext class and imports above it. def test_process_order_accepts_and_totals(): ax = _TestContext() input_msg = OrderRequest(order_id="ord-1", quantity=3, unit_price_cents=250) result = process_order(ax, input_msg) assert result.order_id == "ord-1" assert result.total_cents == 750 assert result.accepted is True def test_process_order_rejects_zero_quantity(): ax = _TestContext() result = process_order(ax, OrderRequest(order_id="ord-2", quantity=0)) assert result.accepted is False ``` Create a `requirements.txt` listing your Python dependencies — it may be empty, but `axiom validate` fails without it. Then run: ```bash # Run inside the package directory axiom test ``` `axiom test` compiles proto bindings (`axiom generate`), validates the package (`axiom validate` — `axiom.yaml` schema, proto definitions, node signatures, node tests, and language files such as `requirements.txt`), then runs `pytest nodes/` with output streamed live. Pass arguments to pytest after `--`, e.g. `axiom test -- -k test_process_order`. Tests run again inside the push build: the platform's image build runs pytest, and a failing test fails the push. `axiom validate` warns — without blocking — when a node has no test. Assert output fields meaningfully — not just type-checked — so the gate actually catches regressions. ## Stream output with a pipeline node Pass `--type pipeline` to scaffold a streaming node: ```bash axiom create node StreamConfirmations --input OrderRequest --output OrderConfirmation --type pipeline ``` A pipeline node takes an iterator of input frames and yields any number of output frames: ```python # nodes/stream_confirmations.py — generated shape of a Python pipeline node from typing import Iterator from gen.messages_pb2 import OrderRequest, OrderConfirmation from gen.axiom_context import AxiomContext def stream_confirmations(ax: AxiomContext, inputs: Iterator[OrderRequest]) -> Iterator[OrderConfirmation]: for input in inputs: yield OrderConfirmation(order_id=input.order_id, accepted=True) ``` For the start node of a flow running in pipeline mode, the iterator yields exactly one item. See [execution model](../concepts/execution-model.md) for how unary and pipeline mode differ end to end. ## Push the package `axiom push` deploys the package to the Axiom platform, visible only to your own tenant — it does not appear in the public marketplace. The platform clones your repository at your current HEAD commit and builds from that, so commit and push first: ```bash # Run inside the package directory git add -A && git commit -m "ProcessOrder node" && git push axiom push ``` Push validates the package locally, then streams the platform pipeline's progress: server-side validation, code generation, Docker image build, and deployment. It requires a prior `axiom login`, a git remote named `origin`, and the current HEAD pushed to that remote. You can push the same version repeatedly — each re-push refreshes the code, descriptions, and OpenAPI spec — which lets you iterate on deployed code before publishing. (A deployed node's input/output wire signature is frozen; a wire change is rejected until you bump the version.) When you are satisfied, run `axiom publish @` (or use the Axiom UI) to make it available to others as an immutable versioned release. Use `--json` for a single machine-readable result object instead of progress output. After pushing, `axiom info your-handle/order-tools` shows the package's nodes, messages, and live endpoint. ## Next steps - Place the node in a flow on the canvas: [Build your first flow](../getting-started/first-flow.md). - Invoke the flow over HTTP: [Invoke a flow via the API](../getting-started/invoke-via-api.md). - Iterate locally with hot reload: `axiom dev` starts a local development server that watches `nodes/`, `messages/`, and `axiom.yaml`, exposes each node at `POST http://localhost:8083/nodes/`, and converts JSON payloads to and from protobuf automatically. - Understand how nodes, packages, and flows relate: [Nodes, packages, and flows](../concepts/nodes-packages-flows.md). --- ## Guides > Create a node in Go {#guides/create-a-node-go} > Scaffold a Go node with the Axiom CLI, implement the handler against generated protobuf types, test it, and run it locally with hot reload. # Create a node in Go This guide takes a Go node from empty directory to a locally running, HTTP-invocable handler. A node is a plain Go function — no SDK imports in your business logic, no Dockerfile, no gRPC plumbing. **Prerequisites** - The `axiom` CLI installed and on your `PATH` (see [installation](/docs/getting-started/installation)). - A Go toolchain. Generated Go packages target `go 1.22` (the version the generated `go.mod` declares), so any Go 1.22+ toolchain builds your package. The higher "Go 1.25 or newer" in [installation](/docs/getting-started/installation) applies **only** to building the `axiom` CLI itself from source — not to the packages you author (and not at all once you install a prebuilt CLI binary). - `protoc` (install guide: https://grpc.io/docs/protoc-installation/) and the Go plugin: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`. ## The short path Four commands scaffold a working Go package with one node: ```bash axiom init demo/text-tools --language go cd text-tools axiom create message TextRequest --fields "text:string" axiom create message WordCountResult --fields "count:int32" axiom create node CountWords --input TextRequest --output WordCountResult --type unary ``` `axiom init` creates a `text-tools/` directory (the part of the package name after the last `/`) containing `axiom.yaml`, `messages/`, `nodes/`, `gen/`, a `go.mod` whose module path is the package name, and a `.gitignore` that excludes the regenerated `gen/` directory. Go is the CLI's default language, so `--language go` is optional but explicit. `axiom create node` writes `nodes/count_words.go` and `nodes/count_words_test.go`, generates the `axiom/context.go` interface file, and registers the node in `axiom.yaml`. Then implement the handler (next sections), run `axiom test`, and run `axiom dev` to invoke it over HTTP. ## Define the input and output messages Every node has a typed input message and a typed output message, defined as Protocol Buffers in `messages/messages.proto`. `axiom create message` appends a message block there and then runs `axiom generate` automatically to compile Go bindings into `gen/`: ```bash axiom create message TextRequest --fields "text:string" ``` `--fields` is a semicolon-separated list and accepts three forms: canonical proto3 (`"string text = 1"`), proto3 without field numbers (`"string text"` — numbers are auto-assigned), or colon shorthand (`"text:string"`). Without `--fields`, the command writes placeholder fields plus a HINTS comment block to edit in your IDE; after editing, run `axiom generate` to rebuild the bindings. Pass `--no-generate` to skip generation while creating several messages in a row. Comments written directly above a message or field (no blank line between comment and declaration) are extracted at publish time and shown in the Axiom registry as documentation. Messages from other packages can also be used as node inputs and outputs — see [import types from another package](/docs/guides/import-package-types). ## Scaffold the node ```bash axiom create node CountWords --input TextRequest --output WordCountResult --type unary ``` The node name must be PascalCase. If `--input` or `--output` is omitted and you are at an interactive terminal, the command prompts with a numbered list of every available message (local and imported); in non-interactive mode both flags are required. `--type` is `unary` (default, one input message in, one output message out) or `pipeline` (streaming — see the pipeline section below). The command creates: - `nodes/count_words.go` — the handler stub (file name is the snake_case of the node name) - `nodes/count_words_test.go` — a matching test stub - `axiom/context.go` — the generated `axiom.Context` interface (regenerated idempotently; do not edit) - an entry under `nodes:` in `axiom.yaml` The comment directly above the function — no blank line before `func` — is extracted at publish time and becomes the node's description in the Axiom registry. Write it as a plain sentence and replace the generated placeholder before pushing. ## Implement the handler A unary Go node is one function in package `nodes`. Local message types come from the generated `gen` package; platform capabilities arrive through the single `ax axiom.Context` parameter: ```go // nodes/count_words.go package nodes import ( "context" "strings" "demo/text-tools/axiom" gen "demo/text-tools/gen" ) // CountWords splits the input text on whitespace and returns the word count. func CountWords(ctx context.Context, ax axiom.Context, input *gen.TextRequest) (*gen.WordCountResult, error) { words := strings.Fields(input.GetText()) ax.Log().Info("counted words", "count", len(words)) return &gen.WordCountResult{Count: int32(len(words))}, nil } ``` The signature is fixed: `(ctx context.Context, ax axiom.Context, input *gen.) (*gen., error)`. `axiom validate` checks it, so keep the shape the scaffold generated. Through `ax` you get structured logging (`ax.Log().Info(msg, key, value, ...)` — use it instead of `fmt.Println`), secrets (`ax.Secrets().Get("MY_API_KEY")` returns `(value string, ok bool)`; declare the name under the node's `required_secrets` in `axiom.yaml` — see [manage secrets in a flow](/docs/guides/manage-secrets)), and execution identity (`ax.ExecutionID()`, `ax.FlowID()`, `ax.TenantID()`). The full Go surface is documented in the [Go SDK reference](/docs/reference/sdk/go). ## Test the node Edit the generated test so it asserts real output values, then run: ```bash axiom test ``` `axiom test` regenerates proto bindings, runs `axiom validate` (failing fast on errors), then runs `go test ./nodes/...` with output streamed live. The exit code mirrors the test runner's. Pass native `go test` flags after `--`: ```bash axiom test -- -v axiom test -- -run TestCountWords ``` The generated `nodes/count_words_test.go` provides a `newTestContext(t)` helper that satisfies `axiom.Context` for unit tests (populate its `secretsMap` with any secrets the node reads). Replace the `TODO` in the generated test function with assertions on output fields: ```go // nodes/count_words_test.go — replace the generated TestCountWords with: func TestCountWords(t *testing.T) { ctx := context.Background() ax := newTestContext(t) input := &gen.TextRequest{Text: "axiom makes nodes easy"} got, err := nodes.CountWords(ctx, ax, input) if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Count != 4 { t.Errorf("Count = %d, want 4", got.Count) } } ``` Tests are a push gate, not a formality: every node needs at least one test (`axiom validate` warns on a node with no test), and the push build runs `go test ./nodes/...` — a failing test fails the push. Assert output fields meaningfully; do not just check for the absence of an error. ## Run the node locally ```bash axiom dev ``` `axiom dev` generates the same gRPC service the publish pipeline builds, compiles and runs it natively, and starts an HTTP bridge (default port 8083, change with `--port`) that converts JSON to and from Protobuf. Invoke any node with curl: ```bash curl localhost:8083/nodes/CountWords -d '{"text": "axiom makes nodes easy"}' ``` Each node is served at `POST /nodes/`. Unary nodes return a single JSON response; pipeline nodes stream Server-Sent Events, one event per output frame. The bridge also serves `GET /openapi.json` (a spec for all nodes) and `GET /docs` (interactive API docs). The dev server watches `nodes/`, `messages/`, and `axiom.yaml` and recompiles and restarts on change. A failed build leaves the previous service running, so you always have a working server while you fix errors. ## Stream output with a pipeline node Pass `--type pipeline` to scaffold a streaming node: ```bash axiom create node StreamCounts --input TextRequest --output WordCountResult --type pipeline ``` A pipeline node receives input frames on a channel and emits any number of output frames via a callback: ```go // nodes/stream_counts.go — generated shape of a Go pipeline node func StreamCounts(ctx context.Context, ax axiom.Context, in <-chan *gen.TextRequest, emit func(*gen.WordCountResult) error) error { for input := range in { _ = input if err := emit(&gen.WordCountResult{}); err != nil { return err } } return nil } ``` For the entry node of a flow running in pipeline mode, the channel yields exactly one item. See [execution model](/docs/concepts/execution-model) for how unary and pipeline mode differ end to end. ## Push the package ```bash axiom login # once axiom push ``` `axiom push` validates the package locally, then deploys it tenant-private: the nodes become available in your own flows but do not appear in the public marketplace. You can push the same version repeatedly — each re-push refreshes the code, descriptions, and OpenAPI spec (a deployed node's wire signature is frozen; a wire change needs a version bump) — and `axiom push` builds from your git remote, so the current HEAD must be pushed before running it. When you are satisfied, run `axiom publish @` (or use the Axiom UI) to make it a public, immutable release. Next: compose the node into a flow ([build your first flow](/docs/getting-started/first-flow)) and [invoke it via the API](/docs/getting-started/invoke-via-api). --- ## Guides > Create a node in TypeScript {#guides/create-a-node-typescript} > Scaffold, implement, test, and push a TypeScript node with the Axiom CLI, using google-protobuf message classes and AxiomContext. # Create a node in TypeScript A node is a plain TypeScript function: it takes `AxiomContext` and a typed input message, and returns a typed output message. This guide scaffolds a TypeScript package, implements and tests a node, runs it locally with hot reload, and pushes it to the platform. ## Prerequisites - The Axiom CLI installed and logged in (`axiom login`) — see [Installation](../getting-started/installation.md). - Node.js and npm. - `protoc` on your PATH ([installation guide](https://grpc.io/docs/protoc-installation/)), plus the two protoc plugins TypeScript codegen uses: ```bash # protoc-gen-ts comes from ts-protoc-gen; protoc-gen-js emits the JS message runtime npm install -g ts-protoc-gen protoc-gen-js ``` `axiom generate` compiles `messages/*.proto` with `protoc-gen-js` (the `google-protobuf` runtime classes, `gen/messages_pb.js`) and `protoc-gen-ts` (the matching type declarations, `gen/messages_pb.d.ts`). It reports which tool is missing if one is not found. ## Scaffold the package and node Run these commands to go from nothing to a compiling node skeleton: ```bash # from any working directory axiom init my-org/greeter --language typescript cd greeter npm install axiom create message GreetRequest --fields "name:string" axiom create message GreetReply --fields "greeting:string" axiom create node Greet --input GreetRequest --output GreetReply ``` `axiom init` creates a `greeter/` directory (the part of the package name after the last `/`) containing `axiom.yaml`, `messages/`, `nodes/`, `gen/`, a `.gitignore`, and the TypeScript toolchain files: `package.json` (with `google-protobuf`, the `@grpc/*` stack, and `jest`/`ts-jest` dev dependencies), `tsconfig.json`, and `jest.config.js`. `axiom create message` appends a message block to `messages/messages.proto` — all messages live in that single file. The `--fields` flag accepts colon shorthand (`name:string`) or canonical proto3; omit it to get example placeholder fields (plus a syntax-hints comment block) to replace. Comments written directly above a message or field (no blank line between) are extracted at publish time as registry documentation. `axiom create node` requires a PascalCase name and creates `nodes/greet.ts` (the handler, with a camelCase function name `greet`) and `nodes/greet_test.ts` (a jest test with a ready-made mock `AxiomContext`), then records the node in `axiom.yaml`: ```yaml # axiom.yaml (written by init + create node) name: my-org/greeter version: 0.1.0 language: typescript nodes: - name: Greet input: GreetRequest output: GreetReply ``` After the scaffold sequence the generated files are already in place: `axiom create message` runs `axiom generate` automatically (compiling `gen/messages_pb.js` and `gen/messages_pb.d.ts`), and `axiom create node` writes `gen/axiomContext.ts`. Both commands accept `--no-generate` to skip their generate step while batching; run `axiom generate` afterwards to produce all three files at once. For TypeScript packages, `gen/` is committed to git; `.axiom/` (build artifacts), `node_modules/`, and `dist/` are ignored. See [The type system](../concepts/type-system.md) for how messages define your node's contract. ## Implement the handler Edit the generated file. Input and output are `google-protobuf` classes with getter/setter accessors: ```typescript // nodes/greet.ts import { GreetRequest, GreetReply } from '../gen/messages_pb'; import { AxiomContext } from '../gen/axiomContext'; /** * Greets the caller by name. This JSDoc directly above the function is * extracted at publish time and shown in the Axiom registry as the node's * description — edit it before publishing. * * @param ax - Platform context: ax.log for logging, ax.secrets for secrets. */ export function greet(ax: AxiomContext, input: GreetRequest): GreetReply { ax.log.info('greeting', { name: input.getName() }); const out = new GreetReply(); out.setGreeting(`Hello, ${input.getName()}!`); return out; } ``` Handlers may also be `async` — declare the return type as `Promise` and the platform awaits the result. This is required when you await other Promise-based platform APIs. ## Use platform capabilities through AxiomContext Every platform capability arrives through the first parameter, `ax` — node code never calls platform services directly (see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)): - `ax.log.debug/info/warn/error(msg, attrs?)` — structured logging. Use it instead of `console.log()`: in production every line carries the execution ID and trace IDs. - `ax.secrets.get(name)` — returns a `[value, ok]` tuple: ```typescript // nodes/greet.ts — inside the handler const [apiKey, ok] = ax.secrets.get('OPENAI_API_KEY'); if (!ok) { ax.log.warn('OPENAI_API_KEY is not configured'); } ``` Declare each secret a node reads under `required_secrets` in `axiom.yaml` so it is validated at publish time and shown in the marketplace — see [Manage secrets in a flow](manage-secrets.md): ```yaml # axiom.yaml — node entry with a declared secret nodes: - name: Greet input: GreetRequest output: GreetReply required_secrets: - OPENAI_API_KEY ``` - `ax.executionId`, `ax.flowId`, `ax.tenantId` — identifiers for the current invocation. The full interface is documented in the [TypeScript SDK reference](../reference/sdk/typescript.md). ## Test the node `axiom test` runs three steps: `axiom generate`, `axiom validate` (manifest schema, proto compilation, node signatures — fails fast), then jest (preferring the locally installed `node_modules/.bin/jest`). Tests live in `nodes/*_test.ts`; the scaffolded `nodes/greet_test.ts` already defines a mock `testContext` you can pass to the handler — replace its TODO assertion with real ones: ```typescript // nodes/greet_test.ts — replace the generated TODO with real assertions describe('Greet', () => { it('greets by name', () => { const input = new GreetRequest(); input.setName('Ada'); const result = greet(testContext, input); expect(result).toBeInstanceOf(GreetReply); expect(result.getGreeting()).toBe('Hello, Ada!'); }); }); ``` ```bash axiom test # full suite axiom test -- --testNamePattern Greet # pass-through args after -- ``` `axiom validate` warns — without blocking — when a node has no test, and `axiom test` runs the suite with jest. The push build compiles the service but does not run tests, so a green `axiom test` before pushing is the quality gate that matters. Assert output fields meaningfully — not just type-checked. ## Run the node locally `axiom dev` generates the gRPC service, runs it with `npx ts-node`, and starts an HTTP bridge (default port 8083, change with `--port`) that converts JSON to and from Protocol Buffers. It watches `nodes/`, `messages/`, and `axiom.yaml` and recompiles on change; a failed build leaves the previous service running while you fix the error. ```bash axiom dev ``` Then, in another terminal: ```bash # invoke the node over the HTTP bridge curl localhost:8083/nodes/Greet -d '{"name":"Ada"}' # → {"greeting":"Hello, Ada!"} ``` The bridge also serves `GET /openapi.json` (importable into Postman, Insomnia, or Bruno) and an interactive try-it page at `GET /docs`. ## Push to the platform ```bash axiom push ``` `axiom push` validates the package locally, then pushes it. A pushed package is visible only to your own tenant — it does not appear in the public marketplace, and you can push the same version repeatedly while iterating — each re-push refreshes the code, descriptions, and OpenAPI spec (a deployed node's wire signature is frozen; a wire change requires a version bump). It requires a prior `axiom login`, and your current git HEAD must be pushed to the remote (the platform builds from it). When you are satisfied, run `axiom publish @` (or use the Axiom UI) to make an immutable public release. Next: place the node in a flow — see [Your first flow](../getting-started/first-flow.md). ## Streaming nodes (pipeline) For streaming, scaffold a pipeline node with `--type pipeline`: ```bash axiom create node Tokenize --input GreetRequest --output GreetReply --type pipeline ``` The handler is an async generator: it consumes an `AsyncIterable` of input frames and yields output frames. When a pipeline node is the entry node of a flow, the iterable yields exactly one item. ```typescript // nodes/tokenize.ts import { GreetRequest, GreetReply } from '../gen/messages_pb'; import { AxiomContext } from '../gen/axiomContext'; /** * Emits one greeting frame per input frame. */ export async function* tokenize( ax: AxiomContext, inputs: AsyncIterable, ): AsyncGenerator { for await (const input of inputs) { const out = new GreetReply(); out.setGreeting(`Hello, ${input.getName()}!`); yield out; } } ``` Flows containing pipeline nodes run in pipeline mode — see [Execution model](../concepts/execution-model.md). --- ## Guides > Create a node in Rust {#guides/create-a-node-rust} > Scaffold a Rust node package, implement the handler against the AxiomContext trait, run cargo-based tests with axiom test, and push it to the platform. # Create a node in Rust This guide takes you from an empty directory to a pushed Rust node: scaffold a package, define messages, implement the handler, test it, and push. If you have never built a node in any language, start with [your first node](../getting-started/first-node.md); this page covers the Rust-specific workflow. ## Prerequisites - The Axiom CLI installed and logged in (`axiom login`) — see [installation](../getting-started/installation.md). - A Rust toolchain with `cargo` on your PATH. If you don't have one: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`. - For `axiom push`: the package directory must be a git repository whose current HEAD is pushed to its remote. You do **not** need `protoc` or any protoc plugin for Rust — proto compilation happens inside the build via `tonic-build` (see [How Rust differs from other languages](#how-rust-differs-from-other-languages)). ## Create a Rust node in six commands ```bash # from any directory — scaffolds ./order-tools/ # the scope before the "/" must be your Axiom handle — push rejects any other scope axiom init your-handle/order-tools --language rust cd order-tools axiom create message OrderRequest --fields "order_id:string; total:double" axiom create message OrderConfirmation --fields "order_id:string; confirmed:bool" axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation axiom test ``` Then implement the handler in `nodes/process_order.rs` (next section), make the generated test assert real output fields, and push: ```bash # from the package root axiom push ``` What each step did: - `axiom init your-handle/order-tools --language rust` created an `order-tools/` subdirectory (the part of the package name after the last `/`) with `axiom.yaml`, the standard layout (`messages/`, `nodes/`, and an empty `gen/`, which Rust leaves unused because codegen happens at build time), a `.gitignore` (ignoring `.axiom/`, `gen/`, and `target/`), and a `Cargo.toml`. Package names must be scoped as `your-handle/package-name`, where the scope matches your Axiom account handle. The crate is always named `service` — the Axiom package name lives in `axiom.yaml`. The manifest pins `tonic`, `prost`, `tokio`, `tokio-stream`, and `serde_json` as dependencies and `tonic-build` as a build dependency. - `axiom create message` appended a message to `messages/messages.proto` — the single file where all of the package's messages live. The `--fields` flag accepts colon shorthand (`name:string`), proto3 syntax with or without field numbers, and the scalar types `string`, `int32`, `int64`, `uint32`, `uint64`, `float`, `double`, `bool`, `bytes`. - `axiom create node ProcessOrder` created `nodes/process_order.rs` and `nodes/process_order_test.rs` (the node name must be PascalCase; the Rust function name is its snake_case form), and registered the node in `axiom.yaml`. Omit `--input`/`--output` to pick messages interactively. - `axiom test` validated the package and ran `cargo test`. - `axiom push` validated the package and pushed it tenant-private to the platform, ready to place in a [flow](../getting-started/first-flow.md). ## Implement the node handler A Rust node is a plain public function in `nodes/.rs`. It receives the `AxiomContext` trait object and a typed input message, and returns a `Result` with the typed output message: ```rust // nodes/process_order.rs use crate::axiom_context::AxiomContext; use crate::gen::messages::{OrderRequest, OrderConfirmation}; use std::collections::HashMap; /// Confirms an incoming order. This doc comment documents the handler for /// anyone reading the node's source, which the registry captures at push time. pub fn process_order( ax: &dyn AxiomContext, input: OrderRequest, ) -> Result> { ax.log().info( "processing order", &HashMap::from([("order_id", input.order_id.clone())]), ); Ok(OrderConfirmation { order_id: input.order_id, confirmed: true, }) } ``` Key points: - **Messages are prost structs.** Each protobuf message in `messages/messages.proto` becomes a Rust struct under `crate::gen::messages` with snake_case fields and a `Default` impl. Messages from imported packages are re-exported into the same `gen::messages` module — see [import package types](./import-package-types.md) and the [type system](../concepts/type-system.md). - **The signature is enforced.** `axiom validate` (run automatically by `axiom test`, `axiom build`, and the publish pipeline) checks that each node declared in `axiom.yaml` has a function of exactly this shape: `pub fn (ax: &dyn AxiomContext, input: ) -> Result<, ...>`. - **The registry description comes from `axiom.yaml`.** The `description` field on the node's entry in `axiom.yaml` (set with `--description` on `axiom create node`, or edited any time before pushing) is what the Axiom registry displays as the node's description. The node's source file — including its doc comment — is captured at push time and shown in the registry's source view, so edit the scaffolded placeholder too. ## Use platform capabilities through AxiomContext `AxiomContext` is how node code reaches Axiom's platform capabilities — logging, secrets, and the rest arrive through the context, never by calling platform services directly (see [sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)). The trait exposes: - `ax.log()` — structured logging: `debug`, `info`, `warn`, `error`, each taking a message and a `&HashMap<&str, String>` of attributes. - `ax.secrets()` — read-only tenant secrets: `ax.secrets().get(name)` returns `(String, bool)` — the value and whether the secret exists. - `ax.execution_id()`, `ax.flow_id()`, `ax.tenant_id()` — identifiers for the current invocation. - `ax.reflection()` and `ax.mutation()` — read and append-only-extend the running flow's graph (advanced agent features). Reading a secret: ```rust // nodes/process_order.rs — inside the handler body let (api_key, ok) = ax.secrets().get("STRIPE_API_KEY"); if !ok { return Err("secret STRIPE_API_KEY is not registered".into()); } ``` Declare every secret the node reads in `axiom.yaml` so it is recorded with the node and shown in the marketplace — users then know which secrets to register before running a flow that contains the node: ```yaml # axiom.yaml nodes: - name: ProcessOrder input: OrderRequest output: OrderConfirmation required_secrets: - STRIPE_API_KEY ``` See [manage secrets in a flow](./manage-secrets.md) for registering the secret value itself. ## Test the node with axiom test `axiom create node` scaffolds `nodes/_test.rs` with a mock `AxiomContext` (`test_context()`) you edit to drive scenarios. Replace the generated TODO with assertions on real output fields: ```rust // nodes/process_order_test.rs — inside the generated #[cfg(test)] module #[test] fn test_process_order() { let ax = test_context(); let input = OrderRequest { order_id: "ord-1".into(), total: 12.5 }; let out = process_order(&ax, input).unwrap(); assert_eq!(out.order_id, "ord-1"); assert!(out.confirmed); } ``` Run the suite from the package root: ```bash # validates the package, then runs cargo test in the assembled build context axiom test # pass filters through to the cargo test binary after -- axiom test -- test_process_order ``` Do not run `cargo test` directly in the package root — it will fail to compile. Rust node tests are `#[cfg(test)]` modules of the generated service crate and reference `crate::` paths that only exist once the build context is assembled. `axiom test` assembles that build context and runs `cargo test` inside it. `axiom validate` (run by `axiom test`, `axiom build`, and `axiom push`) warns when a node has no test file or the test file defines no test. For Rust the push build compiles your crate but does not execute tests, so a green `axiom test` run before pushing is the quality gate that matters. Make the assertions meaningful: a test that only `Ok`-checks the result passes against any implementation. ## Write a streaming pipeline node Pass `--type pipeline` to scaffold a streaming node instead of a unary one: ```bash axiom create node StreamOrders --input OrderRequest --output OrderConfirmation --type pipeline ``` A pipeline node receives an iterator of input frames and returns an iterator of results (for the start node of a pipeline flow, `inputs` yields exactly one item): ```rust // nodes/stream_orders.rs use crate::axiom_context::AxiomContext; use crate::gen::messages::{OrderRequest, OrderConfirmation}; /// Emits one confirmation per incoming order frame. pub fn stream_orders( ax: &dyn AxiomContext, inputs: I, ) -> impl Iterator>> where I: Iterator, { let _ = ax; inputs.map(|input| { Ok(OrderConfirmation { order_id: input.order_id, confirmed: true, }) }) } ``` See the [execution model](../concepts/execution-model.md) for how pipeline mode differs from unary execution. ## How Rust differs from other languages Three Rust-specific behaviors to know, compared with the [Python](./create-a-node-python.md), [Go](./create-a-node-go.md), and [TypeScript](./create-a-node-typescript.md) workflows: - **No `axiom generate` step.** Rust defers all proto codegen to the build tool: a generated `build.rs` compiles `messages/*.proto` (plus any imported packages and the platform's node service proto) with `tonic-build` at `cargo build`. Running `axiom generate` in a Rust package prints a note and does nothing. No `protoc` or protoc plugin is required. - **`axiom dev` supports Rust** via a rebuild-on-save-restart loop: each saved change runs `cargo build` and restarts the service (compiled languages recompile rather than hot-swap, so the first build is slower while the crate graph warms up; a failed build leaves the previous server running). The HTTP bridge (default `:8083`) converts JSON to and from Protobuf so you can `curl` your nodes — the same fast loop Go/Python/TypeScript get. You can still use `axiom build` to produce a local Docker image identical to the publish pipeline (all artifacts are written under `.axiom/` and are inspectable). - **The crate compiles only in the assembled build context.** The package root has no `service.rs`, `build.rs`, or generated bindings — those are injected when `axiom test` or `axiom build` assembles the context under `.axiom/`. Use `axiom test` rather than bare `cargo` commands. ## Push and use the node ```bash # from the package root, with HEAD pushed to your git remote axiom push ``` `axiom push` validates the package locally, then pushes it to the platform **tenant-private**: it is visible only to your own tenant and does not appear in the public marketplace. You can push the same version repeatedly — each re-push refreshes the code, descriptions, and OpenAPI spec — so you can iterate on deployed code. (A deployed node's input/output wire signature is frozen; a wire change is rejected until you bump the version.) When you are satisfied, run `axiom publish @` (or use the Axiom UI) to make it an immutable public release. Next steps: - Place the node in a flow on the canvas — [build your first flow](../getting-started/first-flow.md). - Call the flow over HTTP — [invoke via API](../getting-started/invoke-via-api.md). - Full command details — [axiom init](../reference/cli/axiom-init.md), [axiom create node](../reference/cli/axiom-create-node.md), [axiom test](../reference/cli/axiom-test.md), [axiom build](../reference/cli/axiom-build.md), [axiom push](../reference/cli/axiom-push.md). --- ## Guides > Create a node in Java {#guides/create-a-node-java} > Scaffold a Java package and node with the Axiom CLI, implement the handler against AxiomContext, test it with Maven, and push it to the platform. # Create a node in Java A Java node is a single `public static` method on a class under `nodes/`: it takes an `AxiomContext` and a typed input message, and returns a typed output message. This guide takes you from an empty directory to a pushed package — scaffold, implement, test, push. ## Prerequisites - The Axiom CLI installed and logged in (`axiom login`) — see [Installation](../getting-started/installation.md). - **JDK 21 and Maven (`mvn`)** on your PATH. The generated `pom.xml` targets Java 21 (`maven.compiler.release` 21), and `axiom test` runs `mvn test`. - **Docker** — only needed for `axiom build` (optional local image build). `axiom test` and `axiom push` do not require it. - A **git remote**: `axiom push` requires the current HEAD to be pushed to the remote. ## Create the package and node The shortest path — four commands: ```bash # from any working directory axiom init my-scope/order-tools --language java cd order-tools axiom create message OrderRequest --fields "order_id:string; amount:double" axiom create message OrderConfirmation --fields "order_id:string; approved:bool" axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation ``` What each step does: - `axiom init` creates `./order-tools/` (the part after the last `/`) with `axiom.yaml`, `messages/messages.proto`, a generated Maven `pom.xml` (marked "DO NOT EDIT" — it wires protobuf code generation and the executable jar build), and a `.gitignore`. - `axiom create message` appends a message block to `messages/messages.proto` — all messages live in that single file so they can reference each other. Comments written directly above a message or field are extracted at publish time and shown in the registry as documentation. Without `--fields`, the block is scaffolded with placeholder fields and syntax hints for you to edit. - `axiom create node` (the name must be PascalCase) writes `nodes/process_order.java` and `nodes/process_order_test.java`, generates the `AxiomContext` interface at `src/main/java/axiom/AxiomContext.java`, and registers the node in `axiom.yaml`. If `--input` or `--output` are omitted in a terminal, it prompts with a numbered list of available messages. In a terminal it also asks for the node type — press Enter to accept the default (`unary`), or pass `--type` to skip the prompt. **Keep the snake_case filenames.** `axiom validate` and the build pipeline look up each node at `nodes/.java`; at build time the file is relocated and renamed automatically to the Maven-conventional `src/main/java/nodes/ProcessOrder.java`, so the working-tree name never violates Java's class-per-file convention in the compiled layout. ## Implement the handler The validator requires this exact signature shape — a `public static` method whose name is the camelCase form of the node name: ```java public static OrderConfirmation processOrder(AxiomContext ax, OrderRequest input) ``` Local message types are nested under the generated `gen.Messages` outer class, so each is imported by its fully qualified name and used by its simple name. A complete handler: ```java // nodes/process_order.java package nodes; import axiom.AxiomContext; import gen.Messages.OrderRequest; import gen.Messages.OrderConfirmation; import java.util.Map; public class ProcessOrder { /** * Approves orders under 1000.00 and echoes the order id. This Javadoc is * extracted at publish time and shown in the Axiom registry as the * node's documentation — write the real description here. * * @param ax The AxiomContext: logging, secrets, reflection, mutation. * @param input The decoded OrderRequest for this invocation. */ public static OrderConfirmation processOrder(AxiomContext ax, OrderRequest input) { ax.log().info("processOrder handling", Map.of("order_id", input.getOrderId())); boolean approved = input.getAmount() < 1000.0; return OrderConfirmation.newBuilder() .setOrderId(input.getOrderId()) .setApproved(approved) .build(); } } ``` Messages are standard protobuf-java classes: read fields with getters (`input.getOrderId()`), build outputs with `newBuilder()` … `build()`. See [Type system](../concepts/type-system.md) for how messages define the contract between nodes. ## Reach the platform through AxiomContext `AxiomContext` (the `ax` parameter) is the only way node code reaches the platform — everything goes through the sidecar, never a direct call. The interface lives in your package at `src/main/java/axiom/AxiomContext.java`: - `ax.log()` — structured logging: `ax.log().info("msg", Map.of("k", "v"))`, plus `debug`, `warn`, `error`, each with a single-argument overload. - `ax.secrets().get("NAME")` — returns `Optional`; tenant-scoped secrets resolved by the platform. Declare the names your node needs under `required_secrets` in `axiom.yaml` — see [Manage secrets in a flow](manage-secrets.md). - `ax.executionId()`, `ax.flowId()`, `ax.tenantId()` — identifiers for the current execution. - `ax.reflection().flow()` — read-only view of the running flow graph (`nodes()`, `edges()`, `loopEdges()`, `position()`, `graphId()`). - `ax.mutation().flow()` — buffered, additive mutation of the running flow graph (`addNode`, `addEdge`). Why the indirection: node code is sandboxed and the sidecar is the trust boundary — see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md). ## Java has no axiom generate step For Java packages, protobuf code generation is deferred to the build tool: the protobuf-maven-plugin declared in the generated `pom.xml` compiles `messages/messages.proto` (plus the platform's node service proto) during every `mvn` build. Running `axiom generate` in a Java package prints a notice and generates nothing — there is no `gen/` bindings directory to commit. Two naming rules follow from this: - **Local messages** compile with `option java_package = "gen"` and `option java_outer_classname = "Messages"`, so a local message `Foo` is the Java type `gen.Messages.Foo`. - **Imported messages** (types from another package, added with `axiom import`) compile under `imports.`, so an imported message `Bar` from package `acme/billing` is `imports.acme.billing.Bar`. `axiom create node` writes the correct import lines for whichever source each message comes from — see [Import package types](import-package-types.md). ## Add a third-party library To call a third-party library from a Java node, declare its Maven coordinate in `axiom.yaml` under `build.maven_deps` — a list of exact-pinned `groupId:artifactId:version` entries: ```yaml build: maven_deps: - org.dmfs:lib-recur:0.17.1 ``` Each coordinate is rendered into the `` block of the generated `pom.xml`, so the library resolves from Maven Central during `axiom test`, `axiom build`, `axiom push`, and the server-side registry build alike. Then just `import` and use it in your handler. Do **not** edit `pom.xml` by hand — it is regenerated on every build and your edits are lost; `build.maven_deps` is the supported way to add dependencies. The version must be an exact pin (`axiom validate` rejects ranges, `+`, `LATEST`/`RELEASE`, and `-SNAPSHOT`) so builds are reproducible. Classifiers, exclusions, BOM imports, and custom repositories are not supported yet. See the [`build.maven_deps` reference](../reference/axiom-yaml.md#build). ## Share code between nodes (helper classes) When several nodes need the same logic, factor it into a **helper class** rather than duplicating it. Put the `.java` file under `nodes/` alongside your node files, in package `nodes`: ```java // nodes/OfficeUtil.java package nodes; import org.apache.poi.ss.usermodel.WorkbookFactory; public class OfficeUtil { public static byte[] normalize(byte[] raw) { /* shared logic */ } } ``` Any node in package `nodes` then calls it directly — `OfficeUtil.normalize(...)` — with no import (same package). A helper may use `build.maven_deps` libraries just like a node handler. Two rules, both standard Java: - **The filename must match the public class name** (`OfficeUtil` → `OfficeUtil.java`). Unlike node files — which you author snake_case (`process_order.java`) and the build renames to their PascalCase class — a helper is copied verbatim into `src/main/java/nodes/`, so its name must already be correct. - **A helper is any `.java` under `nodes/` that is not a registered node.** You do **not** list it in `axiom.yaml` (only nodes go there). The build compiles every `.java` under `nodes/`; a `*_test.java` file routes to the test source set and never ships in the deployed image. ## Test the node ```bash axiom test ``` `axiom test` validates the package, then assembles the full Maven build context (the same `src/main` + `src/test` layout `axiom build` compiles, with proto codegen wired into the pom) and runs `mvn test` inside it. Java node tests cannot run in the bare package root — they compile against the generated service, `AxiomContext`, and proto bindings that only exist in the assembled context. The scaffolded `nodes/process_order_test.java` contains a `TestContext` class — a no-op `AxiomContext` implementation you can edit to drive a specific scenario (its secrets return empty, reflection exposes an empty graph). Replace the generated `testProcessOrder` method with real assertions, keeping the generated imports and the `TestContext` class: ```java // nodes/process_order_test.java — replaces the generated testProcessOrder method @Test public void testProcessOrder() { AxiomContext ax = new TestContext(); OrderRequest input = OrderRequest.newBuilder() .setOrderId("ord-1") .setAmount(250.0) .build(); OrderConfirmation result = ProcessOrder.processOrder(ax, input); assertEquals("ord-1", result.getOrderId()); assertTrue(result.getApproved()); } ``` `axiom validate` warns — without blocking — when a node has no test, and `axiom test` runs the suite with Maven. The push build does not execute tests, so a green `axiom test` before pushing is the quality gate that matters. Assert output fields meaningfully — not just null-checked. ## Build and push ```bash axiom validate # axiom.yaml schema, proto definitions, node signatures, node tests axiom build # optional: local Docker image, identical to the publish pipeline axiom push # deploy to the platform, tenant-private ``` - `axiom validate` checks `axiom.yaml`, compiles every proto in `messages/`, and verifies each node's function signature matches the expected shape. It runs automatically inside `axiom build` and the publish pipeline. - `axiom build` produces a local Docker image: the Java build compiles the package and packages it as an executable fat jar that runs on a JRE base image. All artifacts land in `.axiom/` for inspection. - `axiom push` validates, then pushes the package to the platform. Commit and push your changes first — the platform builds from the git remote, not your working tree. A pushed package is visible only to your own tenant, and you can push the same version repeatedly while iterating — each re-push refreshes the code, descriptions, and OpenAPI spec (a deployed node's wire signature is frozen; a wire change requires a version bump). Run `axiom publish @` (or use the Axiom UI) when you want an immutable public release. Note: `axiom dev` supports Java via a rebuild-on-save-restart loop — each saved change runs `mvn package` and restarts the service (compiled languages recompile rather than hot-swap, so the first build is slower while Maven warms up; a failed build leaves the previous server running). The HTTP bridge (default `:8083`) lets you `curl` your nodes with JSON. `axiom test` and `axiom build` remain available. Next: place the node in a flow ([Build your first flow](../getting-started/first-flow.md)) and invoke it ([Invoke a flow via the API](../getting-started/invoke-via-api.md)). ## Streaming pipeline nodes Pass `--type pipeline` to scaffold a streaming node instead of a unary one: ```bash axiom create node TransformFrames --input OrderRequest --output OrderConfirmation --type pipeline ``` A pipeline node consumes an iterator of input frames and returns a stream of output frames: ```java public static Stream transformFrames(AxiomContext ax, Iterator inputs) ``` When a pipeline node is the entry node of a flow, the iterator yields exactly one item. See [Execution model](../concepts/execution-model.md) for how pipeline mode changes flow execution. --- ## Guides > Create a node in C# {#guides/create-a-node-csharp} > Scaffold a C# package, implement a unary or pipeline node against IAxiomContext, and run xUnit tests with axiom test — codegen happens at dotnet build via Grpc.Tools. # Create a node in C# A node is the single unit of compute in Axiom: a plain function with a typed input message and a typed output message. This guide creates a C# package, implements a unary node and a pipeline node against `IAxiomContext`, and runs the tests — everything you need before publishing with `axiom push`. New to Axiom? Do [Write your first node](/docs/getting-started/first-node) first — it walks the same loop step by step (in Python). This guide covers what is specific to C#. ## Prerequisites - The Axiom CLI installed and on your PATH — see [Install the Axiom CLI](/docs/getting-started/installation). - The .NET SDK 8.0 or newer (`dotnet` on your PATH) — the generated projects target `net8.0`. Install from . - Docker, only if you run `axiom build` locally. No protoc and no protobuf plugins are needed: C# proto code generation runs inside `dotnet build` via the Grpc.Tools MSBuild package, which the generated project files already reference. ## The fast path ```bash axiom init my-org/greeter --language csharp cd greeter axiom create message GreetRequest --fields "name:string" axiom create message GreetReply --fields "greeting:string" axiom create node Greet --input GreetRequest --output GreetReply --type unary axiom test ``` After these commands you have a package with one node, a generated xUnit test file, and a passing test run. The sections below explain each artifact and how to implement the node body. ## Create a C# package ```bash axiom init my-org/greeter --language csharp cd greeter ``` `axiom init` creates `./greeter/` (the part of the package name after the last `/`) containing: - `axiom.yaml` — the package manifest. See [axiom.yaml reference](/docs/reference/axiom-yaml). - `messages/messages.proto` — the single file where all of the package's message types are defined. For C# it carries `option csharp_namespace = "Gen";`, so every message compiles into the `Gen` namespace. - `nodes/` — node implementations and their tests. - `service.csproj` — the deployable service project (generated; do not edit). It wires Grpc.Tools so `dotnet build` compiles the `.proto` files; there is no separate codegen step. - `tests/Tests.csproj` — the test project `axiom test` runs with `dotnet test`. It compiles the node sources, the `Axiom/` context interface, and the message protos directly, and references xUnit. - `.gitignore` — excludes `.axiom/`, `gen/`, `bin/`, and `obj/`. Because codegen is deferred to `dotnet build`, `axiom generate` is a no-op for C# — it prints `C# compiles protobufs at build time via Grpc.Tools — nothing to generate.` and exits successfully. ## Define the input and output messages Every node input and output is a message defined in `messages/messages.proto`. `axiom create message` appends a message block to that file: ```bash # Run inside the package directory (where axiom.yaml is). axiom create message GreetRequest --fields "name:string" axiom create message GreetReply --fields "greeting:string" ``` `--fields` is a semicolon-separated list and accepts canonical proto3 (`string name = 1`), proto3 without field numbers (`string name`), or the colon shorthand above. Field numbers are auto-assigned when omitted. Without `--fields` you get placeholder fields plus a HINTS comment block to edit by hand. Two C#-specific points: - Proto field names are `snake_case` in the `.proto` file; the generated C# properties are PascalCase (`name` becomes `input.Name`). - The generated types land in the `Gen` namespace — node files reference them with `using Gen;`. Comments written directly above a message or field (no blank line in between) are extracted at publish time and shown in the Axiom registry as documentation. For how messages type-check across a flow, see [the type system](/docs/concepts/type-system). ## Scaffold the node ```bash # Run inside the package directory (where axiom.yaml is). axiom create node Greet --input GreetRequest --output GreetReply --type unary ``` This creates `nodes/greet.cs` and `nodes/greet_test.cs` (file names are the snake_case of the node name), appends the node entry to `axiom.yaml`, and writes `Axiom/Context.cs` — the generated `IAxiomContext` interface (marked `DO NOT EDIT`; it is regenerated idempotently). Rules and behavior: - The node name must be PascalCase (letters and digits only). For C# the function name is the node name itself. - `--input` and `--output` must name messages defined in `messages/messages.proto` or available from an imported package — see [import package types](/docs/guides/import-package-types). Imported messages get a `using imports.;` line instead of `using Gen;`, where the namespace is the source package name with `/` becoming `.` and `-` becoming `_` (e.g. `using imports.axiom_official.axiom_text_ops;`). - At an interactive terminal, omitted flags trigger prompts: a numbered list of available messages, and a node type prompt defaulting to `unary`. In non-interactive runs `--input` and `--output` are required and `--type` defaults to `unary`. - `--type` is `unary` (one input in, one output out) or `pipeline` (streaming) — see [the execution model](/docs/concepts/execution-model). ## Implement the node Replace the generated stub in `nodes/greet.cs` with a working body. A unary node is a static method on a static class in the `Nodes` namespace, and its signature must match exactly — `axiom validate` checks it: ```csharp // nodes/greet.cs using Axiom; using Gen; using System.Collections.Generic; namespace Nodes; public static class GreetNode { /// /// Returns a personalized greeting for the caller's name. /// public static GreetReply Greet(IAxiomContext ax, GreetRequest input) { ax.Log().Info("greeting requested", new Dictionary { ["name"] = input.Name }); return new GreetReply { Greeting = $"Hello, {input.Name}!" }; } } ``` The XML doc comment (`///`) directly above the method is extracted at publish time and shown in the Axiom registry as the node's documentation — write a real description before publishing. ## Use platform capabilities through IAxiomContext `ax` is the single injection point for every platform capability — node code never calls platform services directly (the sidecar mediates everything; see [sandboxing and tenancy](/docs/concepts/sandboxing-and-tenancy)). The interface lives in the generated `Axiom/Context.cs`: - **Logging** — `ax.Log()` returns a structured logger with `Debug`, `Info`, `Warn`, and `Error`, each taking a message and an optional `IDictionary` of attributes. The logger type is nested as `IAxiomContext.ILogger`, so it never collides with `Microsoft.Extensions.Logging.ILogger`. - **Secrets** — `ax.Secrets().Get("MY_API_KEY")` returns a `(string Value, bool Found)` tuple: ```csharp // Inside a node method body. required_secrets is informational: an // unregistered secret yields found=false here at read time, not an error. var (apiKey, found) = ax.Secrets().Get("MY_API_KEY"); if (!found) { ax.Log().Warn("MY_API_KEY not configured"); } ``` List each secret name under the node's `required_secrets` in `axiom.yaml`; `axiom validate` warns when a `Secrets().Get` call references a name not listed there. See [manage secrets](/docs/guides/manage-secrets). - **Identity** — `ax.ExecutionId()`, `ax.FlowId()`, and `ax.TenantId()` return the current execution context. - **Reflection and mutation** — `ax.Reflection().Flow()` is a read-only view of the running flow graph (`Nodes()`, `Edges()`, `Position()`); `ax.Mutation().Flow()` buffers additive graph changes (`AddNode`, `AddEdge`). ## Write a pipeline node A pipeline node streams: it consumes an async stream of input frames and yields output frames. Scaffold one with `--type pipeline`; the required signature is an async iterator: ```csharp // nodes/stream_greetings.cs using Axiom; using Gen; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; namespace Nodes; public static class StreamGreetingsNode { /// /// Emits one greeting per incoming request frame. /// public static async IAsyncEnumerable StreamGreetings( IAxiomContext ax, IAsyncEnumerable inputs, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var input in inputs.WithCancellation(cancellationToken)) { yield return new GreetReply { Greeting = $"Hello, {input.Name}!" }; } } } ``` For the entry node of a pipeline flow, the stream yields exactly one item. `axiom create node --type pipeline` generates a pipeline-shaped test file alongside the implementation. See [the execution model](/docs/concepts/execution-model) for when to choose pipeline over unary. ## Add a third-party library To call a third-party library from a C# node, declare its NuGet coordinate in `axiom.yaml` under `build.nuget_deps` — a list of exact-pinned `PackageID:Version` entries: ```yaml build: nuget_deps: - Cronos:0.8.4 ``` Each coordinate is rendered as a `` into the generated `service.csproj` (and the `tests/Tests.csproj` that `axiom test` runs), so the library restores from NuGet.org during `axiom test`, `axiom build`, `axiom push`, and the server-side registry build alike. Then just add a `using` and call it in your handler. Do **not** edit `service.csproj` by hand — it is regenerated on every build and your edits are lost; `build.nuget_deps` is the supported way to add dependencies. The version must be an exact pin (`axiom validate` rejects floating versions like `6.0.*` and ranges like `[1.0,2.0)`) so builds are reproducible. Custom NuGet feeds and private sources are not supported yet. See the [`build.nuget_deps` reference](../reference/axiom-yaml.md#build). ## Share code between nodes (helper classes) When several nodes need the same logic, factor it into a **helper class** rather than duplicating it. Put the `.cs` file under `nodes/` alongside your node files: ```csharp // nodes/OfficeUtil.cs namespace nodes; public static class OfficeUtil { public static byte[] Normalize(byte[] raw) { /* shared logic */ } } ``` Any node then calls it — `OfficeUtil.Normalize(...)`. The build compiles every non-test `.cs` under `nodes/`, so a helper needs **no** entry in `axiom.yaml` (only nodes go there) and its filename need not match the class name. A helper may use `build.nuget_deps` libraries just like a node handler. A `*_test.cs` file is compiled only by `axiom test` and never ships in the deployed image. ## Run the tests `axiom test` validates the package, then runs `dotnet test tests/Tests.csproj` in place, streaming output live; the exit code mirrors the test runner's. (The generate step it runs first is a no-op for C#.) `axiom create node` generated `nodes/greet_test.cs` containing a `TestContext` — a no-op `IAxiomContext` you can edit to drive a scenario — and one xUnit `[Fact]`. Replace the generated test body so it asserts real output values: ```csharp // nodes/greet_test.cs — replace the body of the generated [Fact] method [Fact] public void TestGreet() { IAxiomContext ax = new TestContext(); var input = new GreetRequest { Name = "Ada" }; var result = GreetNode.Greet(ax, input); Assert.Equal("Hello, Ada!", result.Greeting); } ``` ```bash axiom test ``` Arguments after `--` go straight to `dotnet test`, e.g. `axiom test -- --filter TestGreet`. `axiom validate` warns — without blocking — when a node has no test, and `axiom test` runs the suite with `dotnet test`. The push build compiles only the service project — tests never enter the deploy image — so a green `axiom test` before pushing is the quality gate that matters. Assert output fields meaningfully — not just null-checked. ## Build and publish `axiom dev` supports C# via a rebuild-on-save-restart loop: each saved change runs `dotnet build` and restarts the service (compiled languages recompile rather than hot-swap, so the first build is slower while the SDK warms up; a failed build leaves the previous server running). The HTTP bridge (default `:8083`) converts JSON to and from Protobuf so you can `curl` your nodes. Use `axiom build` to verify the deployable artifact. `axiom build` produces the same Docker image the publish pipeline builds: it validates the package, generates the service artifacts, and runs `docker build`. The resulting image runs the service on the ASP.NET 8 runtime; the test project is never included, so no test framework ships in the image. ```bash axiom build ``` When you are ready to deploy: ```bash axiom push ``` `axiom push` validates locally, then pushes the package to the Axiom platform, visible only to your own tenant. It requires a prior `axiom login`, and the current git HEAD must be pushed to the remote — the platform builds from the repository, not your working tree. Pushing the same version again refreshes the code, descriptions, and OpenAPI spec (a deployed node's wire signature is frozen; a wire change requires a version bump). When you are satisfied, run `axiom publish @` (or use the Axiom UI) to make an immutable public release. See [push the package and build your first flow](/docs/getting-started/first-flow). ## Next steps - [Push the package and build your first flow](/docs/getting-started/first-flow). - [Invoke a flow via the API](/docs/getting-started/invoke-via-api). - [Import package types](/docs/guides/import-package-types) — use messages from other packages as node inputs and outputs. - [Manage secrets in a flow](/docs/guides/manage-secrets). - Writing in another language? See the guides for [Python](/docs/guides/create-a-node-python), [Go](/docs/guides/create-a-node-go), [TypeScript](/docs/guides/create-a-node-typescript), [Rust](/docs/guides/create-a-node-rust), and [Java](/docs/guides/create-a-node-java). --- ## Guides > Import message types from another package {#guides/import-package-types} > Find a published package with axiom search and axiom info, pull its message types into your package with axiom import, and use them as node inputs and outputs. # Import message types from another package Every node input and output is a message — a Protocol Buffers type. Messages do not have to be defined in your own package: `axiom import` downloads the `.proto` definitions from any published package so your nodes can consume or produce its types. This is how packages — even in different languages — share a contract: a flow can wire another package's node output straight into your node's input because both sides reference the same message type. See [the type system](/docs/concepts/type-system) for how edges type-check. ## Prerequisites - An Axiom package directory (one containing `axiom.yaml`) — create one with `axiom init`, see [write your first node](/docs/getting-started/first-node). - Logged in via `axiom login`. `axiom import` requires it; `axiom search` and `axiom info` work without authentication. - For Go, Python, and TypeScript packages, `axiom import` finishes by running `axiom generate`, which uses the same proto tooling as the rest of the local loop (`protoc` + `protoc-gen-go` for Go; `grpcio-tools` or `protoc` for Python; `protoc` + `protoc-gen-js` + `protoc-gen-ts` for TypeScript). Rust, Java, and C# packages need no extra tooling — their protos compile during the language build. ## The fast path ```bash # Run inside your package directory (where axiom.yaml is). axiom search --type messages TokensResult # which package defines the type? axiom info axiom-official/axiom-text-ops # inspect its nodes and messages axiom import axiom-official/axiom-text-ops # download protos, update axiom.yaml, generate axiom create node Analyze --input TokensResult --output AnalysisReport ``` After `axiom import`, the imported message types appear in `axiom create node`'s message list and work as `--input`/`--output` exactly like messages defined in your own `messages/messages.proto`. ## Search the marketplace `axiom search` queries the marketplace; no login is needed. ```bash axiom search # list recently published packages axiom search text-ops # packages matching "text-ops" axiom search --type nodes "tokenize" # search nodes instead axiom search --type messages TokensResult # search messages instead ``` `--type` (shorthand `-t`) selects what to search: `packages` (the default), `nodes`, or `messages`. Package results list name, version, language, node count, and author. Node and message results include the package and version that publish them — so `--type messages` answers "which package do I import to get this type?" ## Inspect a package before importing `axiom info` shows the full details of a published package; no login is needed. ```bash axiom info axiom-official/axiom-text-ops # most recently published version axiom info axiom-official/axiom-text-ops@0.1.0 # a specific version ``` When `@version` is omitted, the most recently published version is shown. The output includes the description, author, license, and deploy status; the package's live endpoint URL; links to its `openapi.json` and interactive API docs (see [use the interactive API docs](/docs/guides/use-interactive-api-docs)); every node with its input → output message types; and every message the package defines — the names you can use after importing. A package can be *proto-only*: no nodes, just message types published for other packages to import. See [nodes, packages, and flows](/docs/concepts/nodes-packages-flows). ## Import the message types ```bash # Run inside your package directory (where axiom.yaml is). axiom import axiom-official/axiom-text-ops # latest version axiom import axiom-official/axiom-text-ops@0.1.0 # pinned version ``` `axiom import` requires a prior `axiom login` and must run inside a package directory. It does four things: 1. Resolves the version (the most recently published one when `@version` is omitted) and downloads the package's `.proto` files. If this package already pins a *different* version of the same dependency in `axiom.yaml`, a bare `axiom import ` (no `@version`) is **refused** — nothing is downloaded or changed — so an existing pin never silently drifts to a newer "latest". To upgrade on purpose, pass the version explicitly (`axiom import @`); to keep the current pin, re-run with that exact version. 2. Extracts them to `imports///`. A scoped name's `/` becomes `-` in the directory name, so `axiom-official/axiom-text-ops@0.1.0` lands in `imports/axiom-official-axiom-text-ops/0.1.0/`. 3. Records the dependency in `axiom.yaml` under `imports:` with the package, version, and imported message names. 4. Runs `axiom generate` so the language bindings appear under `gen/` immediately. If an imported message has the same name as one in your local `messages/`, the import fails with a collision error before writing anything — rename one of the two first. Commit `imports/` and the updated `axiom.yaml` to your repository: `axiom push` builds your package from your git remote's `HEAD` commit, not your working tree, so unpushed import files would be missing from the build. ## What axiom import writes After importing, `axiom.yaml` carries the pinned dependency: ```yaml # axiom.yaml (excerpt written by axiom import) imports: - package: axiom-official/axiom-text-ops version: 0.1.0 messages: - TextRequest - TokensResult ``` and the raw proto definitions live in your repository. The registry names each downloaded file after the first message it defines (snake_case), not after the upstream file name — here the package's `TextRequest` and `TokensResult` share one proto file, downloaded as `text_request.proto`: ```text imports/ └── axiom-official-axiom-text-ops/ └── 0.1.0/ └── text_request.proto ``` Each entry pins an exact version. Re-running the same import is safe — it merges any newly published message names into the existing entry. Importing a different version of the same package adds a second entry and a second directory side by side; nothing is upgraded implicitly. ### Where the generated bindings go - **Go** — `gen/imports///` (Go protobuf bindings). - **Python** — one module per imported package, named after it: `gen/axiom_official_axiom_text_ops_messages_pb2.py` for the example above. - **TypeScript** — `gen/imports///`, one `_pb.js` plus a `_pb.d.ts` type surface per downloaded proto file: `text_request_pb.js` and `text_request_pb.d.ts` for the example above. - **Rust, Java, C#** — nothing at import time; the protos compile during the language build (`build.rs`/tonic-build for Rust, the protobuf-maven-plugin for Java, Grpc.Tools for C#). ## Use an imported message in a node signature Reference imported messages by their simple name, exactly like local ones. `axiom create node` lists them in its interactive message picker alongside local messages, accepts them for `--input`/`--output`, and prints a note about their origin: ```bash # Run inside your package directory, after axiom import. axiom create node Analyze --input TokensResult --output AnalysisReport # Note: TokensResult is from imported package "axiom-official-axiom-text-ops@0.1.0" ``` The node's entry in `axiom.yaml` uses the simple name (`input: TokensResult`); Axiom resolves which package it comes from via the `imports:` section. For Go, Python, Java, C#, and Rust, the scaffolded node file already contains the correct import statement. For TypeScript, the scaffold always imports from `../gen/messages_pb` (the local messages module) — edit that line to point at the imported module path shown below. ### A complete Go example A Go node whose input type comes from the imported package and whose output type is local. Replace `axiom-analytics` with the module path from your package's `go.mod`: ```go // nodes/analyze.go — input imported from axiom-official/axiom-text-ops package nodes import ( "context" "fmt" "strings" "axiom-analytics/axiom" gen "axiom-analytics/gen" axiomtextops "axiom-analytics/gen/imports/axiom-official-axiom-text-ops/0.1.0" ) // Analyze accepts tokenized text and produces a summary report with the // token count and a human-readable listing of the tokens. func Analyze(ctx context.Context, ax axiom.Context, input *axiomtextops.TokensResult) (*gen.AnalysisReport, error) { return &gen.AnalysisReport{ Summary: fmt.Sprintf("Processed %d tokens: %s", input.GetCount(), strings.Join(input.GetTokens(), ", ")), WordCount: input.GetCount(), }, nil } ``` ### Import statements per language For a message `TokensResult` imported from `axiom-official/axiom-text-ops@0.1.0`: ```python # nodes/analyze.py — Python: per-package module under gen/ from gen.axiom_official_axiom_text_ops_messages_pb2 import TokensResult ``` ```typescript // nodes/analyze.ts — TypeScript: module under gen/imports/, named after the proto file import { TokensResult } from '../gen/imports/axiom-official-axiom-text-ops/0.1.0/text_request_pb'; ``` ```rust // nodes/analyze.rs — Rust: imported messages compile into the same module as local ones use crate::gen::messages::TokensResult; ``` ```java // nodes/Analyze.java — Java: fully qualified type in the imports.* namespace import imports.axiom_official.axiom_text_ops.TokensResult; ``` ```csharp // nodes/analyze.cs — C#: bring in the imports.* namespace, then use the simple name using imports.axiom_official.axiom_text_ops; ``` Java and C# derive the namespace from the package name: `/` becomes `.` and `-` becomes `_`, so `axiom-official/axiom-text-ops` becomes `imports.axiom_official.axiom_text_ops`. Local messages stay where they always are: `gen.Messages.` in Java, the `Gen` namespace in C#. ## Embed an imported message as a field of your own message An imported message doesn't have to be a node's *whole* input or output — you can reference it as a **field** inside a message you declare locally, just like a same-file field reference. Two things go in your `messages/messages.proto`: 1. an `import` of the imported package's **stable flat proto filename**, and 2. the field written with the imported message's **proto-qualified name**. ### The stable filename `axiom generate` exposes every imported package under one deterministic filename, independent of the pinned version's on-disk path: ```text __messages.proto ``` — the package name, lowercased with every `/` and `-` turned into `_`, plus `_messages.proto`. So `axiom-official/axiom-text-ops` is importable as `axiom_official_axiom_text_ops_messages.proto` — the same stem as its Python module (`axiom_official_axiom_text_ops_messages_pb2`). The name is **stable**: it never encodes the version, so `axiom import @` never forces you to edit an embedding site. ### The proto-qualified field type Write the field type as the imported message's fully-qualified proto name — the `package …;` line from the downloaded file under `imports/…` followed by the message name. A package whose proto declares `package acmepay.v1;` with a `PaymentInfo` message is referenced as `acmepay.v1.PaymentInfo`. ### Worked example Given `axiom import acme/pay@1.2.0`, whose proto declares `package acmepay.v1;` and a `PaymentInfo` message, embed it as a field of a local `CheckoutRequest`: ```proto // messages/messages.proto syntax = "proto3"; package mypkg; import "acme_pay_messages.proto"; // the stable flat filename message CheckoutRequest { string order_id = 1; acmepay.v1.PaymentInfo payment = 2; // proto-qualified imported type } ``` `axiom generate` resolves the import and emits a real cross-package reference: the generated Go `gen/messages.pb.go` imports the already-compiled `…/gen/imports/acme-pay/1.2.0` binding, so `payment` is the **one canonical type** published by `acme/pay` — never a duplicated local copy. This is the point of embedding rather than re-declaring a structurally-identical local message: both sides of a flow edge stay the same protobuf type. > **Language support.** The embedded-field pattern is fully wired for **Go** > and **Python** — the generated code references the canonical imported > binding. For **TypeScript, Rust, Java, and C#**, `axiom generate` (or the > language build) resolves the proto symbol so compilation succeeds, but > emitting a correct cross-module reference for an *embedded field* is not yet > wired. For those languages, prefer the whole-input/output import form above, > or verify the generated binding before you publish. ## Troubleshooting - **"message X is listed in axiom.yaml imports but its proto definition is not downloaded"** — `axiom.yaml` declares the import but the files under `imports/` are missing (for example, a fresh checkout where `imports/` was never committed). Run the `axiom import @` command the error message prints. - **"import collision: message X exists in both your local messages/ and the imported package"** — two messages with the same name cannot coexist. Rename your local message (or import a package that doesn't clash), then re-run the import. - **"axiom.yaml not found — run this command from an Axiom package directory"** — `axiom import` only works inside a package directory; `cd` into the directory created by `axiom init` first. - **"Not logged in"** — run `axiom login`. Search and info work logged out; import does not. ## Next steps - [The type system](/docs/concepts/type-system) — how messages type-check across flow edges. - [Push the package and build your first flow](/docs/getting-started/first-flow) — wire your node up in the canvas. - [axiom.yaml reference](/docs/reference/axiom-yaml) — the full `imports:` schema. - [axiom import](/docs/reference/cli/axiom-import), [axiom search](/docs/reference/cli/axiom-search), and [axiom info](/docs/reference/cli/axiom-info) — generated CLI reference for the three commands. --- ## Guides > Configure an Instance from a Generic node {#guides/configure-an-instance} > Bind a Generic node's port to a real type in the console wizard or with axiom instance create — author nested message-typed fields, toggle repeated/optional, and preview a mapping's exact output before you save. # Configure an Instance from a Generic node An [Instance](../reference/glossary.md#instance) binds a [Generic node](../reference/glossary.md#generic-node)'s port(s) to a real, specific message type. The shortest path — one flat, scalar-only message per port — is covered in [`axiom instance create`](../reference/cli/axiom-instance-create.md) and the marketplace's Generic Nodes tab. This guide covers the richer authoring model both surfaces also support: a field whose type is itself another message, defined inline and recursively; `repeated`/`optional` field modifiers; and previewing a mapping's exact output before you commit to a shape. **Nothing you author while configuring an Instance becomes a real, queryable registry message until you save (console) or run `axiom instance create` (CLI) for the whole Instance.** Closing the wizard, hitting Cancel at any nesting level, or a failed create leaves nothing behind — every nested message you draft is synthesized transactionally, in the same request that creates the Instance, never as a separate call per message. ## Prerequisites - You are logged in to the Axiom app, and the CLI has run `axiom login` ([Installation](../getting-started/installation.md)). - A published **Generic node** exists to instantiate — a package with `kind: generic` and at least one `generic_ports` entry (see [Generic and Instance nodes](../reference/axiom-yaml.md#generic-and-instance-nodes)). Browse one via the marketplace's **Generic Nodes** tab. ## Configure an Instance in the console Open the wizard from the marketplace's Generic Nodes tab (**Create Instance** on a node's detail page) or by navigating to `/instances/new`. Each generic port (output, input, or both) gets its own column with two steps: **Select message**, then **Map fields**. ### Author a message with nested fields In Step 1, **Create new message** opens a form: a message name, then a list of fields. Each field has a name and a type — either a scalar (`string`, `int32`, `int64`, `uint32`, `uint64`, `double`, `float`, `bool`, `bytes`) or `message`, a reference to another message. Picking `message` opens a picker with its own **Create new message** button — authoring a nested message reuses the exact same form, recursively. There is no fixed depth limit in the UI; author as many levels as the shape actually needs (the server enforces a generous cap — see [Limits on a drafted message tree](#limits-on-a-drafted-message-tree) below). Today, a nested field's `message` type can only be an inline, freshly authored message — picking an already-published message as a nested field's type is not yet supported end to end (it would carry no field definitions of its own, and the request is rejected). Author the nested shape inline instead. Each field also has independent **repeated** and **optional** toggles. They're mutually exclusive — proto3 forbids `repeated optional` — so turning one on clears the other. A **Proto preview** toggle on the form shows the exact `.proto` text your current name/fields/modifiers produce, live, recomputed on every keystroke — useful for confirming a nested shape's rendered syntax before committing. ### Map fields and preview the mapping Step 2 renders the same field-mapping panel used for ordinary flow edges, so target fields bind to source values by name, CEL expression, or literal. Its collapsible **Preview** section runs your mapping against sample JSON you type per source, showing the *exact* value it currently produces or the mapping error blocking it — the same engine the registry and worker use at real compile/dispatch time, not an approximation. This works identically whether every message involved is already real or one is a message you just authored in Step 1: an as-yet-undrafted message is sent to the preview endpoint as an inline definition instead of a registry name, so you can iterate on a brand-new nested shape and see real output before anything is saved. ### Name and save Once every declared generic port shows **resolved**, name the Instance (`your-handle/instance-name`) and click **Save instance**. The whole draft tree — the top-level facade for each port plus every nested message, however deep — is synthesized in one transaction; a validation failure at any level leaves nothing behind. ## Configure an Instance with the CLI `axiom instance create` supports the same two authoring shapes as the console, chosen per port. ### Flat fields (the common case) `--output-field`/`--input-field name=kind` (repeatable) covers a flat, scalar-only message — see [`axiom instance create`](../reference/cli/axiom-instance-create.md) for the full flag reference. This shape is unchanged and always available. ### Nested fields with a fields-file For repeated/optional modifiers, or a field whose type is another message (recursively), pass `--output-fields-file`/`--input-fields-file` instead: a path to a JSON file shaped `{"message_name": "...", "fields": {...}}`. Each value in `fields` is either: - a bare scalar-kind string (`"total": "double"`) — identical to the flat shape, and - a JSON object for anything richer: `{"kind": "", "repeated": true}`, `{"kind": "", "optional": true}`, or `{"kind": "message", "message": {"message_name": "...", "fields": {...}}}` for a nested, not-yet-real message. `--output-fields-file` is mutually exclusive with `--output-message`/ `--output-field` on the same port — pick one shape of input per port. `--input-fields-file` is likewise exclusive with `--input-message`/ `--input-field`. The two ports are independent: the output port can use a fields-file while the input port uses flat flags, or vice versa. A worked example — an `OrderSummary` output facade with a top-level `repeated` field and a nested `Customer` message: ```json { "message_name": "OrderSummary", "fields": { "id": "int64", "tags": { "kind": "string", "repeated": true }, "customer": { "kind": "message", "message": { "message_name": "Customer", "fields": { "name": "string", "age": { "kind": "int32", "optional": true } } } } } } ``` Save that as `order-summary.fields.json` and pass it in: ```bash axiom instance create acme/orders-query \ --generic-package acme/postgres-query --generic-node "Postgres Query" \ --output-fields-file ./order-summary.fields.json ``` `OrderSummary` and `Customer` are both synthesized in the same request that creates `acme/orders-query` — neither exists as a registry row if the request fails validation at any level. ### Preview a mapping before you create `axiom instance preview` wraps `POST /adapters/preview` verbatim — the same engine the registry compiler and worker use at real dispatch time, run against sample JSON you supply, with no live invoke. It reads a request body from `--request-file` (or stdin): ```json { "dst_msg_name": "OrderSummary", "sources": [ {"edge_id": "e1", "msg_name": "Order", "sample": {"id": 1, "amount": 42.5}} ], "adapter": {"total": "$.amount"} } ``` Exactly one of `adapter` (a single-edge mapping) or `compose_mapping` (a multi-source compose consumer) is required. Any `msg_name` slot — `dst_msg_name`, or a `sources[]` entry — accepts a bare registry message name (a JSON string, as above) **or** an inline draft in place of it: the identical `{"message_name": ..., "fields": {...}}` shape the fields-file above uses, letting you preview a mapping against a message you haven't created yet: ```json { "dst_msg_name": {"message_name": "OrderSummary", "fields": {"total": "double"}}, "sources": [{"edge_id": "e1", "msg_name": "Order", "sample": {"amount": 42.5}}], "adapter": {"total": "$.amount"} } ``` ```bash axiom instance preview --request-file ./preview-request.json ``` A mapping-level failure (bad expression, type mismatch, unresolvable field) prints as an error and exits non-zero — it's a property of the mapping you're iterating on, not a malformed request. Once the output looks right, reuse the exact same message shape as the fields-file input to `axiom instance create`. See [`axiom instance preview`](../reference/cli/axiom-instance-preview.md) for the full flag reference. ## Limits on a drafted message tree Both surfaces synthesize (console Save) or accept (CLI preview/create) a draft tree under the same bounds, so a request fails fast with a clear error instead of an unbounded compile: | Limit | Bound | |---|---| | Nesting depth (top-level message counts as depth 1) | 10 | | Distinct messages in one tree (both ports combined) | 100 | | Fields on any one message | 200 | | Request body size | 1 MiB | A name reused for two different shapes within one request is rejected as a collision; drafting the identical name and shape twice (for example, the same nested message referenced from both the output and input port) is a harmless dedup, not an error. --- ## Guides > Manage secrets in a flow {#guides/manage-secrets} > Store an API key on the console's Secrets page, read it from node code with ax.secrets.get, and know exactly what is and is not encrypted. # Manage secrets in a flow To give a flow a credential — an LLM API key, a database connection string — register it once as a secret in the console, then read it from node code with `ax.secrets.get("NAME")`. Secret values are encrypted at rest, never shown again after you save them, and decrypted by the platform only when an execution starts. Node code never sees other tenants' secrets, and secrets never appear in package source or flow definitions. ## Prerequisites - You are logged in to the Axiom app ([Installation](../getting-started/installation.md)). - To read a secret from code, you need a package with at least one node — see [Your first node](../getting-started/first-node.md) or the per-language guides such as [Create a node in Python](./create-a-node-python.md). ## Add a secret in the console 1. In the app, go to **Console → Secrets** (`/console/secrets`). 2. Click **Add secret**. 3. Enter a **Name** (for example `ANTHROPIC_API_KEY`) and a **Value**. The value field is a password-style input with a show/hide toggle. 4. Click **Save secret**. ![The Secrets page in the console with one saved secret named ANTHROPIC_API_KEY in the list, showing only its name and the date it was added, with a delete button on the row](../assets/screenshots/console-secrets-page.png) What to expect after saving: - **The value is never shown again.** The secrets list displays only the name and the date added; no console page or API response ever returns a stored value. - **Saving an existing name replaces the value.** The form warns you and requires an explicit **I understand, replace it** confirmation before it lets you overwrite. - **Deletion is immediate.** The delete (trash) button on a secret's row removes it; the next execution will no longer see it. A node dispatched after the delete can tell "this was revoked" from "this was never configured" via the typed status accessor — see [Distinguish revoked from never-configured](#distinguish-revoked-from-never-configured) below. - **Secrets are tenant-scoped, not per flow.** Every secret registered here is readable by every node in every flow of your tenant. To give one flow config a different value, use a config-scoped override (see "Override a secret for one flow config" below). ## Read a secret from node code Inside a node handler, call the secrets accessor on `AxiomContext`. It returns the plaintext value plus a found flag — a missing secret yields an empty value and `false`, never an exception: ```python # nodes/greet.py — a node whose package defines GreetRequest/GreetReply from gen.messages_pb2 import GreetRequest, GreetReply from gen.axiom_context import AxiomContext def greet(ax: AxiomContext, input: GreetRequest) -> GreetReply: """Greets the caller, signing with the tenant's configured signature.""" signature, ok = ax.secrets.get("GREETER_SIGNATURE") if not ok: signature = "anonymous" return GreetReply(greeting=f"Hello {input.name}, from {signature}") ``` The same accessor exists in every SDK language: | Language | Call form | Returns | |------------|-------------------------------|--------------------------------| | Python | `ax.secrets.get("NAME")` | `(value, ok)` tuple | | Go | `ax.Secrets().Get("NAME")` | `(string, bool)` | | TypeScript | `ax.secrets.get("NAME")` | `[value, ok]` tuple | | Rust | `ax.secrets().get("NAME")` | `(String, bool)` | | Java | `ax.secrets().get("NAME")` | `Optional` | | C# | `ax.Secrets().Get("NAME")` | `(string Value, bool Found)` | All of the tenant's registered secrets are resolved when an execution starts, so a node can read any secret by name without per-flow wiring. In unit tests, the generated test file for each node ships a mock `AxiomContext` you can load with test values — for example, the Python mock accepts `secrets_map={"GREETER_SIGNATURE": "test-sig"}`. ## Distinguish revoked from never-configured `get()` alone returns the same not-found result whether a secret was deleted after the execution started or was never registered at all — there is no way to tell "re-authorize this credential" from "this was never set up" from that result. Use the `status()` accessor when your node needs to react differently to the two: ```python status = ax.secrets.status("GREETER_SIGNATURE") if status == SecretStatus.REVOKED: raise RuntimeError("GREETER_SIGNATURE was revoked — re-authorize it") elif status == SecretStatus.UNSET: raise RuntimeError("GREETER_SIGNATURE was never configured — see axiom.yaml") ``` | Language | Call form | Values | |------------|-------------------------------------|------------------------------------| | Python | `ax.secrets.status("NAME")` | `SecretStatus.{AVAILABLE,REVOKED,UNSET}` | | Go | `axiom.SecretStatusOf(ax.Secrets(), "NAME")` | `axiom.SecretStatus{Available,Revoked,Unset}` | | TypeScript | `secretStatusOf(ax.secrets, "NAME")` | `SecretStatus.{Available,Revoked,Unset}` | | Rust | `ax.secrets().status("NAME")` | `SecretStatus::{Available,Revoked,Unset}` | | Java | `ax.secrets().status("NAME")` | `AxiomContext.SecretStatus.{AVAILABLE,REVOKED,UNSET}` | | C# | `ax.Secrets().Status("NAME")` | `IAxiomContext.SecretStatus.{Available,Revoked,Unset}` | Go and TypeScript expose status as a free helper (`SecretStatusOf` / `secretStatusOf`) rather than a method on the secrets object; Rust, Java, and C# add it as a default interface/trait method. Both forms are deliberately **backward-compatible** — an existing node that only implements the secrets `get` accessor keeps compiling (ADR-156). `AVAILABLE` means `get()` will return a value right now. `REVOKED` means the secret existed when this execution started but was deleted or rotated out before this node was dispatched. `UNSET` means the tenant never registered this name at all (or, for a node you don't own, that you're not authorized to see it). `get()`'s own signature and return values are unchanged. ## Declare required secrets in axiom.yaml A node that reads secrets should declare their names in its `axiom.yaml` entry so users know what to register before invoking a flow that contains it: ```yaml # axiom.yaml name: my-org/greeter version: 0.1.0 language: python nodes: - name: Greet input: GreetRequest output: GreetReply required_secrets: - GREETER_SIGNATURE ``` The marketplace displays `required_secrets` on the package listing. `axiom validate` scans node source for secret reads and warns — it does not fail — when a name referenced in code is missing from the list. The declaration is informational: the platform does not block invocation when a declared secret is unregistered; the node simply receives a not-found result at read time. ## What is and is not encrypted Encrypted at rest: - **Secret values** saved on the Secrets page. - **Config-scoped secret overrides** saved on a flow config. Stored and displayed in plain form: - **Secret names and timestamps** — they appear in the console list, so do not put sensitive data in a secret's name. - **Flow config parameters** — config parameter values are not encrypted. Put credentials in a secret (or a config's secret override), never in an ordinary config parameter. See [Flow configs](./flow-configs.md). At runtime, the platform decrypts values when an execution starts and delivers them to node code only through the sidecar. The platform never returns a value through the management API and never writes one to its own logs or traces. After `ax.secrets.get` hands your code the plaintext, hygiene is your code's responsibility: do not log a secret with `ax.log`, copy it into an output message, or write it to agent memory — those surfaces are not encrypted secret storage, and data placed there is visible in execution and debug views. See [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md) for the isolation model. ## Override a secret for one flow config A flow config can carry its own value for a secret name, replacing the tenant-wide value for executions invoked with that config: 1. Go to **Console → Flow Configs** (`/console/flow-configs`). 2. In a config's form, open **Secret overrides** — pick the secret name and enter the override value (same masked input as the Secrets page). 3. Save the config. The list shows which names a config overrides — never the values. Overrides are encrypted at rest like ordinary secrets and are scoped to the one config that owns them. An execution invoked with that config reads the override through the same `ax.secrets.get` call; node code cannot tell the difference. See [Flow configs](./flow-configs.md) for selecting a config at invocation time. --- ## Guides > Set per-flow config values {#guides/flow-configs} > Create flow config profiles in the console — secret overrides — pick one in the Run dialog or via config_id, and see how resolved values reach nodes. # Set per-flow config values A **flow config** is a named set of runtime parameters for a flow — today, secret overrides. Configs are not part of the flow itself: they are resolved at invocation time and injected into the run, so changing one affects the next run without editing or re-saving the flow. A config scoped to one flow is called a **profile**; the **tenant default** applies to every flow you own. Prerequisites: you are logged in to the Axiom editor and have saved at least one flow ([Push and run your first flow](../getting-started/first-flow.md)). ## Create a per-flow profile On the Flow Configs page (`/console/flow-configs`): 1. Pick your flow in the **Flow** picker. With no profiles yet, the page shows "No profiles for this flow yet." 2. Click **+ New profile**. 3. Enter a **Profile name** (left blank, it becomes `default` — a flow's `default` profile is applied automatically; see "How config values are resolved" for the exact layering and its one exception). 4. Optionally add **Secret overrides**: click **+ Add override**, pick a **Secret name** from your existing secrets (or type a name when you have none yet), enter a **Value**, and click **Save override**. 5. Click **Create**. Each profile row lists its name and the names of its secret overrides, with **Edit** and **Delete** buttons. Override values are write-only: the page (and the API behind it) returns override names, never values. Two more ways to reach the same controls: - **From the editor**: with nothing selected on the canvas, the inspector shows a **Run configuration** card containing the same profile list and create form for the open flow, plus a link to the tenant default. - **Deep link**: `/console/flow-configs?graph=` preselects that flow in the picker. To manage the secrets themselves (the tenant-wide values that overrides replace), see [Manage secrets in a flow](manage-secrets.md). ## Pick a profile when you run In the editor's Run dialog, a **Config profile** selector appears once any config applies to the flow — a per-flow profile or the tenant default. It defaults to **Tenant default**; pick a profile to run with its values instead. The selection applies to that run only. When invoking over HTTP, set `config_id` in the request body. It accepts either a profile name (for the flow being invoked) or a profile ID; when omitted, the default hierarchy applies (tenant default, then the flow's `default` profile). See [Invoke a flow via the API](../getting-started/invoke-via-api.md) for the endpoint, authentication, and `graph_id`. ```bash # Run the flow with the "load-test" profile (assumes AXIOM_API_KEY is exported) curl -X POST "https://api.axiomide.com/invocations/v1/flows/invoke" \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD", "input": {"name": "Ada"}, "wait": true, "config_id": "load-test" }' ``` ## How config values are resolved At invocation time the platform merges up to three layers, later layers overriding earlier ones parameter by parameter: 1. **Tenant default** — your config named `default` that is not scoped to any flow (the one the Flow Configs page edits under "Tenant default"). 2. **Flow default** — the invoked flow's profile named `default`, when one exists. 3. **Selected profile** — the profile named in the Run dialog or in `config_id`, when it isn't `default`. Secret overrides merge across the same layers, with the later layer winning for a given secret name. One exception: selecting a profile by its ID (which is what the Run dialog sends) applies that profile directly on top of the tenant default — the flow's `default` profile is not layered in between. ## How nodes receive config values Resolution happens before the flow starts: the merged parameters and secret values are injected into the run's variables, which travel with the execution to every node. - **Secret overrides are invisible to node code.** A node reads secrets through `AxiomContext` exactly as described in [Manage secrets in a flow](manage-secrets.md); when the run's profile overrides a secret, the same read returns the override value instead of the tenant-wide one. No code change is needed to support per-profile values: ```python # nodes/summarize.py — gets the tenant-wide OPENAI_KEY, or the selected # profile's override of it, through the same call from gen.messages_pb2 import SummarizeRequest, SummarizeReply from gen.axiom_context import AxiomContext def summarize(ax: AxiomContext, input: SummarizeRequest) -> SummarizeReply: api_key, found = ax.secrets.get("OPENAI_KEY") if not found: ax.log.error("OPENAI_KEY is not set for this tenant") return SummarizeReply() ax.log.info("key resolved for this run") # never log the value itself return SummarizeReply() ``` This snippet assumes a Python package with `SummarizeRequest` and `SummarizeReply` messages and a `Summarize` node — see [Create a node in Python](create-a-node-python.md) for the scaffold. ## Configure per-node retry and backoff Retry and backoff work differently from everything else on this page: they aren't a config profile resolved at invocation time — they're a **per-node policy compiled into the flow itself**, authored in the node's `retry:` block in `flow.yaml` (or the canvas **Reliability** tab), the same way you author edges and node placement. A node with no retry policy keeps today's behavior: any failure ends the execution (subject to [failure edges](../concepts/execution-model.md#failure-edges-and-typed-errors) and [compensation](../concepts/execution-model.md#compensation-saga-rollback)). ### Author a policy ```yaml nodes: - id: call_provider node: acme/http-connector.CallProvider retry: max_attempts: 5 initial_interval_ms: 1000 backoff_coefficient: 2.0 max_interval_ms: 60000 overall_timeout_ms: 300000 non_retryable_codes: [USER] ``` The same fields live on the node's **Reliability** tab in the canvas, with a live preview of the resulting attempt schedule. ### How backoff is computed - `max_attempts` caps the total number of attempts, including the first. `0` (or omitting the field) means "use the platform default" — 3 attempts — and any value is clamped to a hard platform ceiling of 10, to bound retry storms. - Between attempts, the wait grows exponentially: `initial_interval_ms × backoff_coefficient ^ (attempt - 1)`, capped at `max_interval_ms` so a fast-growing backoff never waits longer than the ceiling. - Retries never hold a worker slot. On a retryable failure the worker durably records the attempt and releases the slot immediately; a background sweeper re-dispatches the node when its backoff elapses. This is why a connector can wait minutes for a rate limit to clear without starving other work on the same worker. ### Per-attempt and overall timeouts - `per_attempt_timeout_ms` bounds a single attempt — it can only narrow, never widen, the node's existing timeout. - `overall_timeout_ms` bounds the total time spent across every attempt. Once it's spent, retries stop even if `max_attempts` hasn't been reached — a node exhausted this way falls through to `Failed` (or a failure edge) exactly like exhausting attempts. ### Retryable vs. non-retryable failures Every failure is classified into a typed error code before the policy is consulted: `TRANSPORT` and `TIMEOUT` are retryable by default (a dropped connection or a slow upstream is worth retrying); `USER` (a deterministic business failure — retrying it just fails the same way again), `PANIC`, and `CANCELLED` are not. `non_retryable_codes` can only remove a code from retrying — it can never grant retryability to `USER`, `PANIC`, or `CANCELLED`. ### Scope: unary nodes only Retry policy applies to **unary** nodes only. Pipeline nodes (streaming, `type: pipeline` in `axiom.yaml`) and heartbeat-based liveness are not retried in this release — a pipeline node's `retry:` block, if set, is ignored. ### Side-effecting nodes must be idempotent A retried node runs again from the top — the platform has no way to know whether the previous attempt's side effect (a charge, a write, a provisioned resource) actually landed before it failed. Give any node that calls an external system, especially marketplace connectors, an idempotency key or an equivalent check-before-write guard. Marketplace connectors are a particular case worth calling out explicitly: a connector is a generic engine (HTTP, database, queue) re-typed per use — a `kind: generic` **Generic node** typed once, server-side, by an **Instance** (see the [axiom.yaml reference](../reference/axiom-yaml.md#generic-and-instance-nodes)), not a hand-authored node with its own idempotency logic baked in. The retry policy has no visibility into what the underlying call actually does, so it cannot guarantee exactly-once delivery to the external system on your behalf — the idempotency key or check-before-write guard above is still yours to provide at the connector's configured operation. --- ## Guides > Publish a flow to the marketplace {#guides/publish-a-flow} > Compile a flow.yaml with axiom flow compile, then promote the artifact to the public marketplace with axiom flow publish so other tenants can fork it or import it as a subflow. # Publish a flow to the marketplace `axiom flow publish` promotes a compiled flow to the public marketplace from the command line — the same action the editor's **Publish** button performs. Once published, every Axiom tenant can discover the flow, fork it, and import it as a [subflow](/docs/concepts/nodes-packages-flows) inside their own flows. You publish a **compiled artifact**, not a `flow.yaml` source file. So the two steps are always compile, then publish: ```bash # Compile the flow.yaml in the current directory; prints the artifact id. axiom flow compile # → compiled flow artifact 01J8Z6K3W9XQ4M7C2YB5N0A1RT # Publish that artifact to the public marketplace. axiom flow publish 01J8Z6K3W9XQ4M7C2YB5N0A1RT ``` ## Prerequisites - **A login session.** Both `axiom flow compile` and `axiom flow publish` require `axiom login`; publishing without it prints `Not logged in. Run "axiom login" first.` and exits. - **A compiled artifact.** Run `axiom flow compile` first and copy the artifact id it prints. Publishing takes that id as its only positional argument. ## Publish a compiled artifact ```bash axiom flow publish 01J8Z6K3W9XQ4M7C2YB5N0A1RT ``` Because publishing is irreversible, the command prints a confirmation prompt and waits for `y` before doing anything: ```text Publishing flow 01J8Z6K3W9XQ4M7C2YB5N0A1RT makes it publicly visible to every Axiom tenant. This cannot be undone — published flows are immutable. Publish? [y/N] ``` On confirmation it makes the flow publicly visible and prints: ```text ✓ published flow 01J8Z6K3W9XQ4M7C2YB5N0A1RT visibility: public — marketplace-visible, forkable, importable as a subflow this version is immutable; compile a new flow to iterate further ``` A published artifact can never be edited or unpublished. To change a published flow, edit its `flow.yaml`, run `axiom flow compile` again to get a **new** artifact id, and publish that. ## Publish without the prompt (CI and scripts) Pass `--yes` (`-y`) to skip the confirmation — required when stdin is not a terminal, or the command blocks waiting for input: ```bash axiom flow publish 01J8Z6K3W9XQ4M7C2YB5N0A1RT --yes ``` Add `--json` to emit a single machine-readable object instead of the human lines, so a script (or an agent) can confirm the result programmatically: ```bash axiom flow publish 01J8Z6K3W9XQ4M7C2YB5N0A1RT --yes --json ``` ```json { "artifact_id": "01J8Z6K3W9XQ4M7C2YB5N0A1RT", "visibility": "public" } ``` ## The flow CLI lifecycle `axiom flow publish` is the last verb in the command-line flow lifecycle. The full set of `axiom flow` verbs takes a flow from an empty file to a public artifact without ever opening the canvas: | Verb | What it does | |------|--------------| | `axiom flow new ` | Scaffold a starter `.flow.yaml` with two example nodes and an edge. | | `axiom flow validate [flow.yaml]` | Run local structural checks (required fields, unique node ids, edge shape). No login needed. Add `--json` for structured output. | | `axiom flow compile [flow.yaml]` | Resolve each node against the registry, auto-lay-out unpinned nodes, and compile the graph into a runnable artifact. Prints the artifact id. Requires login. Add `--json`. | | `axiom flow run ` | Invoke a compiled artifact and print its result. Pass input with `--data`/`-d` (default `{}`), wait time with `--timeout` (default 60 s), and `--json` for the full response. Requires login. | | `axiom flow pull ` | Materialize an existing compiled artifact back into an editable `flow.yaml` (`--out`/`-o`, default `flow.yaml`). | | `axiom flow publish ` | Promote a compiled artifact to the public marketplace. Requires login. | A typical command-line session runs them in order: ```bash axiom flow new greeter # write greeter.flow.yaml axiom flow validate greeter.flow.yaml # local sanity check axiom flow compile greeter.flow.yaml # → artifact id axiom flow run 01J8Z6… -d '{"name":"Ada"}' # try it axiom flow publish 01J8Z6… # share it ``` ## CLI and editor publish are the same action `axiom flow publish` and the editor's **Publish** dialog are the same action — both promote the artifact to the same public marketplace listing with the same final visibility. Publishing from the CLI and publishing from the canvas produce an identical marketplace listing; use whichever surface you are already in. With this verb, all three Axiom artifact types now have full command-line ↔ UI parity for both creating and publishing: - **Nodes/packages** — `axiom push` then `axiom publish @`, or the console. - **Flows** — `axiom flow compile` then `axiom flow publish `, or the editor's Publish button. ## Next steps - [Run your first flow](/docs/getting-started/first-flow) — build a `flow.yaml` and run it before you publish. - [Nodes, packages, and flows](/docs/concepts/nodes-packages-flows) — how a published flow becomes an importable subflow. - [Author and consume packages and flows with Claude Code](/docs/guides/author-with-claude-code) — drive this whole lifecycle from an AI agent. --- ## Guides > Debug a flow {#guides/debug-a-flow} > Start a debug session with breakpoints and pause/step controls, inspect each node's input and output on the canvas, and replay any past execution on its detail page. # Debug a flow Axiom gives you three debugging surfaces: a **live debug session** started from the editor (breakpoints, pause, single-step), **payload inspection** on any node that has executed (its decoded input and output messages), and the **execution detail page** at `/executions/`, which replays any past execution event-by-event and lets you fork a new debug session from any checkpoint. **Prerequisites:** a saved flow that runs — complete [Run your first flow](../getting-started/first-flow.md) first. Debug runs are available for unary flows only; a flow in pipeline mode runs without the debug controls. ## Start a debug run In the editor, click **Run** at the bottom-center of the canvas, fill in the input form in the **Run Graph** dialog, and click **Debug** (the amber button next to **▶ Run**). Where **▶ Run** simply executes the flow, **Debug** starts a live debug session: the run honors breakpoints and gives you manual pause/step controls. While the session is live, a control bar appears at the bottom-center of the canvas with three buttons: - **Pause** — pause the run before the next node is dispatched. Available while the run is executing. - **Continue** — resume a paused run. Replaces Pause while paused; the bar gets an amber outline when the run is paused. - **Step** — execute exactly one node, then pause again. Enabled only while the run is paused. The **Debug** button appears in the Run Graph dialog only for unary flows — pipeline flows stream frames and cannot be paused node-by-node. See [Execution model](../concepts/execution-model.md) for the unary/pipeline distinction. ## Set breakpoints Right-click any node on the canvas and choose **Set breakpoint**. A red dot appears on the node; the same menu shows **Clear breakpoint** to remove it. Breakpoints require a saved flow — on an unsaved flow the menu item shows "Save the flow first to enable breakpoints." instead of setting one. When a debug run (started with the **Debug** button) reaches a node that has a breakpoint, the run pauses before that node executes. The bottom-center control bar switches into its paused state: **Step** becomes enabled and **Continue** replaces **Pause**. Step through node-by-node, or click Continue to run until the next breakpoint or the end of the flow. Once at least one breakpoint is set, a **Breakpoints** tab appears in the result panel below the canvas. It lists every breakpoint for the open flow with the node name and package, a per-row **Clear** action, and a **Clear all** button. Breakpoints only affect runs started with **Debug** — plain **▶ Run** ignores them. ## Inspect a node's input and output After a node has executed — successfully or with an error — click it on the canvas. A popover opens showing the node's **Input** and **Output** decoded from protobuf to JSON, each with a copy-to-clipboard button. The input shown is what the node actually received at execution time. Nodes that are idle or still running do not open the popover. For pipeline nodes the popover shows **Input Frames** and **Output Frames** lists instead — expand a frame to decode it. This works on the editor canvas during and after a debug run, and on the execution detail page (`/executions/`), where clicking a node opens the same popover backed by the node's stored checkpoint and includes a **Fork from here** button (see the forking section below). Checkpointed payloads are not kept forever: snapshots are evicted by a 30-day TTL policy. An expired checkpoint shows "Snapshot expired" in the popover, but the execution's event history remains available in the **Events** tab of the detail page. ## Open the execution detail page Click **Executions** in the header navigation to open the executions list — a table with **Status**, **Started**, **Duration**, **Flow**, and **Execution** columns. Click a row to open that execution's detail page at `/executions/`. The detail page shows, top to bottom: - **A status header** — the execution's status (Queued, Running, Debug paused, Completed, Failed, …) plus started/updated/completed timestamps and duration. - **A read-only canvas replay** — the flow as it executed, with replay controls (play/pause, step, speed) and a timeline scrubber. Dragging the scrubber replays node states up to any point in the event history. - **A detail tab strip** with six tabs: - **Timeline** — the event sequence for scrubbing. - **Events** — the full paginated event log, filterable by node and by event type. - **Checkpoints** — one row per node checkpoint with step number, node name, checkpoint ID, and creation time. Clicking a row opens the payload popover for that checkpoint; each row also has a **Fork from here** button. - **Pauses** — pause/resume history for the run. - **Branches** — branch activity for the run. - **Output** — the terminal node's result. If the execution was itself forked from another execution, a **Forked from** link in the page header navigates to the parent; executions that have forks show a fork-descendants section linking to each child. ## Fork a debug session from a checkpoint On the execution detail page, open the **Checkpoints** tab (or click an executed node to open its payload popover) and click **Fork from here**. This creates a debug session in FORK mode and navigates to `/debug/` — a dedicated page for re-executing the flow from that checkpoint. The debug session page shows: - **A header** with the session ID, a mode pill (LIVE, REPLAY, or FORK), and a fork button. - **The session canvas** with a step toolbar at the bottom-center: **Step Back** (keyboard `,`), **Step Forward** (keyboard `.`), and **Run to End** (which becomes **Stop** while running). - **An Inspector panel** on the right showing the selected node's checkpointed output, plus the same six detail tabs as the execution detail page. To change state before re-executing, select a node and click **Modify…** in the Inspector. The dialog replaces one **top-level key** of the checkpointed output with a new JSON value — top-level key replacement, not a deep merge. Two values are rejected: a literal `null` (use **Clear** to remove an override key instead) and values over 1 MB. Applied overrides take effect when you fork and step forward; the Step Forward button becomes **Apply N override(s) and Fork** when overrides are pending. The same promotion happens in a live debug session in the editor: if you apply overrides while paused and then click **Step**, a confirmation dialog asks before forking — there are no silent forks. Confirming creates the fork and navigates to its `/debug/` page. Forking depends on the stored checkpoint. After the 30-day checkpoint TTL, step, modify, and fork are disabled with the tooltip "Checkpoint expired — fork unavailable". ## Related pages - [Execution model](../concepts/execution-model.md) — unary vs pipeline execution, executions, and durability. - [Run your first flow](../getting-started/first-flow.md) — build and run the flow you debug here. - [Manage secrets in a flow](./manage-secrets.md) — fix the unregistered-secrets warning shown in the Run Graph dialog. - [Flow configs](./flow-configs.md) — the **Config profile** selector in the Run Graph dialog. --- ## Guides > Use the interactive API docs {#guides/use-interactive-api-docs} > Open generated, interactive OpenAPI docs for any flow from the inspector's API section, send test requests from the browser, and fetch the raw openapi.json for codegen. # Use the interactive API docs Every flow you build gets generated, interactive API documentation: an OpenAPI description of exactly how to invoke that flow over HTTP, rendered in the editor as browsable docs with a built-in request runner. Open them from the flow inspector's **API** section — the docs are generated from the flow you currently have on the canvas, so they always match what's drawn. You can also fetch the underlying `openapi.json` directly for client generation or sharing. ## Prerequisites - A saved flow open in the editor, with at least one node on the canvas — the **Open interactive docs** button is disabled while the canvas is empty. Finish [Build your first flow](../getting-started/first-flow.md) first if you don't have one. - An API key, if you want to send test requests from the docs or fetch the raw spec with `curl` — see [Create and manage API keys](../guides/api-keys.md). Just browsing the docs needs no key beyond being signed in to the editor. ## Open the docs from the flow inspector 1. Open your flow in the editor and click an empty spot on the canvas so nothing is selected. The right-side **Inspector** panel shows the flow overview. 2. Find the **API** section. It displays the flow's invoke endpoint: `POST https://api.axiomide.com/invocations/v1/flows/invoke`, or `…/v1/flows/invoke/stream` when the flow runs in pipeline mode. 3. Click **Open interactive docs**. The button reads **Compiling flow…** while the editor compiles the current canvas into a compiled artifact and fetches its generated spec; then a full-screen overlay titled **<flow name> — Interactive API Docs** opens with the rendered documentation. 4. Click the **Close docs** button (×) in the overlay header to return to the canvas. ![The flow inspector panel with its API section showing the flow's POST invoke endpoint, the Open interactive docs button, and the Use via API (curl) link](../assets/screenshots/inspector-api-section.png) Opening the docs compiles the canvas first, so the docs can never describe a stale version of the flow — edit the flow and reopen the docs to document the new version. If compiling or fetching the spec fails, the error message appears under the button instead of an overlay. The docs renderer (Scalar) loads from a CDN, so the overlay needs internet access to display. ## What the docs describe The docs contain one operation — `POST /v1/flows/invoke` for a flow in unary mode, or `POST /v1/flows/invoke/stream` for a flow in pipeline mode — with a complete, typed request and response schema and a pre-filled example body: | Body field | What the docs show | |---|---| | `graph_id` | Pinned to the compiled artifact ID these docs were generated for, and pre-filled. Recompiling the flow produces a new ID. | | `input` | The entry node's input message as a JSON schema, field by field, with an example. | | `wait` | Unary mode only. When true, the call blocks until the execution completes and `result` is populated; when false, only `execution_id` is returned. | | `timeout_seconds` | Optional execution timeout in seconds. | | `config_id` | Optional flow config ID; when omitted, the defaults apply (tenant default → flow default). See [Flow configs](../guides/flow-configs.md). | For unary mode, the documented `202` response carries `accepted`, `execution_id`, and (with `wait: true`) a `result` object whose `output` schema is the terminal node's output message. For pipeline mode, the documented `200` response is a Server-Sent Events stream (`text/event-stream`) of frames carrying `execution_id`, `frame_index`, `payload`, `is_final`, `success`, and `error`. Both variants document the `401` unauthorized and `500` failure responses and the bearer authentication scheme. If the terminal node's output schema can't be resolved — for example the node was deleted from its package — the docs still open, but the output is documented as a generic object. ## Send a test request from the docs The docs' built-in request runner sends real requests from your browser to `https://api.axiomide.com/invocations` — the same gateway endpoint a `curl` or a generated client uses, so a test request genuinely executes the flow. 1. In the open docs overlay, set the **Bearer** authentication value to an API key (create one under **Console → API Keys** — see [Create and manage API keys](../guides/api-keys.md)). The key must belong to the same account that owns the flow. 2. The example body already has `graph_id` pinned to the version of the flow you opened the docs from; fill in the `input` fields and send. 3. A missing or invalid key returns `401` with `{"error":"unauthorized"}`. A successful unary invoke returns `202` with the result inline — the same shapes documented on the page. To invoke the flow from your own code instead, copy the ready-made command from **Use via API (curl)** in the same inspector **API** section — see [Invoke a flow via API](../getting-started/invoke-via-api.md). ## Fetch the raw openapi.json The spec behind the docs is served as plain JSON on an authenticated route, useful for client generation or for tooling that consumes OpenAPI: ```bash # Replace the ID with your flow's compiled artifact ID — the graph_id shown # in the Use via API dialog and pre-filled in the interactive docs. curl -H "Authorization: Bearer $AXIOM_API_KEY" \ 'https://api.axiomide.com/api/graphs/01JX3F8Q4ZJ4M9W4Y0B8T2K7RD/openapi.json' ``` - The response is an OpenAPI 3.0 document. Its `info.version` is the compiled artifact ID, and its `servers` entry points at the invoke endpoint on the origin you fetched from. - A flow's input and output schemas are private to your tenant, so the route requires authentication: a missing or invalid key returns `401` with `{"error":"unauthorized"}`, and an artifact ID you don't own returns `404`. - Compiled artifacts are immutable, so the spec for a given artifact ID never changes. To document an edited flow, recompile (reopen the interactive docs or the **Use via API** dialog) and fetch the new ID. ## View interactive docs for a marketplace package Published packages have interactive API docs too, with one difference: package docs are public marketplace data, generated when the package is published. 1. In the **Marketplace**, open a package's detail panel. 2. Click **Docs**. An overlay titled **<name>@<version> — Interactive Docs** opens with one operation per node in the package. No sign-in is needed to read package docs: the docs page is served at `/api/packages/@/docs` and the raw spec at `/api/packages/@/openapi.json`, both public. If the **Docs** button is disabled with "No API spec stored for this package", the package was published before specs were generated — republish it with the latest `axiom` CLI. To build a typed SDK from a package, see [Build a client SDK](../guides/build-a-client-sdk.md). ## Next steps - [Invoke a flow via API](../getting-started/invoke-via-api.md) — the ready-made `curl` command and the full request/response walkthrough. - [Create and manage API keys](../guides/api-keys.md) — mint the key the request runner and raw-spec fetch need. - [HTTP API reference](../reference/http-api.md) — every endpoint, field, and error shape. --- ## Guides > Build a client SDK {#guides/build-a-client-sdk} > Bundle marketplace nodes and flows into a client — in Console → Client Builder or headlessly with `axiom client` — build it into a typed SDK in up to six languages, and call it with an API key from the environment. # Build a client SDK The Client Builder turns a [client](../reference/glossary.md#client) — a named bundle of marketplace nodes and flows — into a typed SDK in any of six languages: Python, Go, TypeScript, Java, C#, and Rust. The SDK uses real Protocol Buffers types and exposes one method per bundled node or flow. You can build a client two ways: - **Console** — assemble a client by hand in **Console → Client Builder**. Covered first below. - **CLI** — headlessly, with the `axiom client` command group (no browser). Covered in [Build a client with the CLI](#build-a-client-with-the-cli), including the declarative `axiom-client.yaml` + `axiom client sync` workflow, which is the primary way to keep a consumer app's SDK in sync. Both paths drive the same tenant-scoped clients, members, and versions — a client created in the console can be synced from a manifest, and one created by the CLI shows up in the console. This guide creates a client, adds members from the marketplace, builds and downloads an SDK, and calls it. ## Prerequisites - An Axiom account you can sign in to the editor with. - Something to bundle: a published package (yours via `axiom push`, or any marketplace package) and/or a saved flow owned by your account. - An API key for runtime calls — the generated SDK authenticates every request with one (see [API keys](../guides/api-keys.md)). ## Create a client 1. In the app, go to **Console → Client Builder** (`/console/client-builder`): click **Console** in the top header — Client Builder is the first entry in the console sub-nav. It is also reachable from the command palette (⌘K, then "Client Builder"). 2. Click **New client**. 3. Enter a **Name** — the name your code will know the SDK by, such as `billing` — and click **Create client**. A client starts empty, and no language is chosen at creation: you pick the languages per build, and you can build the same client in more languages later. ## Add nodes and flows A client's **members** are the nodes and flows its SDK exposes — each member becomes one method. In the client's detail view: 1. Click **+ Add nodes & flows**. The page splits in two: the client's configuration on the left, the marketplace on the right. 2. Pick members in the marketplace panel: - **Packages tab** — click **+ Add all** on a package card to take every node in the package, or open the package and add nodes one at a time. A version dropdown on the card (and in the package detail) selects which package version to add. - **Flows tab** — click **+ Add** on a flow. Flows already in the client show **In client**. 3. Picked members collect in the tray at the bottom of the panel. Each has an **alias** — the method name the SDK will generate — prefilled from the node or flow name and editable inline. Aliases must be non-empty and unique within the client. 4. Click **Add N members**. The **Members** list groups node members by their package, with a **Remove all** per package and a **Remove** per member; flow members are listed individually. Removing members changes only future builds — an already-built version is never altered (see the next section). ## Build a version 1. In the **Build a version** section, select one or more languages — Python, Go, TypeScript, Java, C#, Rust. Every language you select shares **one version** of the bundle. 2. Click **Build**. The build allocates the next version number, snapshots the client's current members (with their pinned package versions), and compiles each selected language from that snapshot. The version appears in the **Versions** list with a per-language status — `pending`, `building`, then `succeeded` (or `failed`, with the error shown inline); the page refreshes statuses automatically. A version is an immutable snapshot: editing the client's members later never changes an existing version, only the next build. To add a language to an existing version, use the **+ language…** dropdown under that version. It compiles the new language from the version's frozen snapshot, so SDKs of the same version match exactly even if the client's members have changed since. ## Download and install the SDK Each succeeded language row has a **Download** button that saves a ZIP (for a client named `billing`, `billing-sdk-py.zip`). The ZIP is a ready-to-build package for the language's standard toolchain. Every SDK ships with a `README.md` covering install, authentication, and the full method list, and a `manifest.json` recording exactly which nodes, flows, and package versions the version contains. | Language | Typed messages come from | Install/build | |---|---|---| | Python | pregenerated protobuf modules (needs `protobuf>=4.0`) | `pip install -e .` | | Go | pregenerated protoc-gen-go types | `go mod tidy` | | TypeScript | protobufjs at runtime | `npm install && npm run build` | | Java | protobuf-java, compiled by Maven | `mvn package` | | C# | Google.Protobuf, compiled by Grpc.Tools | `dotnet build` | | Rust | prost + pbjson, compiled by Cargo | `cargo build` | ## Build a client with the CLI Everything above — create, add members, build, download — is also available headlessly with `axiom client`, a normal CLI wrapper around the same Client Builder the console drives. Requires a prior `axiom login`. There are two ways to use it: - **Declarative (primary path)** — commit an `axiom-client.yaml` manifest to the *consumer* app's repo and run `axiom client sync` to reconcile. This is the one to reach for by default, the same way you'd reach for `package.json` or a Terraform plan rather than hand-installing dependencies. - **Imperative verbs (escape hatches)** — `create`, `add`, `members`, `remove-member`, `build`, `status`, `list`, `get`, `download` — for one-off exploration or scripting a step `sync` doesn't cover. If you're working with Claude Code, install the `axiom-client-authoring` skill (`axiom skills install`) — it encodes this same workflow and its sharp edges so the agent can author the manifest and run `sync` for you. ### The declarative workflow: axiom-client.yaml + axiom client sync Add `axiom-client.yaml` at the root of the app that will import the generated SDK — not this platform's own repo: ```yaml # axiom-client.yaml client: billing # required; created if no client with this name/slug/id exists yet description: Billing SDK # only used when creating; not reconciled onto an existing client languages: [python, go] # SDK languages sync builds; six supported: go, python, rust, java, typescript, csharp members: - package: acme/billing-utils@1.2.0 # handle/name@version; omit the version to pin @latest node: Summarize # node name within the package alias: summarize # required, unique per manifest — becomes the SDK method name - flow: 01J8Z3K9Q2R4S6T8V0W2X4Y6Z8 # artifact id of a saved/compiled flow alias: run_pipeline ``` Each member is either a `package` + `node` (a marketplace node) or a `flow` (a saved flow's artifact ID) — exactly one of the two, plus a required, manifest-unique `alias`. Then reconcile: ```bash axiom client sync # reads ./axiom-client.yaml axiom client sync --file path/to/f.yaml axiom client sync --json # machine-readable result ``` Every run reconciles the server to match the manifest exactly — `kubectl apply` / Terraform-plan semantics, not a one-time bootstrap script: - creates the client (by its `client:` name) if it doesn't exist yet - adds every manifest member missing from the server - **removes every server member no longer listed in the manifest** - builds every manifest language not yet covered by the client's current build state — if membership changed since the last build, this cuts a brand-new version (an existing version is an immutable snapshot, so it can never be reused once membership drifts); otherwise it only tops up the missing languages onto the current latest version A converged run — the manifest unchanged since the last sync — makes **no mutating calls at all**: `sync` is idempotent and safe to re-run from a script, a pre-commit hook, or CI. An omitted package version pins `@latest` server-side; the resolved pin is printed to stderr so an unattended run stays reproducible. Once `sync` reports a build, download and install it exactly as in [Download and install the SDK](#download-and-install-the-sdk) above: ```bash axiom client download --version 2 --lang python -o ./sdks ``` `download` prints the install step for the language it just fetched (`pip install -e .`, `go mod tidy`, and so on — see the table above) and refuses to overwrite an existing file at its destination path. ### Imperative verbs (escape hatches) Use these for one-off exploration, or when scripting a step outside the declarative loop. Every command that takes a `` (as opposed to `get`, which also accepts a name or slug) wants the literal ID a client was created or listed with — get it from `create --json`, `list`, or `get`. ```bash # create a client axiom client create billing --description "Billing SDK" --language python # add a member: a marketplace node, or a saved flow axiom client add --package acme/billing-utils@1.2.0 --node Summarize --alias summarize axiom client add --flow --alias run_pipeline # list/inspect members axiom client members axiom client remove-member # build and watch a version axiom client build --lang python,go # cut a new version, build both axiom client build --lang rust --version 3 # add a language to version 3 axiom client status --version 2 --watch # poll until every language finishes # list clients, or inspect one by name, slug, or id axiom client list axiom client get billing # download a succeeded build axiom client download --version 2 --lang python -o ./sdks ``` #### Flag reference Each row is the exact flag set a subcommand registers, besides the built-in `-h`/`--help`. It mirrors the CLI's own `--help` output. | Command | Flags | |---|---| | `axiom client create ` | `--description`, `--language`, `--json` | | `axiom client list` | `--json` | | `axiom client get ` | `--json` | | `axiom client add ` | `--package`, `--node`, `--flow`, `--alias`, `--json` | | `axiom client members ` | `--json` | | `axiom client remove-member ` | _none_ | | `axiom client build ` | `--lang`, `--version`, `--json` | | `axiom client status ` | `--version`, `--watch`, `--json` | | `axiom client download ` | `--version`, `--lang`, `--output` (`-o`) | | `axiom client sync` | `--file`, `--json` | So `--json` is available on every subcommand except `download` (it always writes a file, never JSON) and `remove-member` (registers no flags at all). `status` (with or without `--watch`) exits non-zero if any language in the requested version failed. ## Call the SDK No API key is embedded in a generated SDK. Every language's client reads the `AXIOM_API_KEY` environment variable at construction (or takes the key as an explicit constructor argument) and sends it as a bearer token on every request: ```bash export AXIOM_API_KEY="" ``` ```python # app.py — after installing the Python SDK of a client named "billing" from billing_sdk import Client client = Client() # reads AXIOM_API_KEY from the environment result = client.summarize(req) # one method per member, named by its alias ``` Methods take the member's typed input message and return its typed output message — the message classes are the generated protobuf types listed in the SDK's `README.md`. Method names derive from the alias in the language's own convention (`summarize_invoice` in Python and Rust, `summarizeInvoice` in Go, TypeScript, and Java, `SummarizeInvoiceAsync` in C#). A node member invokes that node directly by its human-readable URL (`/v1/nodes/{owner}/{package}/{version}/{node}`); a flow member invokes the flow's compiled artifact and returns its result. Pipeline (streaming) members return a stream of typed frames — a generator in Python, an async iterator in TypeScript, and the language's equivalent elsewhere. The deployment origin the SDK calls is baked in at build time from the deployment you downloaded it from; every constructor accepts a base-URL override for pointing the same SDK at another deployment. A `401` response means a missing, invalid, or revoked API key — see [API keys](../guides/api-keys.md). --- ## Guides > Create and manage API keys {#guides/api-keys} > Mint, use, and revoke API keys in Console → API Keys, understand what a key can access, and read the Usage page's execution stats. # Create and manage API keys API keys authenticate the Axiom CLI and any code that calls your flows over HTTP. This guide creates a key in the account console, shows the three ways to use it, explains what a key can and cannot access, covers revocation, and walks through the Usage page next to it in the Console. ## Prerequisites - An Axiom account you can sign in to the editor with. - To follow the invocation example: a saved flow owned by the same account — see [Invoke a flow via API](../getting-started/invoke-via-api.md). ## Create an API key 1. In the app, go to **Console → API Keys** (`/console/api-keys`). 2. Click **Create key**. 3. In the **Create API key** dialog, fill in **Name** — pick something that identifies where the key will live, such as `ci`, `laptop`, or `production-cli`. A name is required. 4. Click **Create key** (or press Enter). The **Save your API key** dialog then shows the raw key — a 64-character hex string — **exactly once**. Copy it with the copy button and store it somewhere safe before clicking **Done**. The platform stores only the key's SHA-256 hash, so the full key is never shown again and cannot be recovered; if you lose it, revoke it and create a new one. After the dialog closes, the key appears in the list with four columns: - **Name** — the name you gave it. - **Key** — a masked form only: the first 8 characters, an ellipsis, and the last 4 (for example `a1b2c3d4…9f0e`). The full key never appears in the list. Keys minted automatically by `axiom login` before masking metadata existed show `—` here. - **Created** — the creation date. - **Last used** — `Never` until the key authenticates a request; afterwards the date of recent use (updated at most once per minute, so it can lag a burst of requests slightly). ## What a key can access Keys have no per-key scopes or permission levels: every key carries the full access of the account that created it. One key can authenticate the CLI, invoke any flow the account owns, and call the management API — there is no way to mint a read-only or single-flow key today. Treat each key like a password, and create one key per place it lives (one per CI system, one per machine) so you can revoke them independently. Access is bounded by your tenant. A key resolves to the tenant that created it, and the platform enforces that one tenant's flows, secrets, memory, and results are never visible to another — a key cannot reach another tenant's data, and invoking a flow requires that the key belongs to the same account that owns the flow. See [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md). ## Use a key **From the CLI.** `axiom login` runs a browser OAuth device flow and stores a freshly minted key (it appears in the key list under the name `cli`) in `~/.axiom/credentials`. In CI, skip the browser entirely by setting `AXIOM_API_KEY` before logging in: ```bash # CI: store the key without a browser flow, then confirm the identity export AXIOM_API_KEY="" axiom login axiom whoami # prints the Email and Tenant the stored key resolves to ``` **Over HTTP.** Send the key as a bearer token in the `Authorization` header. For example, invoking a compiled flow (get the exact command, including your `graph_id`, from the **Use via API** dialog — see [Invoke a flow via API](../getting-started/invoke-via-api.md)): ```bash # Use the graph_id of your own compiled flow (copy the ready-made command from # the Use via API dialog); the id below is only an example. curl -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD", "input": {}, "wait": true}' ``` **From a Client Builder SDK.** SDKs built with the Client Builder read the key from the `AXIOM_API_KEY` environment variable (no key is ever embedded in an SDK) — see [Build a client SDK](./build-a-client-sdk.md). ## Revoke a key 1. Go to **Console → API Keys**. 2. In the key's row, click the **Revoke <name>** button (the trash icon). 3. Confirm with **Revoke key** in the dialog. Revocation is immediate and cannot be undone: the key fails authentication on its very next use, because keys are checked against the database on every request with no cache. Any client still using the key — including a CLI logged in with it — stops working at once; if you revoke the key your own CLI session uses, run `axiom login` again. You can only revoke keys belonging to your own account. ## Monitor usage **Console → Usage** (`/console/usage`) shows your account's flow executions over the last 30 days. It reflects activity only — it is not a bill. - **Summary cards:** **Executions** (total runs in the window), **Success rate** (percentage of runs that completed successfully), and **Avg duration** (mean wall-clock time of completed runs — in-flight runs never skew it). - **Executions per day:** a stacked bar chart, one bar per day. Hovering a bar shows the date, the day's total, and the succeeded/failed split. Successful and failed runs are colored distinctly; runs still in flight (or paused) count toward the total only. A run counts as failed if it ended in failure, was cancelled, or ended while compensating. All numbers are scoped to your tenant. For digging into an individual [execution](../reference/glossary.md#execution) rather than aggregates, see [Debug a flow](./debug-a-flow.md). --- ## Guides > Author and consume packages and flows with Claude Code {#guides/author-with-claude-code} > Install Axiom's Skills into Claude Code with axiom skills install so your agent knows the create→validate→push→publish authoring loop AND the search→inspect→source→invoke loop for using published nodes, then drives the axiom CLI through the shell. # Author and consume packages and flows with Claude Code Axiom lets [Claude Code](https://www.anthropic.com/claude-code) author packages and flows for you — and find, verify, and call already-published ones — through **Skills**: Markdown contracts that teach the agent each workflow and the sharp edges that make it fail. Install them with `axiom skills install`, then the agent drives the `axiom` CLI through the shell. That is all Claude Code needs: it already runs shell commands, so the Skill's *workflow knowledge* plus the CLI's `--json` output is enough for the agent to author, validate, and publish — or to discover, inspect, and invoke a published node — end to end. There is nothing else to install or register. ## Install the Skills The `axiom` CLI ships the Skills embedded in the binary, so installing them needs no checkout and no network: ```bash # From your project directory. axiom skills install ``` This writes the Skills into `./.claude/skills` by default — project-local, so they travel with the repository and your agent picks them up the moment it opens the project. The command prints what it installed: ```text ✓ axiom-client-authoring → .claude/skills/axiom-client-authoring ✓ axiom-consume → .claude/skills/axiom-consume ✓ axiom-flow-authoring → .claude/skills/axiom-flow-authoring ✓ axiom-instance-authoring → .claude/skills/axiom-instance-authoring ✓ axiom-package-authoring → .claude/skills/axiom-package-authoring Installed 5 skill(s) into .claude/skills. Open the project in Claude Code — it picks up SKILL.md automatically. ``` Claude Code reads any `SKILL.md` under a `.claude/skills/` directory automatically; there is no enable step. ### Choose where they install | Command | Target | Use when | |---------|--------|----------| | `axiom skills install` | `./.claude/skills` | Per-project — the Skills commit alongside your code. | | `axiom skills install --global` | `~/.claude/skills` | Every project on your machine. | | `axiom skills install --dir DIR` | `DIR` | An explicit location. | | `axiom skills install --force` | (as above) | Overwrite Skills that already exist in the target — otherwise existing copies are left untouched and reported as skipped. | List what the binary bundles without installing anything: ```bash axiom skills list # axiom-client-authoring # axiom-consume # axiom-flow-authoring # axiom-instance-authoring # axiom-package-authoring ``` ## What each Skill covers | Skill | Use it to | |-------|-----------| | `axiom-package-authoring` | Author a **node package** — the create→validate→dev→push→publish loop, the frozen per-language node signatures, how to read `axiom validate --json` as a fix loop, and the sharp edges (messages before nodes, PascalCase names, commit *and* push before `axiom push`). | | `axiom-flow-authoring` | Author a **flow** with `axiom flow` — the full `flow.yaml` schema and the new→validate→compile→run→publish (and pull) workflow, including pauses, conditional branches, loops, joins, and nested subflows. | | `axiom-client-authoring` | Author a **client** (typed SDK) with `axiom client` — the declarative `axiom-client.yaml`→`sync`→download loop for bundling published nodes/flows into an SDK for another codebase. | | `axiom-instance-authoring` | Bind a **Generic node**'s port(s) to a real type with `axiom instance create` — the declare→create→reference loop for turning a marketplace connector (a database query, an HTTP call) into a node typed for one specific table or endpoint. | | `axiom-consume` | **Find, verify, and call** already-published nodes and flows — the search→inspect (schema)→source (code + full repo)→invoke→pin loop for using someone else's marketplace artifact, including how to trust a third-party node before wiring it in. Also the **propose/feedback** loop: when the catalog is missing something the agent needed, or a node or the CLI was awkward, it reports that *after trying* (`axiom propose`, `axiom feedback`). The consume counterpart to the authoring Skills, and the same catalog surface the [hosted MCP server](/docs/guides/hosted-mcp-server) exposes. | ## Install the Skills manually If you are not using the CLI binary, copy the Skill directories straight into a Claude Code skills directory: ```bash cp -r skills/axiom-client-authoring ~/.claude/skills/ cp -r skills/axiom-consume ~/.claude/skills/ cp -r skills/axiom-flow-authoring ~/.claude/skills/ cp -r skills/axiom-instance-authoring ~/.claude/skills/ cp -r skills/axiom-package-authoring ~/.claude/skills/ ``` This is equivalent to `axiom skills install --global` — the embedded copies the CLI installs are generated from these same source directories. ## Next steps - [Write your first node](/docs/getting-started/first-node) and [run your first flow](/docs/getting-started/first-flow) — the workflows the Skills encode. - [Publish a flow to the marketplace](/docs/guides/publish-a-flow) — the last step the flow Skill drives. --- ## Guides > Use Axiom from any MCP client (hosted MCP server) {#guides/hosted-mcp-server} > Add Axiom's hosted MCP server in any MCP client, authenticate with an Axiom API key, and search the catalog, inspect schemas, read a node's source, invoke nodes and flows, and pin favorites as typed tools — without installing the CLI. # Use Axiom from any MCP client (hosted MCP server) Axiom ships a **hosted MCP server** (Model Context Protocol) so an external agent or user can use Axiom **without installing anything**. Add the server in any MCP client, authenticate with your Axiom API key, and your agent can search the catalog, inspect a node or flow's schema, read a node's source code to verify what it does, invoke it synchronously, and pin favorites that show up as native typed tools — all over the gateway, scoped to your tenant. This is a **consume-only** surface. It does not author: you cannot create, compose, push, or publish from the MCP server. The `axiom` CLI stays the fully-featured authoring tool (see [Author with Claude Code](./author-with-claude-code.md)); the MCP server is how an agent *uses* what has been published. ## What it is - **Hosted and zero-install.** The server runs in the Axiom platform behind the gateway; your MCP client connects to a URL, not a local process. No CLI, no Docker, no SDK in your agent's code. - **Consume-only.** Search, inspect, read source, invoke, and pin. No authoring lifecycle. - **Authenticated and tenant-scoped.** Your API key authenticates through the same gateway every other Axiom client uses (see [API keys](./api-keys.md)). Every action is scoped to your tenant: you see public catalog items, your own private items, and items you've been granted an entitlement to — never another tenant's private data. - **Typed.** Pinned nodes and flows surface as first-class MCP tools with a derived input schema, so your agent calls them with structured arguments instead of guessing JSON shapes. ## Add the Axiom MCP server You need one thing first: an **Axiom API key** — create one in **Console → API Keys** (see [Create and manage API keys](./api-keys.md)). Then add the server in a single step. **The only edit you make is replacing `YOUR_AXIOM_API_KEY` with your key** — everything else is ready to paste as-is. **Claude Code** — run one command: ```bash claude mcp add --transport http axiom https://api.axiomide.com/mcp \ --header "Authorization: Bearer YOUR_AXIOM_API_KEY" ``` **Claude Desktop, Cursor, or any client with an `mcpServers` config file** — paste this block: ```json { "mcpServers": { "axiom": { "type": "http", "url": "https://api.axiomide.com/mcp", "headers": { "Authorization": "Bearer YOUR_AXIOM_API_KEY" } } } } ``` That's it — reload your client and the Axiom tools below appear. Under the hood the server speaks the MCP Streamable HTTP transport at the `/mcp` path on the gateway; the gateway validates your key (the same JWT/API-key authorizer the HTTP API uses) and stamps the authoritative tenant identity, so you only ever send the `Authorization` header — the server never trusts a tenant the client supplies. ## The tools Once connected, `tools/list` advertises the consume surface: - **`axiom_search`** — search the catalog for **nodes** and **flows**, lexical and semantic, tenant-scoped (public + your own + what you're entitled to). Results carry the invocable id (`node_id` for nodes, `graph_id` for flows) so they pipe straight into inspect or invoke. - **`axiom_inspect`** — get the compiled input/output **JSON Schema** for a node or a flow (the schema a typed invoke payload must satisfy). - **`axiom_get_source`** — get a node's handler **source code** plus a pointer to its **full repository** (`source_url`, pinned to the exact published commit). `axiom_inspect` tells you a node's input/output *shape*; `axiom_get_source` tells you what it actually *does*, so you can verify a node before invoking it. The snippet is the handler body; `source_url` is the escape hatch for reading code the handler calls into. For an Instance node the source is the inherited generic implementation, and `generic_package` labels its origin. - **`axiom_invoke`** — synchronously invoke a published **node** or a compiled **flow**, resolved by id, alias, or a pinned name. This wraps the existing invoke endpoints — it does not create a new invoke path. - **`axiom_pin` / `axiom_unpin`** — pin a node or a flow as a favorite (one pin targets one node *or* one flow). Pins live in a shared store: a pin you make from the MCP server is also visible to `axiom list-my-tools` in the CLI, and vice versa. - **`axiom_list_my_tools`** — list your pinned favorites. - **`axiom_propose`** — propose a **package, node, or flow** that the catalog doesn't have yet. Reach for it the moment a task needs an artifact `axiom_search` can't find: your proposal is high-signal demand because you hit the gap doing real work. It is **propose-only** — an agent can request a missing artifact, but filing product bug reports and feature requests stays with the human (the CLI's `axiom feedback` and the editor's feedback dialog), because those need human repro judgment. ## Pinned tools become typed tools When you pin a node or a flow, it does not just appear in a list — it shows up on `tools/list` as a **first-class MCP tool** with a derived `inputSchema` compiled from the target's proto definition. Your agent can then call it directly with structured arguments, exactly like a native function. Unpin it and the tool disappears. (A pinned node's schema comes from its compiled input JSON Schema; a pinned flow's comes from its on-demand OpenAPI spec.) ## Current limitations The hosted MCP server is **synchronous only** today. Know these before you rely on it: - **Sync-only, ~30s.** `axiom_invoke` waits for a synchronous result and has a roughly 30-second budget. There is no async `get_result`, no HITL resume, no `recommend`, and no `cancel` yet — those are planned but not available today. - **A target that can't finish synchronously tells you so — it never hangs or silently pauses.** If you invoke something that requires human approval (a HITL-bearing target) or that would exceed the sync budget, `axiom_invoke` returns a clear, explicit **not-callable** result with the reason (`hitl_target` or `exceeds_sync_timeout`). It does **not** return a silent `PAUSED` and it does not hang your agent — that is a hard guarantee, not best-effort. - **No authoring.** You cannot create, compose, push, or publish from the MCP server. To author, use the `axiom` CLI (see [Author with Claude Code](./author-with-claude-code.md)). - **Tenant-scoped.** You see only public catalog items, your own, and what you are entitled to — the same boundary as the HTTP API (see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)). ## Next steps - Call a flow the classic way — [Invoke a flow via API](../getting-started/invoke-via-api.md). - Author and publish nodes and flows with the CLI — [Author with Claude Code](./author-with-claude-code.md). - The HTTP API the MCP server wraps — [HTTP API reference](../reference/http-api.md). --- ## Guides > Send feedback and propose marketplace artifacts {#guides/send-feedback-and-propose} > Tell Axiom what to build or fix, and request a package, node, or flow that doesn't exist yet — from the editor's feedback dialog, the axiom CLI (axiom feedback / axiom propose), or an MCP client. Every submission feeds the roadmap review. # Send feedback and propose marketplace artifacts Axiom has two ways to hear from you, split by **what you're telling us**: - **Feedback on the product** — a bug, a feature idea, or general thoughts about Axiom itself. - **A proposal for the marketplace** — a **package, node, or flow you wish existed** (or a change to one that does). This is the request that turns "the catalog is missing X" into demand we can act on. Both land in the same review queue and seed our roadmap planning. Nothing here is public, and nothing is deleted — submissions are kept so we can come back to them. ## From the editor Click **Send feedback** in the header (or open the command palette with `⌘K` / `Ctrl-K` and choose *Send feedback…*). Pick one of four options: | Option | Use it for | |--------|------------| | **Feature request** | Something Axiom should do | | **Bug report** | Something broken or surprising | | **General feedback** | Anything else on your mind | | **Propose a package/node/flow** | A marketplace artifact you wish existed | Choosing **Propose a package/node/flow** reveals a small extra picker: - **What kind?** — Node, Package, or Flow. - **Against an existing artifact? (optional)** — if you're asking us to *extend* something that already exists (say, "add pagination to `acme/notion`"), name it here. Leave it blank for a brand-new artifact. Then describe what it should do — for a proposal, its inputs, outputs, and an example are the most useful things you can give us — and click **Send**. ## From the CLI The `axiom` CLI mirrors the editor. Both verbs need a prior `axiom login`; the message can be an argument or piped on stdin, and `--json` prints the persisted submission. ### Report on the product ```bash axiom feedback bug "axiom invoke printed the error to stdout, not stderr — broke my jq pipe" axiom feedback feature "let axiom search filter by handle so I can find one publisher's nodes" axiom feedback general "inspect + source is a great combo — I trusted a node in one pass" ``` ### Propose a marketplace artifact ```bash axiom propose node "refund a Stripe charge by id, returning the refund object" axiom propose package "first-class Notion connector (query + append rows)" axiom propose flow "new-signup onboarding: create user, send welcome email, seed workspace" # --details adds the longer shape; --for points at an existing artifact to extend. axiom propose node "add cursor pagination" --for acme/notion \ --details "page through database rows; inputs: cursor, page_size; outputs: rows, next_cursor" ``` See the [`axiom feedback`](/docs/reference/cli/axiom-feedback) and [`axiom propose`](/docs/reference/cli/axiom-propose) reference pages for the full flag set. ## From an MCP client If you drive Axiom through the [hosted MCP server](/docs/guides/hosted-mcp-server), the **`axiom_propose`** tool submits a package/node/flow proposal — the same review queue as the CLI and editor. It is **propose-only**: an agent can request a missing artifact (the highest-signal moment is right after `axiom_search` turns up nothing for a real task), but product bug reports and feature requests stay with a human, through the CLI or the editor, because they need human judgment to reproduce. ## What happens next Every submission goes to Axiom's review queue and feeds roadmap planning. Today proposals are reviewed centrally by the Axiom team — they are **not** yet routed to the author of the package you named in `--for`; that's on the roadmap. The best proposals read like a spec: what the artifact does, the inputs it takes, the outputs it returns, and a concrete example of when you needed it. --- ## Reference > axiom {#reference/cli/axiom} > Axiom CLI — build and push node packages # axiom Axiom CLI — build and push node packages Axiom is a CLI for creating, developing, and pushing node packages. Use "axiom init" to start a new package, then "axiom create node" to add nodes, "axiom dev" to run locally, and "axiom push" to deploy as a private package. When you're ready to share it, "axiom publish @" promotes a pushed package to an immutable, publicly listed marketplace release. ## Usage ```sh axiom [flags] axiom [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for axiom | | `--version` | `-v` | bool | | version for axiom | ## Subcommands | Command | Description | |---|---| | [axiom build](./axiom-build.md) | Build a local Docker image identical to the publish pipeline | | [axiom client](./axiom-client.md) | Create and manage Client Builder clients (typed SDKs) | | [axiom create](./axiom-create.md) | Create Axiom resources | | [axiom dev](./axiom-dev.md) | Start a local development server with hot reload | | [axiom doctor](./axiom-doctor.md) | Check the local toolchain needed to build and test Axiom packages | | [axiom feedback](./axiom-feedback.md) | Send Axiom a bug report, feature request, or general feedback | | [axiom flow](./axiom-flow.md) | Author and compile flows (graphs of published nodes) | | [axiom generate](./axiom-generate.md) | Compile .proto files into language bindings | | [axiom import](./axiom-import.md) | Import proto definitions from a published package | | [axiom info](./axiom-info.md) | Show package details, nodes, messages, and live endpoint | | [axiom init](./axiom-init.md) | Initialize a new Axiom package | | [axiom inspect](./axiom-inspect.md) | Print the compiled JSON Schema for a node or flow (the shape an invoke must satisfy) | | [axiom instance](./axiom-instance.md) | Create Instances from a Generic node (type-bind, no build, no deploy) | | [axiom invoke](./axiom-invoke.md) | Invoke a published node directly and print its raw JSON response | | [axiom list-my-tools](./axiom-list-my-tools.md) | List the nodes and flows you have pinned | | [axiom login](./axiom-login.md) | Authenticate with the Axiom platform | | [axiom pin](./axiom-pin.md) | Pin a node or flow as a favorite tool | | [axiom propose](./axiom-propose.md) | Propose a new package, node, or flow for the Axiom marketplace | | [axiom publish](./axiom-publish.md) | Publish a pushed package to the public marketplace | | [axiom push](./axiom-push.md) | Push the package to the Axiom platform (tenant-private) | | [axiom remove](./axiom-remove.md) | Remove Axiom resources | | [axiom search](./axiom-search.md) | Search the Axiom package marketplace | | [axiom skills](./axiom-skills.md) | Manage the Axiom Skills for Claude Code | | [axiom source](./axiom-source.md) | Print a node's handler source code and a link to its full repository | | [axiom test](./axiom-test.md) | Run language-native tests with axiom validation | | [axiom unpin](./axiom-unpin.md) | Unpin a node or flow | | [axiom validate](./axiom-validate.md) | Validate axiom.yaml, proto definitions, and node signatures | | [axiom version](./axiom-version.md) | Print the axiom CLI version | | [axiom whoami](./axiom-whoami.md) | Show the current authenticated user | ## See also - [axiom build](./axiom-build.md) — Build a local Docker image identical to the publish pipeline - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) - [axiom create](./axiom-create.md) — Create Axiom resources - [axiom dev](./axiom-dev.md) — Start a local development server with hot reload - [axiom doctor](./axiom-doctor.md) — Check the local toolchain needed to build and test Axiom packages - [axiom feedback](./axiom-feedback.md) — Send Axiom a bug report, feature request, or general feedback - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) - [axiom generate](./axiom-generate.md) — Compile .proto files into language bindings - [axiom import](./axiom-import.md) — Import proto definitions from a published package - [axiom info](./axiom-info.md) — Show package details, nodes, messages, and live endpoint - [axiom init](./axiom-init.md) — Initialize a new Axiom package - [axiom inspect](./axiom-inspect.md) — Print the compiled JSON Schema for a node or flow (the shape an invoke must satisfy) - [axiom instance](./axiom-instance.md) — Create Instances from a Generic node (type-bind, no build, no deploy) - [axiom invoke](./axiom-invoke.md) — Invoke a published node directly and print its raw JSON response - [axiom list-my-tools](./axiom-list-my-tools.md) — List the nodes and flows you have pinned - [axiom login](./axiom-login.md) — Authenticate with the Axiom platform - [axiom pin](./axiom-pin.md) — Pin a node or flow as a favorite tool - [axiom propose](./axiom-propose.md) — Propose a new package, node, or flow for the Axiom marketplace - [axiom publish](./axiom-publish.md) — Publish a pushed package to the public marketplace - [axiom push](./axiom-push.md) — Push the package to the Axiom platform (tenant-private) - [axiom remove](./axiom-remove.md) — Remove Axiom resources - [axiom search](./axiom-search.md) — Search the Axiom package marketplace - [axiom skills](./axiom-skills.md) — Manage the Axiom Skills for Claude Code - [axiom source](./axiom-source.md) — Print a node's handler source code and a link to its full repository - [axiom test](./axiom-test.md) — Run language-native tests with axiom validation - [axiom unpin](./axiom-unpin.md) — Unpin a node or flow - [axiom validate](./axiom-validate.md) — Validate axiom.yaml, proto definitions, and node signatures - [axiom version](./axiom-version.md) — Print the axiom CLI version - [axiom whoami](./axiom-whoami.md) — Show the current authenticated user --- ## Reference > axiom build {#reference/cli/axiom-build} > Build a local Docker image identical to the publish pipeline # axiom build Build a local Docker image identical to the publish pipeline Full local reproducible build — identical artifacts to the publish pipeline. Steps: ```text 1. Compile proto bindings (axiom generate) 2. Validate package (axiom validate) 3. Generate gRPC service (.axiom/service.) 4. Generate Dockerfile (.axiom/Dockerfile) 5. Assemble build context (.axiom/image/) 6. Build Docker image (docker build) ``` All artifacts are written to .axiom/ and are fully inspectable. ## Usage ```sh axiom build [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for build | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) --- ## Reference > axiom client {#reference/cli/axiom-client} > Create and manage Client Builder clients (typed SDKs) # axiom client Create and manage Client Builder clients (typed SDKs) Create and manage Client Builder clients: tenant-scoped bundles of marketplace nodes and flows that compile into a typed SDK. ```text axiom client create # create a new, empty client axiom client list # list your tenant's clients axiom client get # show one client's members and versions axiom client add --package ... --node ... --alias ... axiom client add --flow --alias ... axiom client members # list members axiom client remove-member # remove a member axiom client build --lang python,go # build an SDK axiom client status --version N --watch # poll build status axiom client sync # reconcile a client to match axiom-client.yaml ``` See docs/guides/build-a-client-sdk.md for the full Client Builder workflow. Requires a prior "axiom login". ## Usage ```sh axiom client [flags] axiom client [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for client | ## Subcommands | Command | Description | |---|---| | [axiom client add](./axiom-client-add.md) | Add a marketplace node or a saved flow as a client member | | [axiom client build](./axiom-client-build.md) | Build an SDK for a client in one or more languages | | [axiom client create](./axiom-client-create.md) | Create a new, empty Client Builder client | | [axiom client download](./axiom-client-download.md) | Download a succeeded build's SDK zip | | [axiom client get](./axiom-client-get.md) | Show one Client Builder client | | [axiom client list](./axiom-client-list.md) | List your tenant's Client Builder clients | | [axiom client members](./axiom-client-members.md) | List a client's members | | [axiom client remove-member](./axiom-client-remove-member.md) | Remove a member from a client's draft | | [axiom client status](./axiom-client-status.md) | Show a client version's per-language build status | | [axiom client sync](./axiom-client-sync.md) | Reconcile a client to match a declarative axiom-client.yaml manifest | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom client add](./axiom-client-add.md) — Add a marketplace node or a saved flow as a client member - [axiom client build](./axiom-client-build.md) — Build an SDK for a client in one or more languages - [axiom client create](./axiom-client-create.md) — Create a new, empty Client Builder client - [axiom client download](./axiom-client-download.md) — Download a succeeded build's SDK zip - [axiom client get](./axiom-client-get.md) — Show one Client Builder client - [axiom client list](./axiom-client-list.md) — List your tenant's Client Builder clients - [axiom client members](./axiom-client-members.md) — List a client's members - [axiom client remove-member](./axiom-client-remove-member.md) — Remove a member from a client's draft - [axiom client status](./axiom-client-status.md) — Show a client version's per-language build status - [axiom client sync](./axiom-client-sync.md) — Reconcile a client to match a declarative axiom-client.yaml manifest --- ## Reference > axiom client add {#reference/cli/axiom-client-add} > Add a marketplace node or a saved flow as a client member # axiom client add Add a marketplace node or a saved flow as a client member Add one member to a client's draft — a marketplace node, or a saved flow. Exactly one of --package or --flow must be given (flag branching): ```text axiom client add c1 --package billing-utils --node summarize --alias summarize axiom client add c1 --package billing-utils@1.2.0 --node summarize --alias summarize axiom client add c1 --flow --alias run_pipeline ``` --package accepts "name" or "name@version". Omitting the version pins @latest server-side — the resolved pinned version is printed as a loud warning so an unattended run (e.g. a skill wiring this up) never silently loses reproducibility. Requires a prior "axiom login". ## Usage ```sh axiom client add [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--alias` | | string | | Member alias — the SDK method name; must be unique within the client | | `--flow` | | string | | Artifact ID of a saved flow to add | | `--help` | `-h` | bool | | help for add | | `--json` | | bool | | Emit a single JSON result object | | `--node` | | string | | Node name within --package | | `--package` | | string | | Package to add a node from, as name or name@version | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client build {#reference/cli/axiom-client-build} > Build an SDK for a client in one or more languages # axiom client build Build an SDK for a client in one or more languages Start a build for a client: snapshot its current draft members (or reuse an existing version's frozen snapshot) and compile each selected language. ```text axiom client build c1 --lang python,go # cut a new version, build both axiom client build c1 --lang rust --version 3 # add a language to version 3 axiom client build c1 --lang python,go --json # machine-readable output ``` A build starts every selected language in "pending"; it transitions to "building" then "succeeded" or "failed" asynchronously. Poll progress with: ```text axiom client status c1 --version N --watch ``` Requires a prior "axiom login". ## Usage ```sh axiom client build [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for build | | `--json` | | bool | | Emit a JSON array of the started build rows | | `--lang` | | string | | Comma-separated languages to build (python, go, typescript, java, csharp, rust) | | `--version` | | int | `0` | Existing version to add languages to (omit to cut a new version from the current draft) | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client create {#reference/cli/axiom-client-create} > Create a new, empty Client Builder client # axiom client create Create a new, empty Client Builder client Create a new Client Builder client: a named, tenant-scoped bundle of nodes and flows that later builds into a typed SDK. A client starts empty and picks no language at creation — add members and build with the console (Console → Client Builder) or a follow-on "axiom client" subcommand, then pick languages per build. ```text axiom client create billing axiom client create billing --description "Billing SDK" --language python axiom client create billing --json # machine-readable output ``` Requires a prior "axiom login". ## Usage ```sh axiom client create [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--description` | | string | | Optional client description | | `--help` | `-h` | bool | | help for create | | `--json` | | bool | | Emit a single JSON result object | | `--language` | | string | | Default SDK language (python, go, typescript, java, csharp, rust) | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client download {#reference/cli/axiom-client-download} > Download a succeeded build's SDK zip # axiom client download Download a succeeded build's SDK zip Download the SDK zip for one succeeded language build of a client version. ```text axiom client download c1 --version 2 --lang python axiom client download c1 --version 2 --lang go -o ./sdks ``` Refuses to download — without touching disk — unless the version/language has already reached "succeeded". Check progress with: ```text axiom client status c1 --version 2 --watch ``` This command only writes the ZIP; it does not unzip or install it. See docs/guides/build-a-client-sdk.md for the per-language install step. Requires a prior "axiom login". ## Usage ```sh axiom client download [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for download | | `--lang` | | string | | Language to download (required) | | `--output` | `-o` | string | `.` | Directory to write the SDK zip to | | `--version` | | int | `0` | Version to download (required) | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client get {#reference/cli/axiom-client-get} > Show one Client Builder client # axiom client get Show one Client Builder client Show one Client Builder client: its members and build versions. may be the client's name, slug, or id. ```text axiom client get billing axiom client get billing --json # machine-readable output ``` Requires a prior "axiom login". ## Usage ```sh axiom client get [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for get | | `--json` | | bool | | Emit a single JSON result object | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client list {#reference/cli/axiom-client-list} > List your tenant's Client Builder clients # axiom client list List your tenant's Client Builder clients List every Client Builder client owned by your tenant. ```text axiom client list axiom client list --json # machine-readable output ``` --json always emits a JSON array, even when there are zero or exactly one client — a script or agent parsing the output can rely on one shape regardless of count. Requires a prior "axiom login". ## Usage ```sh axiom client list [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for list | | `--json` | | bool | | Emit a JSON array of clients (always an array, never a bare object) | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client members {#reference/cli/axiom-client-members} > List a client's members # axiom client members List a client's members List the nodes and flows currently bundled into a client's draft. ```text axiom client members c1 axiom client members c1 --json ``` Requires a prior "axiom login". ## Usage ```sh axiom client members [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for members | | `--json` | | bool | | Emit a JSON array of members | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client remove-member {#reference/cli/axiom-client-remove-member} > Remove a member from a client's draft # axiom client remove-member Remove a member from a client's draft Remove one member from a client's draft. This only affects future builds — an already-built version is never altered. ```text axiom client remove-member c1 m1 ``` Requires a prior "axiom login". ## Usage ```sh axiom client remove-member [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for remove-member | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client status {#reference/cli/axiom-client-status} > Show a client version's per-language build status # axiom client status Show a client version's per-language build status Show the build status of every language row for one version of a client. ```text axiom client status c1 --version 2 axiom client status c1 --version 2 --watch # poll until every language finishes axiom client status c1 --version 2 --json ``` --watch polls until every language row for the version reaches a terminal status (succeeded or failed), then exits. It exits non-zero if any language ended failed — --watch never reports success on a failed build, and it never polls forever: a canceled/expired context stops it too. Requires a prior "axiom login". ## Usage ```sh axiom client status [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for status | | `--json` | | bool | | Emit a JSON array of the version's build rows | | `--version` | | int | `0` | Version to show status for (required) | | `--watch` | | bool | | Poll until every language in the version reaches succeeded or failed | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom client sync {#reference/cli/axiom-client-sync} > Reconcile a client to match a declarative axiom-client.yaml manifest # axiom client sync Reconcile a client to match a declarative axiom-client.yaml manifest Reconcile a tenant-scoped Client Builder client to match a declarative manifest — kubectl-apply / Terraform semantics, not an imperative script. axiom-client.yaml lives at the root of your app's own repo: a package.json-style manifest of which PUBLISHED members — a package@version + node, or a saved flow's artifact id, from potentially many publishers — your client SDK should contain, plus their aliases and the languages to build: ```text client: billing description: Billing SDK languages: [python, go] members: - package: acme/billing-utils@1.2.0 node: Summarize alias: summarize - flow: 01J8Z3K9Q2R4S6T8V0W2X4Y6Z8 alias: run_pipeline axiom client sync # reads ./axiom-client.yaml axiom client sync --file path/to/f.yaml axiom client sync --json # machine-readable output ``` Each run reconciles the server to match the manifest exactly: ```text - creates the client (by its "client" name) if it doesn't exist yet - adds every manifest member missing from the server - removes every server member no longer listed in the manifest - builds every manifest language not yet covered by the client's current build state ``` A converged run — the manifest unchanged since the last sync — makes NO mutating calls: sync is idempotent, like re-applying an unchanged Kubernetes manifest or Terraform plan. An omitted package version pins @latest server-side; the resolved pin is printed so an unattended (e.g. CI or agent-driven) run stays reproducible. Requires a prior "axiom login". ## Usage ```sh axiom client sync [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--file` | | string | `axiom-client.yaml` | Path to the client manifest | | `--help` | `-h` | bool | | help for sync | | `--json` | | bool | | Emit a single JSON result object | ## See also - [axiom client](./axiom-client.md) — Create and manage Client Builder clients (typed SDKs) --- ## Reference > axiom create {#reference/cli/axiom-create} > Create Axiom resources # axiom create Create Axiom resources Create messages, nodes, and other Axiom package resources. ## Usage ```sh axiom create [flags] axiom create [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for create | ## Subcommands | Command | Description | |---|---| | [axiom create message](./axiom-create-message.md) | Scaffold a new protobuf message in messages/messages.proto | | [axiom create node](./axiom-create-node.md) | Scaffold a new node in nodes/ | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom create message](./axiom-create-message.md) — Scaffold a new protobuf message in messages/messages.proto - [axiom create node](./axiom-create-node.md) — Scaffold a new node in nodes/ --- ## Reference > axiom create message {#reference/cli/axiom-create-message} > Scaffold a new protobuf message in messages/messages.proto # axiom create message Scaffold a new protobuf message in messages/messages.proto Append a new message block to messages/messages.proto. All messages live in a single file so they can reference each other without any import statements. Each generated block includes: ```text • A detached HINTS block explaining proto syntax (ignored by the registry) • A leading doc-comment placeholder — edit this to document your message • Example placeholder fields showing correct syntax — replace or remove them ``` When --fields is provided the placeholder fields are replaced with the supplied field definitions and the HINTS block is omitted. This is useful for scripted or agent workflows where the field types are already known. Comments written directly above a message or field (no blank line between the comment and the declaration) are extracted at publish time and shown in the Axiom registry as documentation. After adding the message, axiom generate is run automatically to produce language bindings in gen/. Examples: ```text axiom create message OrderRequest axiom create message AddInput --fields "double a=1; double b=2" axiom create message MathResult --fields "double result=1" axiom create message ConvRequest --fields "session_id:string; user_message:string" ``` ## Usage ```sh axiom create message [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--fields` | | string | | Semicolon-separated field definitions. Accepts canonical proto3 ("string name = 1"), proto3 without field numbers ("string name"), or colon shorthand ("name:string"). Field numbers are auto-assigned when omitted. | | `--help` | `-h` | bool | | help for message | | `--no-generate` | | bool | | Skip running axiom generate after adding the message (useful when creating multiple messages before generating) | ## See also - [axiom create](./axiom-create.md) — Create Axiom resources - Guide: [Import package types](../../guides/import-package-types.md) --- ## Reference > axiom create node {#reference/cli/axiom-create-node} > Scaffold a new node in nodes/ # axiom create node Scaffold a new node in nodes/ Create a node implementation file and test file in nodes/, then update axiom.yaml. The node name must be PascalCase. Input and output messages must be defined in messages/messages.proto or available from an imported package in gen/imports/. If --input or --output are omitted and stdin is a terminal, the command will prompt interactively with a numbered list of all available messages. The comment block immediately above the function in the generated file is extracted by the publish pipeline and shown in the Axiom registry as this node's description — the same convention used for proto message leading comments. Edit the placeholder before publishing. Examples: ```text axiom create node ProcessOrder --input OrderRequest --output OrderConfirmation axiom create node ValidateOrder --input OrderRequest --output ValidationResult axiom create node ProcessOrder # interactive: prompts for input and output ``` ## Usage ```sh axiom create node [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--description` | | string | | Node description (optional, can be set later in axiom.yaml) | | `--generic-ports` | | string | | Mark this node Generic, naming which port(s) an Instance later maps onto a real type — "input", "output", or "input,output" (the port's own message should declare real, concrete, generalized fields — never zero fields) | | `--help` | `-h` | bool | | help for node | | `--input` | | string | | Input message name | | `--no-generate` | | bool | | Skip running axiom generate after scaffolding (useful when creating multiple nodes before generating) | | `--output` | | string | | Output message name | | `--type` | | string | | Node type: unary (default) or pipeline | ## See also - [axiom create](./axiom-create.md) — Create Axiom resources - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) --- ## Reference > axiom dev {#reference/cli/axiom-dev} > Start a local development server with hot reload # axiom dev Start a local development server with hot reload Start a local development server with hot reload. Generates the gRPC service (identical to the publish pipeline), compiles and runs it natively, then starts an HTTP bridge for easy testing with curl or any HTTP client. JSON payloads are automatically converted to and from Protobuf. Watches nodes/, messages/, and axiom.yaml for changes and recompiles/restarts the service automatically. Failed builds leave the previous service running so you always have a working server while you fix errors. Supported for all six languages. Go, Rust, Java, and C# use a rebuild-on-save-restart loop: each change recompiles the native artifact and restarts the process (there is no in-process hot-swap), so the first reload of a compiled package is slower while its toolchain warms up. Python and TypeScript run from source. ## Usage ```sh axiom dev [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for dev | | `--port` | `-p` | int | `8083` | HTTP bridge port | | `--socket` | | string | `/tmp/axiom.sock` | Unix socket path for gRPC service | | `--with-memory` | | bool | | Start the local Axiom memory service (requires PostgreSQL+pgvector via PG_CONN env var) | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) --- ## Reference > axiom doctor {#reference/cli/axiom-doctor} > Check the local toolchain needed to build and test Axiom packages # axiom doctor Check the local toolchain needed to build and test Axiom packages Check the local toolchain needed to build and test Axiom packages. Inside an Axiom package directory, checks the requirements for that package's language and exits non-zero if anything is missing. Outside a package, surveys all supported languages (informational; always exits zero). With --fix, runs the missing project-local installs for the package language: go mod download, go install of protoc plugins (to GOBIN), npm install in the project, or pip install -r requirements.txt into an ACTIVE virtualenv. System tools such as protoc and language runtimes are only advised — axiom never invokes a system package manager (apt, brew, …) and never escalates privileges. ## Usage ```sh axiom doctor [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--fix` | | bool | | Run missing project-local installs (never system package managers) | | `--help` | `-h` | bool | | help for doctor | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom feedback {#reference/cli/axiom-feedback} > Send Axiom a bug report, feature request, or general feedback # axiom feedback Send Axiom a bug report, feature request, or general feedback Send the Axiom team feedback about the product itself. ```text axiom feedback bug "the flow canvas loses my selection when I scroll" axiom feedback feature "let me pin a node's output panel while I edit others" axiom feedback general "marketplace search is great — I just forget to use it" ``` The message may be given as arguments or piped on stdin: ```text echo "…" | axiom feedback bug ``` To ask for a marketplace artifact that does not exist yet — a package, node, or flow — use "axiom propose" instead. Requires a prior "axiom login". ## Usage ```sh axiom feedback [message] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for feedback | | `--json` | | bool | | Print the persisted submission as JSON | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom flow {#reference/cli/axiom-flow} > Author and compile flows (graphs of published nodes) # axiom flow Author and compile flows (graphs of published nodes) Author and compile Axiom flows from a flow.yaml. A flow.yaml composes published nodes (referenced by package + node name) into a graph. The usual loop: ```text axiom flow new my-flow # scaffold my-flow.flow.yaml # edit it: add nodes + wire edges axiom flow validate my-flow.flow.yaml # structural checks axiom flow layout my-flow.flow.yaml # auto-position on the canvas grid axiom flow compile my-flow.flow.yaml # resolve nodes + compile a runnable flow ``` Node positions are coarse grid cells (col, row); omit them to auto-layout. ## Usage ```sh axiom flow [flags] axiom flow [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for flow | ## Subcommands | Command | Description | |---|---| | [axiom flow compile](./axiom-flow-compile.md) | Resolve nodes, lay out, and compile a runnable flow artifact | | [axiom flow layout](./axiom-flow-layout.md) | Auto-position nodes on the grid and write col/row back | | [axiom flow mapping-scope](./axiom-flow-mapping-scope.md) | Show the CEL variables and functions in scope for an edge mapping | | [axiom flow new](./axiom-flow-new.md) | Scaffold a starter flow.yaml | | [axiom flow preview](./axiom-flow-preview.md) | Preview the exact value(s) a mapping produces for sample input | | [axiom flow publish](./axiom-flow-publish.md) | Publish a flow document to the public marketplace | | [axiom flow pull](./axiom-flow-pull.md) | Materialize an existing compiled flow into an editable flow.yaml | | [axiom flow run](./axiom-flow-run.md) | Invoke a compiled flow and print its result | | [axiom flow save](./axiom-flow-save.md) | Save a flow as an editor document openable in the visual editor | | [axiom flow validate](./axiom-flow-validate.md) | Validate a flow.yaml's structure (local checks) | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom flow compile](./axiom-flow-compile.md) — Resolve nodes, lay out, and compile a runnable flow artifact - [axiom flow layout](./axiom-flow-layout.md) — Auto-position nodes on the grid and write col/row back - [axiom flow mapping-scope](./axiom-flow-mapping-scope.md) — Show the CEL variables and functions in scope for an edge mapping - [axiom flow new](./axiom-flow-new.md) — Scaffold a starter flow.yaml - [axiom flow preview](./axiom-flow-preview.md) — Preview the exact value(s) a mapping produces for sample input - [axiom flow publish](./axiom-flow-publish.md) — Publish a flow document to the public marketplace - [axiom flow pull](./axiom-flow-pull.md) — Materialize an existing compiled flow into an editable flow.yaml - [axiom flow run](./axiom-flow-run.md) — Invoke a compiled flow and print its result - [axiom flow save](./axiom-flow-save.md) — Save a flow as an editor document openable in the visual editor - [axiom flow validate](./axiom-flow-validate.md) — Validate a flow.yaml's structure (local checks) --- ## Reference > axiom flow compile {#reference/cli/axiom-flow-compile} > Resolve nodes, lay out, and compile a runnable flow artifact # axiom flow compile Resolve nodes, lay out, and compile a runnable flow artifact Compile a flow.yaml into a runnable flow artifact: ```text 1. validate the file structurally 2. resolve each node's package + node name to a registry node ULID and its input/output message types 3. auto-layout any unpinned nodes on the grid 4. POST the assembled graph to the registry compiler ``` Requires a prior "axiom login". Prints the compiled artifact id. Defaults to ./flow.yaml. ## Usage ```sh axiom flow compile [flow.yaml] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for compile | | `--json` | | bool | | Emit a single JSON result object | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow layout {#reference/cli/axiom-flow-layout} > Auto-position nodes on the grid and write col/row back # axiom flow layout Auto-position nodes on the grid and write col/row back Run the coarse-grid layout engine over the flow's topology and write the resulting (col, row) cell onto every node, so you can see and tweak the placement. Pinned nodes keep their cell. Defaults to ./flow.yaml. ## Usage ```sh axiom flow layout [flow.yaml] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for layout | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow mapping-scope {#reference/cli/axiom-flow-mapping-scope} > Show the CEL variables and functions in scope for an edge mapping # axiom flow mapping-scope Show the CEL variables and functions in scope for an edge mapping Introspect the CEL authoring surface before writing a mapping: which variables are in scope for a concrete edge, and which functions/macros the platform's CEL environment provides. With --edge, prints the edge's in-scope variables with their types: ```text axiom flow mapping-scope --edge from:to ``` For a plain single-source edge that is "value" (the piped input) plus every scalar field of the source message, bound by name, plus every repeated *message* field (typed as a dynamic value) whose indexed nested scalars are addressable as "field[0]." — so an edge over a source with a repeated "regions" message field lists "regions[0].x" and its sibling scalar leaves as in-scope, letting an adapter address one element's scalar ("regions[0].x"). Singular nested message fields and repeated scalar fields are not surfaced on this single-source path. When the edge's target joins several sources (a compose mapping or multiple incoming edges), the typed "inputs.." leaves of every incident edge are also in scope — note an edge id that is not a legal CEL identifier (e.g. containing hyphens) cannot be referenced that way and is omitted, exactly as the compiler omits it. Without --edge, prints just the function manifest. The function manifest is fetched from the registry (GET /v1/cel/manifest) and is derived from the live CEL environment — the same surface the compiler and worker evaluate, versioned by env_version. --json emits the machine contract {"edge","variables","manifest"} for agents; recommended loop: mapping-scope -> author -> "axiom flow preview" -> compile. Requires a prior "axiom login". Defaults to ./flow.yaml. ## Usage ```sh axiom flow mapping-scope [flow.yaml] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--edge` | | string | | Edge to introspect, given as "from:to" node aliases | | `--help` | `-h` | bool | | help for mapping-scope | | `--json` | | bool | | Emit the machine-readable {edge, variables, manifest} object | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow new {#reference/cli/axiom-flow-new} > Scaffold a starter flow.yaml # axiom flow new Scaffold a starter flow.yaml Scaffold a starter .flow.yaml with two example nodes and an edge. Edit it to set each node's package + node name (find them with "axiom search --json") and wire the edges, then validate/layout/compile. ## Usage ```sh axiom flow new [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for new | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow preview {#reference/cli/axiom-flow-preview} > Preview the exact value(s) a mapping produces for sample input # axiom flow preview Preview the exact value(s) a mapping produces for sample input Preview a flow.yaml edge adapter or compose-consumer mapping against sample source value(s), without a live invoke. Runs the SAME mapping engine (PrepareAdapter/ExecuteAdapter, CEL, transforms) the registry compiler and worker use at real dispatch time — the output is the exact value your mapping produces, not an approximation. Preview a plain edge's adapter (a single source, "from" node's alias, to a "to" node's alias): ```text axiom flow preview --edge from:to --sample source.json ``` source.json is one JSON object shaped like the "from" node's output message. Preview a compose-consumer node's compose mapping (one or more incident edges joined into a single input): ```text axiom flow preview --node consumer_alias --sample sources.json ``` sources.json is a JSON object keyed by each incident edge's SOURCE node alias: ```text {"branch_a": {...}, "branch_b": {...}} ``` --sample defaults to stdin when omitted. Requires a prior "axiom login" and a flow.yaml whose nodes/edges resolve against the live registry (same resolution "axiom flow compile" performs) — the flow does not need to be compiled or saved first. ## Usage ```sh axiom flow preview [flow.yaml] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--edge` | | string | | Preview a plain edge's adapter, given as "from:to" node aliases | | `--help` | `-h` | bool | | help for preview | | `--json` | | bool | | Emit a single JSON result object | | `--node` | | string | | Preview a compose-consumer node's compose mapping, given its alias | | `--sample` | | string | | Path to a JSON file with sample source value(s) (defaults to stdin) | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow publish {#reference/cli/axiom-flow-publish} > Publish a flow document to the public marketplace # axiom flow publish Publish a flow document to the public marketplace Publish a flow document — the GRAPH artifact id printed by "axiom flow save" — to the public marketplace. Publishing sets the flow's visibility to MARKETPLACE_FREE: every Axiom tenant can then discover it (it becomes searchable), fork it, and import it as a subflow. This is CLI parity with the editor's Publish action, which publishes the saved flow document. Pass the id from "axiom flow save", NOT the id from "axiom flow compile": the compile id is a runtime snapshot (COMPILED_GRAPH) that never appears in catalog search or saved-flows listings, so publishing it would strand the flow invisible. The publish endpoint rejects a compiled-snapshot id for that reason. ```text axiom flow save my-flow.flow.yaml # prints the GRAPH document id axiom flow publish 01J… # publish that id axiom flow publish 01J… --yes # skip the prompt (CI) ``` Publishing is immutable — a published flow cannot be edited or unpublished. To iterate, edit the flow and "axiom flow save" a new document, then publish it. Requires a prior "axiom login". ## Usage ```sh axiom flow publish [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for publish | | `--json` | | bool | | Emit a single JSON result object | | `--yes` | `-y` | bool | | Skip the confirmation prompt (for CI and scripted use) | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow pull {#reference/cli/axiom-flow-pull} > Materialize an existing compiled flow into an editable flow.yaml # axiom flow pull Materialize an existing compiled flow into an editable flow.yaml Pull an existing flow artifact's stored SourceGraph and write it back out as a flow.yaml you can edit and re-compile — the inverse of "axiom flow compile". The UI edits existing flows; this lets the CLI do the same. Requires a prior "axiom login". Writes ./flow.yaml (or -o ). ## Usage ```sh axiom flow pull [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for pull | | `--out` | `-o` | string | `flow.yaml` | Output flow.yaml path | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow run {#reference/cli/axiom-flow-run} > Invoke a compiled flow and print its result # axiom flow run Invoke a compiled flow and print its result Invoke a compiled flow — the artifact id printed by "axiom flow compile" — and wait for the result. Pass the start node's input as JSON with -d; the ingress transcodes it to protobuf automatically, and the result is decoded back to JSON. Requires a prior "axiom login". ```text axiom flow run 01J… -d '{"value":"hello"}' ``` ## Usage ```sh axiom flow run [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--data` | `-d` | string | `{}` | JSON input for the start node, e.g. -d '{"value":"hi"}' | | `--help` | `-h` | bool | | help for run | | `--json` | | bool | | Emit the full JSON response | | `--timeout` | | uint32 | `60` | Seconds to wait for the flow to complete | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow save {#reference/cli/axiom-flow-save} > Save a flow as an editor document openable in the visual editor # axiom flow save Save a flow as an editor document openable in the visual editor Save a flow.yaml as an editable flow document (a type GRAPH artifact): ```text 1. validate the file structurally 2. resolve each node's package + node name to a registry node ULID and its input/output message types (same assembly as compile) 3. auto-layout any unpinned nodes on the grid 4. POST the assembled SourceGraph to the registry as a GRAPH document ``` Unlike "axiom flow compile" — which produces a runnable COMPILED_GRAPH snapshot that is intentionally hidden from the saved-flows list — "save" creates the GRAPH document the visual editor lists in File > Open, so a CLI-authored flow becomes openable and editable in the UI. Run it with "axiom flow compile" + "axiom flow run". Requires a prior "axiom login". Prints the graph document id. Defaults to ./flow.yaml. ## Usage ```sh axiom flow save [flow.yaml] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for save | | `--json` | | bool | | Emit a single JSON result object | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom flow validate {#reference/cli/axiom-flow-validate} > Validate a flow.yaml's structure (local checks) # axiom flow validate Validate a flow.yaml's structure (local checks) Run local structural validation on a flow.yaml: required fields, unique node aliases, well-formed package references, edges that reference known nodes, and well-formed pins. Node existence and edge type-compatibility are checked at compile time against the registry. Defaults to ./flow.yaml. ## Usage ```sh axiom flow validate [flow.yaml] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for validate | | `--json` | | bool | | Emit structured JSON instead of human-readable output | ## See also - [axiom flow](./axiom-flow.md) — Author and compile flows (graphs of published nodes) --- ## Reference > axiom generate {#reference/cli/axiom-generate} > Compile .proto files into language bindings # axiom generate Compile .proto files into language bindings Compile all .proto files in messages/ into language bindings in gen/. Reads axiom.yaml for the target language and shells out to protoc with the appropriate flags. Requires protoc and the language-specific protoc plugin to be installed. Run automatically as part of: axiom dev, axiom test, axiom build. ## Usage ```sh axiom generate [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for generate | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) - Guide: [Import package types](../../guides/import-package-types.md) --- ## Reference > axiom import {#reference/cli/axiom-import} > Import proto definitions from a published package # axiom import Import proto definitions from a published package Download .proto files from a published Axiom package into gen/imports/. The imported message types become available for use as node inputs/outputs in this package. An entry is added to imports: in axiom.yaml, and axiom generate is run so the IDE and compiler immediately see the new types. When @version is omitted, the most recently published version is imported — UNLESS this package already pins a different version of the same dependency in axiom.yaml. In that case the bare import is REFUSED (nothing is downloaded or changed) so an existing pin never silently drifts to a newer "latest". To re-pin on purpose, pass the version explicitly (axiom import @); to keep the current pin, re-run with that exact version. A bare re-import of the already-pinned version is allowed and just refreshes its imported message list. Requires a prior "axiom login". Examples: ```text axiom import payments # first import, or refresh the pinned version axiom import payments@2.0.1 # pin, or deliberately re-pin, an exact version ``` ## Usage ```sh axiom import [@version] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for import | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Import package types](../../guides/import-package-types.md) --- ## Reference > axiom info {#reference/cli/axiom-info} > Show package details, nodes, messages, and live endpoint # axiom info Show package details, nodes, messages, and live endpoint Display full details for a published Axiom package. When @version is omitted, the most recently published version is shown. Use --json for the full package detail (nodes, messages, endpoint) as JSON. Examples: ```text axiom info order-processing axiom info payments@2.0.1 --json ``` ## Usage ```sh axiom info [@version] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for info | | `--json` | | bool | | Emit structured JSON instead of human-readable output | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Import package types](../../guides/import-package-types.md) - Guide: [Use the interactive API docs](../../guides/use-interactive-api-docs.md) --- ## Reference > axiom init {#reference/cli/axiom-init} > Initialize a new Axiom package # axiom init Initialize a new Axiom package Initialize a new Axiom package. Creates a subdirectory named after the package (the part after the last "/"), then writes axiom.yaml, the standard directory layout, and a .gitignore. ```text axiom init axiom-official/axiom-conv-ai --language python # Creates ./axiom-conv-ai/ with axiom.yaml, messages/, nodes/, gen/ ``` For Go packages, also generates a go.mod file. ## Usage ```sh axiom init [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--description` | | string | | Package description (written to axiom.yaml) | | `--help` | `-h` | bool | | help for init | | `--install` | | bool | | After scaffolding, run the project-local dependency install for the language (never system package managers) | | `--language` | `-l` | string | `go` | package language (go \| python \| rust \| java \| typescript \| csharp) | | `--no-agent-guide` | | bool | | Skip writing the CLAUDE.md agent-authoring guide into the package | | `--no-example-comment` | | bool | | Omit the commented example node block from axiom.yaml (useful for scripted/agent use) | | `--version` | | string | `0.1.0` | Initial package version | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) --- ## Reference > axiom inspect {#reference/cli/axiom-inspect} > Print the compiled JSON Schema for a node or flow (the shape an invoke must satisfy) # axiom inspect Print the compiled JSON Schema for a node or flow (the shape an invoke must satisfy) Print the compiled input/output schema for a published node or a compiled flow — the same schema the hosted MCP's axiom_inspect returns, and the shape an invoke payload must satisfy. ```text axiom inspect node //[@version] axiom inspect flow ``` The schema JSON is printed to stdout exactly as returned (pipes cleanly into jq); all diagnostics go to stderr. Requires a prior "axiom login". ## Usage ```sh axiom inspect [flags] axiom inspect [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for inspect | ## Subcommands | Command | Description | |---|---| | [axiom inspect flow](./axiom-inspect-flow.md) | Print a compiled flow's OpenAPI spec (its input schema) | | [axiom inspect node](./axiom-inspect-node.md) | Print a published node's compiled input/output JSON Schema | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom inspect flow](./axiom-inspect-flow.md) — Print a compiled flow's OpenAPI spec (its input schema) - [axiom inspect node](./axiom-inspect-node.md) — Print a published node's compiled input/output JSON Schema --- ## Reference > axiom inspect flow {#reference/cli/axiom-inspect-flow} > Print a compiled flow's OpenAPI spec (its input schema) # axiom inspect flow Print a compiled flow's OpenAPI spec (its input schema) Fetch a compiled flow's OpenAPI spec (GET /graphs/{id}/openapi.json) — the same spec the MCP's axiom_inspect returns for a flow. The artifact id is the one "axiom flow compile" prints (also accepted by "axiom flow run"). ```text axiom inspect flow 01HQY5K7XWZ6N8R2BTEF4... | jq . ``` ## Usage ```sh axiom inspect flow [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for flow | ## See also - [axiom inspect](./axiom-inspect.md) — Print the compiled JSON Schema for a node or flow (the shape an invoke must satisfy) --- ## Reference > axiom inspect node {#reference/cli/axiom-inspect-node} > Print a published node's compiled input/output JSON Schema # axiom inspect node Print a published node's compiled input/output JSON Schema Fetch a published node's compiled input/output JSON Schema (GET /nodes/{id}/schema.json) — the same schema the MCP's axiom_inspect returns for a node. When @version is omitted, the most recently published version is used. ```text axiom inspect node nadia/summarizer/Summarize axiom inspect node nadia/summarizer/Summarize@0.1.0 | jq . ``` ## Usage ```sh axiom inspect node //[@version] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for node | ## See also - [axiom inspect](./axiom-inspect.md) — Print the compiled JSON Schema for a node or flow (the shape an invoke must satisfy) --- ## Reference > axiom instance {#reference/cli/axiom-instance} > Create Instances from a Generic node (type-bind, no build, no deploy) # axiom instance Create Instances from a Generic node (type-bind, no build, no deploy) Create an Instance: a Generic node's generalized port(s) mapped onto a real, specific type via a field mapping. An Instance carries no code of its own — it shares the Generic node's already-built, already-deployed image, so creating one is a pure type binding: no git commit, no build, no Knative deploy. ```text axiom instance create your-handle/orders-query \ --generic-package acme/postgres-query --generic-node "Postgres Query" \ --output-message OrderSummary --output-field id=int64 --output-field total=double ``` A facade field can also be a nested (not-yet-real) message, recursively, via --output-fields-file/--input-fields-file; "axiom instance preview" previews such a mapping's output before you commit to a shape. Requires a prior "axiom login". ## Usage ```sh axiom instance [flags] axiom instance [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for instance | ## Subcommands | Command | Description | |---|---| | [axiom instance create](./axiom-instance-create.md) | Create a new Instance from a Generic node | | [axiom instance preview](./axiom-instance-preview.md) | Preview the exact output a mapping produces, including not-yet-real (drafted) messages | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom instance create](./axiom-instance-create.md) — Create a new Instance from a Generic node - [axiom instance preview](./axiom-instance-preview.md) — Preview the exact output a mapping produces, including not-yet-real (drafted) messages --- ## Reference > axiom instance create {#reference/cli/axiom-instance-create} > Create a new Instance from a Generic node # axiom instance create Create a new Instance from a Generic node Create an Instance: map a Generic node's generalized port(s) onto a real, specific type via a field mapping. There is no build and no Knative deploy — the Instance shares the Generic node's already-deployed image. is the Instance's scoped name ("your-handle/instance-name") — it becomes the shell package name and, unqualified, the instance node name. At least one of --output-message or --input-message is required, matching whichever port(s) the Generic node declared generic; each bound message needs at least one --output-field/--input-field as name=kind (repeatable, or comma-separated in one flag: "id=int64,total=double"). ```text axiom instance create acme/orders-query \ --generic-package acme/postgres-query --generic-node "Postgres Query" \ --output-message OrderSummary --output-field id=int64 --output-field total=double axiom instance create acme/orders-query --json # machine-readable output ``` For anything beyond a flat scalar field set — repeated/optional modifiers, or a field whose type is itself a nested (not-yet-real) message, recursively — use --output-fields-file/--input-fields-file instead: a path to a JSON file shaped {"message_name": "...", "fields": {...}}, where a field value is either a bare scalar-kind string (as above) or an object like {"kind": "string", "repeated": true} or {"kind": "message", "message": {...}}. --output-fields-file is mutually exclusive with --output-message/--output-field on the SAME port (pick one shape of input); --input-fields-file is likewise exclusive with --input-message/--input-field. The two ports are independent. ```text axiom instance create acme/orders-query \ --generic-package acme/postgres-query --generic-node "Postgres Query" \ --output-fields-file ./order-summary.fields.json ``` Give a port an authored CEL mapping — exactly the CEL an ordinary edge uses — with --output-map/--input-map as dst-field=expression pairs (repeatable). The output map reshapes the node's generic output INTO the facade; the input map reshapes the facade INTO the node's generic input. A mapping applies to the port it names, which must be bound. A fields-file may also carry a top-level "mapping" object; an explicit --output-map/--input-map flag overrides it. ```text axiom instance create acme/orders-query \ --generic-package acme/postgres-query --generic-node "Postgres Query" \ --output-message OrderSummary --output-field id=int64 --output-field total=double \ --output-map id=row_id --output-map total="amount * 1.1" ``` Requires a prior "axiom login". ## Usage ```sh axiom instance create [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--generic-node` | | string | | Name of the Generic node within the package (required) | | `--generic-package` | | string | | Generic node's package, scoped as handle/name (required) | | `--generic-version` | | string | | Generic package version to pin (default: resolve and pin "latest") | | `--help` | `-h` | bool | | help for create | | `--input-field` | | stringToString | `[]` | Input facade field as name=kind (repeatable) | | `--input-fields-file` | | string | | Path to a JSON {message_name,fields} draft for the input facade (mutually exclusive with --input-message/--input-field) | | `--input-map` | | stringToString | `[]` | Input-port CEL mapping as dst-field=CEL-expression (repeatable); reshapes the facade into the generic input | | `--input-message` | | string | | Name for the synthesized input facade message | | `--json` | | bool | | Emit a single JSON result object | | `--output-field` | | stringToString | `[]` | Output facade field as name=kind (repeatable) | | `--output-fields-file` | | string | | Path to a JSON {message_name,fields} draft for the output facade (mutually exclusive with --output-message/--output-field) | | `--output-map` | | stringToString | `[]` | Output-port CEL mapping as dst-field=CEL-expression (repeatable); reshapes the generic output into the facade | | `--output-message` | | string | | Name for the synthesized output facade message | ## See also - [axiom instance](./axiom-instance.md) — Create Instances from a Generic node (type-bind, no build, no deploy) --- ## Reference > axiom instance preview {#reference/cli/axiom-instance-preview} > Preview the exact output a mapping produces, including not-yet-real (drafted) messages # axiom instance preview Preview the exact output a mapping produces, including not-yet-real (drafted) messages Preview an /adapters/preview mapping against sample source value(s), without a live invoke — wraps POST /adapters/preview verbatim. Runs the SAME mapping engine (PrepareAdapter/ExecuteAdapter, CEL, transforms) the registry compiler and worker use at real dispatch time. The request body is read from --request-file (or stdin) and forwarded unmodified. It must match the registry's adapterPreviewRequest shape: ```text { "dst_msg_name": "OrderSummary", "sources": [ {"edge_id": "e1", "msg_name": "Order", "sample": {"id": 1}} ], "adapter": {"total": "$.amount"} } ``` Any "msg_name" (dst_msg_name or a sources[] entry) may be a bare registry message name (a JSON string, as above) OR an inline, not-yet-real draft — the IDENTICAL {"message_name": ..., "fields": {...}} shape "axiom instance create"'s --output-fields-file/--input-fields-file use — in place of the string, letting you preview a mapping against a message you haven't created yet: ```text {"dst_msg_name": {"message_name": "OrderSummary", "fields": {"total": "double"}}, ...} ``` Exactly one of "adapter" (single-edge DSL) or "compose_mapping" (edge_adapters + compose_plan, for a multi-source compose consumer) is required. ```text axiom instance preview --request-file ./preview-request.json axiom instance preview --json # machine-readable output, reads stdin ``` Requires a prior "axiom login". ## Usage ```sh axiom instance preview [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for preview | | `--json` | | bool | | Emit a single JSON result object | | `--request-file` | | string | | Path to a JSON /adapters/preview request body (defaults to stdin) | ## See also - [axiom instance](./axiom-instance.md) — Create Instances from a Generic node (type-bind, no build, no deploy) --- ## Reference > axiom invoke {#reference/cli/axiom-invoke} > Invoke a published node directly and print its raw JSON response # axiom invoke Invoke a published node directly and print its raw JSON response Invoke a published node directly — the same endpoint "axiom push" prints after a successful push — and print its raw JSON response body to stdout. Provide the node's input JSON with exactly one of: ```text --input '' inline JSON, e.g. --input '{"text":"hello world"}' --input-file read JSON from a file piped stdin echo '{"text":"hello world"}' | axiom invoke ... ``` When @version is omitted, the most recently published version is used. Input size: the request body is capped at 16 MiB (the platform run-payload cap). Larger inputs are rejected before the node runs. Binary inputs (e.g. an image for a resize/convert node) are base64-encoded inside the JSON, which inflates them by ~33%, so a raw file up to ~12 MiB fits. A typical phone photo (1–5 MB) is well within the limit and does not need to be pre-shrunk. The response body is printed to stdout exactly as returned, so it pipes cleanly into jq; all diagnostics and status messages go to stderr. Exits non-zero on a node error or non-2xx HTTP status, 0 on success. Requires a prior "axiom login". ```text axiom invoke nadia/summarizer/Summarize --input '{"text":"hello world"}' ``` ## Usage ```sh axiom invoke //[@version] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for invoke | | `--input` | | string | | Inline JSON input, e.g. --input '{"value":"hi"}' | | `--input-file` | | string | | Path to a file containing JSON input | | `--timeout` | | uint32 | `30` | Seconds to wait for the node to respond | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom list-my-tools {#reference/cli/axiom-list-my-tools} > List the nodes and flows you have pinned # axiom list-my-tools List the nodes and flows you have pinned List the nodes and flows you have pinned as favorite tools. These are the same pinned tools the hosted Axiom MCP server exposes as first-class typed tools — the list is your tenant+client's shared tool store. ```text axiom list-my-tools axiom list-my-tools --kind node axiom list-my-tools --json ``` Requires a prior "axiom login". ## Usage ```sh axiom list-my-tools [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for list-my-tools | | `--json` | | bool | | Emit structured JSON instead of a human-readable table | | `--kind` | | string | | filter to one tool kind: node or flow (default: both) | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom login {#reference/cli/axiom-login} > Authenticate with the Axiom platform # axiom login Authenticate with the Axiom platform Authenticate with the Axiom platform using the OAuth Device Flow. Opens a browser to complete authentication via GitHub or Google. The resulting API key is stored in ~/.axiom/credentials. In CI environments, set AXIOM_API_KEY to skip the browser flow entirely. Use "axiom whoami" to confirm your current identity. ## Usage ```sh axiom login [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for login | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [API keys](../../guides/api-keys.md) --- ## Reference > axiom pin {#reference/cli/axiom-pin} > Pin a node or flow as a favorite tool # axiom pin Pin a node or flow as a favorite tool Pin a published node or compiled flow as a favorite. Pins live in your tenant+client's shared tool store — the SAME store the hosted Axiom MCP server reads — so a tool pinned here surfaces as a first-class typed tool there too, and vice versa. ```text axiom pin node nadia/summarizer/Summarize@0.1.0 axiom pin flow 01HQY5K7XWZ6N8R2BTEF4... ``` Pinning is idempotent: re-pinning an already-pinned target is a no-op. Requires a prior "axiom login" (pins are scoped to your identity). ## Usage ```sh axiom pin [flags] axiom pin [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for pin | ## Subcommands | Command | Description | |---|---| | [axiom pin flow](./axiom-pin-flow.md) | Pin a flow as a favorite tool | | [axiom pin node](./axiom-pin-node.md) | Pin a node as a favorite tool | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom pin flow](./axiom-pin-flow.md) — Pin a flow as a favorite tool - [axiom pin node](./axiom-pin-node.md) — Pin a node as a favorite tool --- ## Reference > axiom pin flow {#reference/cli/axiom-pin-flow} > Pin a flow as a favorite tool # axiom pin flow Pin a flow as a favorite tool ## Usage ```sh axiom pin flow [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for flow | ## See also - [axiom pin](./axiom-pin.md) — Pin a node or flow as a favorite tool --- ## Reference > axiom pin node {#reference/cli/axiom-pin-node} > Pin a node as a favorite tool # axiom pin node Pin a node as a favorite tool ## Usage ```sh axiom pin node [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for node | ## See also - [axiom pin](./axiom-pin.md) — Pin a node or flow as a favorite tool --- ## Reference > axiom propose {#reference/cli/axiom-propose} > Propose a new package, node, or flow for the Axiom marketplace # axiom propose Propose a new package, node, or flow for the Axiom marketplace Ask the Axiom team to build a marketplace artifact that does not exist yet. ```text axiom propose node "refund a Stripe charge by id, returning the refund" axiom propose package "first-class Notion connector (query + append)" axiom propose flow "new-signup onboarding: create user, email, seed workspace" ``` Use --details for the longer shape (inputs/outputs, an example), and --for to point at an existing artifact the proposal is against (e.g. "add pagination to this package"): ```text axiom propose node "add pagination" --for acme/notion \ --details "page through database rows with a cursor" ``` The summary may be given as arguments or piped on stdin. Proposals go to the Axiom review queue. Requires a prior "axiom login". ## Usage ```sh axiom propose [summary] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--details` | `-d` | string | | Longer description (inputs/outputs, an example) appended to the summary | | `--for` | | string | | An existing artifact the proposal is against (e.g. handle/package) | | `--help` | `-h` | bool | | help for propose | | `--json` | | bool | | Print the persisted submission as JSON | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom publish {#reference/cli/axiom-publish} > Publish a pushed package to the public marketplace # axiom publish Publish a pushed package to the public marketplace Publish a pushed package to the public marketplace. Publishing flips a package you previously deployed with "axiom push" from tenant-private to publicly visible. The package must already be pushed and owned by your tenant; publishing deploys nothing new. Making a version public is immutable — you cannot unpublish or overwrite it. Iterate privately with "axiom push" (which may overwrite the same version), then publish when you are satisfied. ```text axiom publish my-handle/my-package@0.1.0 axiom publish my-handle/my-package@0.1.0 --yes # skip the prompt (CI) ``` Requires a prior "axiom login". ## Usage ```sh axiom publish @ [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for publish | | `--yes` | `-y` | bool | | Skip the confirmation prompt (for CI and scripted use) | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom push {#reference/cli/axiom-push} > Push the package to the Axiom platform (tenant-private) # axiom push Push the package to the Axiom platform (tenant-private) Validate the package locally, then push it to the Axiom platform. Unlike publishing, a pushed package is only visible to your own tenant — it does not appear in the public marketplace. You can push the same version repeatedly to iterate on it deployed before making it public: each re-push refreshes the code, the package and per-node descriptions, and the OpenAPI spec from your source. One exception protects anyone who has already composed a deployed node into a graph: once a node is deployed, its input/output WIRE SIGNATURE is frozen for that version. A same-version push that changes a node's input/output message type — or the shape of a local message it uses — is rejected, and you must bump the version in axiom.yaml to publish the new contract. Descriptions, docs, and code are never frozen; only the wire signature is. When you are satisfied with the package, publish it through the Axiom UI to make it available to others as an immutable versioned release. Requires a prior "axiom login". The current HEAD must be pushed to the remote. ## Usage ```sh axiom push [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--allow-dirty` | | bool | | Skip the uncommitted/unpushed working tree check and push anyway | | `--help` | `-h` | bool | | help for push | | `--json` | | bool | | Emit a single JSON result object instead of human-readable progress output | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Use the interactive API docs](../../guides/use-interactive-api-docs.md) - Guide: [Manage secrets in a flow](../../guides/manage-secrets.md) --- ## Reference > axiom remove {#reference/cli/axiom-remove} > Remove Axiom resources # axiom remove Remove Axiom resources Remove nodes and other Axiom package resources, with confirmation. ## Usage ```sh axiom remove [flags] axiom remove [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for remove | ## Subcommands | Command | Description | |---|---| | [axiom remove node](./axiom-remove-node.md) | Remove a node from nodes/ and axiom.yaml | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom remove node](./axiom-remove-node.md) — Remove a node from nodes/ and axiom.yaml --- ## Reference > axiom remove node {#reference/cli/axiom-remove-node} > Remove a node from nodes/ and axiom.yaml # axiom remove node Remove a node from nodes/ and axiom.yaml Delete the node implementation file and test file from nodes/, then remove the node entry from axiom.yaml. A confirmation prompt is shown before any files are deleted. Use --force to skip the prompt in scripts or CI environments. If either file is missing from disk (e.g. it was already deleted manually), a warning is printed but the operation continues — the axiom.yaml entry is still removed. Examples: ```text axiom remove node ProcessOrder axiom remove node ProcessOrder --force ``` ## Usage ```sh axiom remove node [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--force` | | bool | | Skip confirmation prompt | | `--help` | `-h` | bool | | help for node | ## See also - [axiom remove](./axiom-remove.md) — Remove Axiom resources --- ## Reference > axiom search {#reference/cli/axiom-search} > Search the Axiom package marketplace # axiom search Search the Axiom package marketplace Search for packages, nodes, flows, or messages in the Axiom marketplace. When called without a query, lists recently published packages. Use --type to search nodes, flows, or messages instead of packages. Use --json for machine-readable output (e.g. to find a message to import). Examples: ```text axiom search axiom search order-processing axiom search --type nodes "validate order" axiom search --type flows "user signup" axiom search --type messages Payment --json ``` ## Usage ```sh axiom search [query] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for search | | `--json` | | bool | | Emit structured JSON instead of human-readable tables | | `--type` | `-t` | string | `packages` | entity type to search: packages, nodes, flows, or messages | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Import package types](../../guides/import-package-types.md) --- ## Reference > axiom skills {#reference/cli/axiom-skills} > Manage the Axiom Skills for Claude Code # axiom skills Manage the Axiom Skills for Claude Code Install the Axiom Skills for Claude Code. Claude Code reads "Skills" (SKILL.md files under a .claude/skills/ directory) to learn how to drive a tool. Axiom ships five — four for authoring, one for consuming: ```text axiom-package-authoring the create→validate→dev→push→publish loop for a node package axiom-flow-authoring the new→validate→compile→run→publish loop for a flow axiom-client-authoring the axiom-client.yaml→sync→download loop for pulling a typed client SDK into another codebase axiom-instance-authoring the declare-binding→create→reference-in-a-flow loop for binding a Generic node's placeholder port(s) to a real type axiom-consume the search→inspect→source→invoke→pin loop for finding, verifying, and calling already-published nodes and flows ``` "axiom skills install" writes them into your Claude Code skills directory so your agent is oriented the moment you open a project. ## Usage ```sh axiom skills [flags] axiom skills [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for skills | ## Subcommands | Command | Description | |---|---| | [axiom skills install](./axiom-skills-install.md) | Install the Axiom authoring Skills into a Claude Code skills directory | | [axiom skills list](./axiom-skills-list.md) | List the Skills bundled in this CLI | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom skills install](./axiom-skills-install.md) — Install the Axiom authoring Skills into a Claude Code skills directory - [axiom skills list](./axiom-skills-list.md) — List the Skills bundled in this CLI --- ## Reference > axiom skills install {#reference/cli/axiom-skills-install} > Install the Axiom authoring Skills into a Claude Code skills directory # axiom skills install Install the Axiom authoring Skills into a Claude Code skills directory Install the embedded Axiom authoring Skills into a Claude Code skills directory. By default the Skills are written to ./.claude/skills (project-local, so they travel with the repo). Use --global to install them into ~/.claude/skills for every project, or --dir to choose an explicit target. ```text axiom skills install # ./.claude/skills axiom skills install --global # ~/.claude/skills axiom skills install --dir ./skills # an explicit directory axiom skills install --force # overwrite existing copies ``` ## Usage ```sh axiom skills install [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--dir` | | string | | Explicit target directory (overrides the default and --global) | | `--force` | `-f` | bool | | Overwrite a skill that already exists in the target | | `--global` | | bool | | Install into ~/.claude/skills instead of ./.claude/skills | | `--help` | `-h` | bool | | help for install | ## See also - [axiom skills](./axiom-skills.md) — Manage the Axiom Skills for Claude Code --- ## Reference > axiom skills list {#reference/cli/axiom-skills-list} > List the Skills bundled in this CLI # axiom skills list List the Skills bundled in this CLI ## Usage ```sh axiom skills list [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for list | ## See also - [axiom skills](./axiom-skills.md) — Manage the Axiom Skills for Claude Code --- ## Reference > axiom source {#reference/cli/axiom-source} > Print a node's handler source code and a link to its full repository # axiom source Print a node's handler source code and a link to its full repository Print a published node's handler source code — the same data the hosted MCP's axiom_get_source returns — plus a pointer to its full repository pinned to the exact published commit. Use this to verify what a node actually does before invoking it: "axiom inspect" shows the input/output shape, and "axiom source" shows the code. The snippet is the handler body; the repository link (printed to stderr) is where you read code the handler calls into. ```text axiom source node //[@version] ``` ## Usage ```sh axiom source [flags] axiom source [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for source | ## Subcommands | Command | Description | |---|---| | [axiom source node](./axiom-source-node.md) | Print a published node's handler source and repository link | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom source node](./axiom-source-node.md) — Print a published node's handler source and repository link --- ## Reference > axiom source node {#reference/cli/axiom-source-node} > Print a published node's handler source and repository link # axiom source node Print a published node's handler source and repository link Fetch a published node's handler source (GET /nodes/{id}/source) — the same data the MCP's axiom_get_source returns. When @version is omitted, the most recently published version is used. By default the source code prints to stdout (pipes cleanly into a file or pager) and the repository link + metadata print to stderr. With --json, the full structured response (source_snippet, repo_url, commit_hash, source_url, and — for an Instance node — the inherited generic origin) prints to stdout as one object. ```text axiom source node nadia/summarizer/Summarize axiom source node nadia/summarizer/Summarize@0.1.0 > Summarize.py axiom source node nadia/summarizer/Summarize --json | jq . ``` ## Usage ```sh axiom source node //[@version] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for node | | `--json` | | bool | | Emit the full structured response as one JSON object | ## See also - [axiom source](./axiom-source.md) — Print a node's handler source code and a link to its full repository --- ## Reference > axiom test {#reference/cli/axiom-test} > Run language-native tests with axiom validation # axiom test Run language-native tests with axiom validation Run the language-native test suite after validating the axiom package. Steps: ```text 1. Compile proto bindings (axiom generate) 2. Validate package (axiom validate — fail fast on errors) 3. Run tests (language-native runner, output streamed live) ``` Exit code mirrors the test runner exit code. Pass extra arguments to the native runner after --: ```text axiom test -- -v (Go: verbose output) axiom test -- -run TestProcessOrder (Go: run a specific test) axiom test -- -k test_process_order (Python: filter by name) axiom test -- --testNamePattern Foo (TypeScript: filter by name) ``` ## Usage ```sh axiom test [-- ] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for test | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) --- ## Reference > axiom unpin {#reference/cli/axiom-unpin} > Unpin a node or flow # axiom unpin Unpin a node or flow Remove a node or flow from your pinned tools. ```text axiom unpin node nadia/summarizer/Summarize@0.1.0 axiom unpin flow 01HQY5K7XWZ6N8R2BTEF4... ``` Requires a prior "axiom login". ## Usage ```sh axiom unpin [flags] axiom unpin [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for unpin | ## Subcommands | Command | Description | |---|---| | [axiom unpin flow](./axiom-unpin-flow.md) | Unpin a flow | | [axiom unpin node](./axiom-unpin-node.md) | Unpin a node | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom unpin flow](./axiom-unpin-flow.md) — Unpin a flow - [axiom unpin node](./axiom-unpin-node.md) — Unpin a node --- ## Reference > axiom unpin flow {#reference/cli/axiom-unpin-flow} > Unpin a flow # axiom unpin flow Unpin a flow ## Usage ```sh axiom unpin flow [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for flow | ## See also - [axiom unpin](./axiom-unpin.md) — Unpin a node or flow --- ## Reference > axiom unpin node {#reference/cli/axiom-unpin-node} > Unpin a node # axiom unpin node Unpin a node ## Usage ```sh axiom unpin node [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for node | ## See also - [axiom unpin](./axiom-unpin.md) — Unpin a node or flow --- ## Reference > axiom validate {#reference/cli/axiom-validate} > Validate axiom.yaml, proto definitions, and node signatures # axiom validate Validate axiom.yaml, proto definitions, and node signatures Validate the current axiom package across three layers: ```text 1. axiom.yaml schema — required fields, semver format, language, message references 2. Proto definitions — all .proto files in messages/ compile without errors 3. Node signatures — each node has an implementation with the expected function signature ``` Run automatically as part of: axiom build, axiom publish. ## Usage ```sh axiom validate [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for validate | | `--json` | | bool | | Emit structured JSON instead of human-readable output | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [Create a node (Python)](../../guides/create-a-node-python.md) - Guide: [Create a node (Go)](../../guides/create-a-node-go.md) - Guide: [Create a node (TypeScript)](../../guides/create-a-node-typescript.md) - Guide: [Create a node (Rust)](../../guides/create-a-node-rust.md) - Guide: [Create a node (Java)](../../guides/create-a-node-java.md) - Guide: [Create a node (C#)](../../guides/create-a-node-csharp.md) --- ## Reference > axiom version {#reference/cli/axiom-version} > Print the axiom CLI version # axiom version Print the axiom CLI version ## Usage ```sh axiom version [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for version | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages --- ## Reference > axiom whoami {#reference/cli/axiom-whoami} > Show the current authenticated user # axiom whoami Show the current authenticated user Display the user and tenant associated with the stored API key. Requires a prior "axiom login". ## Usage ```sh axiom whoami [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for whoami | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - Guide: [API keys](../../guides/api-keys.md) --- ## Reference > axiom.yaml manifest reference {#reference/axiom-yaml} > The complete schema of the axiom.yaml package manifest: every field, its type, default, validation rule, and which CLI commands read or write it. # axiom.yaml manifest reference `axiom.yaml` is the package manifest. It sits at the root of every Axiom package and declares the package's identity, its nodes, the message types it imports from other packages, and optional build overrides. The Axiom CLI locates the package root by walking up from the current directory until it finds `axiom.yaml`. Three fields are required: `name`, `version`, and `language`. Everything else is optional. `axiom validate` checks the manifest before publishing (see [Validation rules](#validation-rules)). ## Complete example Every field the parser accepts, in one manifest: ```yaml # axiom.yaml — at the package root name: christian/text-tools version: 1.2.0 language: python description: Text processing nodes author: Christian Lucas license: MIT tags: - text - nlp nodes: - name: SummarizeText description: Summarize input text input: TextRequest output: SummaryResult required_secrets: - OPENAI_API_KEY - name: StreamTokens input: TextRequest output: TokenFrame type: pipeline imports: - package: axiom-official/axiom-conv-ai version: 1.0.0 messages: - ChatTurn env: - name: LOG_LEVEL description: Verbosity of node logging required: false secret: false default: info build: dockerfile: custom/Dockerfile system_deps: - ffmpeg maven_deps: - org.dmfs:lib-recur:0.17.1 nuget_deps: - Cronos:0.8.4 ``` A minimal valid manifest is just the three identity fields — `axiom init` writes exactly that (plus your `--description` if given, and a commented example `nodes:` block you can delete). ## Top-level fields | Field | Type | Required | Default | Purpose | |---|---|---|---|---| | `name` | string | yes | — | Scoped package name, `handle/package-name` | | `version` | string | yes | — | Semantic version of the package | | `language` | string | yes | — | One of `go`, `python`, `rust`, `java`, `typescript`, `csharp` | | `type` | string | no | `""` | `proto-only` to skip build and deployment; otherwise omit | | `description` | string | no | `""` | Shown in the registry on publish | | `author` | string | no | `""` | Stored with the package record on publish | | `license` | string | no | `""` | Stored with the package record on publish | | `tags` | list of string | no | `[]` | Discovery tags, stored in the registry's tag index on publish | | `nodes` | list of node | no | `[]` | The package's nodes (see [nodes](#nodes)) | | `imports` | list of import | no | `[]` | Message types from other packages (see [imports](#imports)) | | `env` | list of env var | no | `[]` | Runtime environment variables (see [env](#env)) | | `build` | object | no | unset | Build overrides (see [build](#build)) | `description`, `author`, and `license` are written to the registry when you publish with `axiom push`. `tags` are written to the registry's tag index on publish. Marketplace search currently matches on package name, description, and author — not on these tags. ## Identity fields: name, version, language **`name`** must be scoped: `your-handle/package-name`. Both parts may contain only lowercase letters, digits, and hyphens, and each part must start with a letter or digit (pattern: `^[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$`). An unscoped or missing name fails validation. `axiom init christian/text-tools` creates a local directory named after the part after the last `/` (`text-tools/`). **`version`** must be semantic versioning: `MAJOR.MINOR.PATCH`, with an optional leading `v` and an optional pre-release suffix of letters, digits, and dots after a hyphen (pattern: `^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$`). Examples: `1.0.0`, `v2.1.3`, `0.4.0-beta.1`. `axiom init` writes `0.1.0` unless you pass `--version`. **`language`** must be exactly one of `go`, `python`, `rust`, `java`, `typescript`, `csharp`. It selects the node templates, the generated service code, and the Dockerfile the build uses. `axiom init` defaults to `go` unless you pass `--language` (`-l`). ## nodes Each entry in `nodes` declares one node in the package. `axiom create node` appends entries here for you; you can also edit the list by hand. | Field | Type | Required | Default | Purpose | |---|---|---|---|---| | `name` | string | yes | — | PascalCase node name, unique within the package | | `description` | string | no | `""` | Shown in the registry | | `input` | string | yes | — | Input message name | | `output` | string | yes | — | Output message name | | `type` | string | no | unary | Omit or `unary` for one-in/one-out; `pipeline` for a streaming generator | | `required_secrets` | list of string | no | `[]` | Secret names the node reads at runtime | | `mutation_capable` | bool | no | `false` | Declares the node emits state mutations on its response | | `kind` | string | no | `""` | `""` / `"concrete"` (default) — fixed, specific types decided at authoring time; `"generic"` — a marketplace connector whose port(s) an **Instance** later binds to a real type (see [Generic and Instance nodes](#generic-and-instance-nodes)) | | `generic_ports` | list of string | no | `[]` | Which port(s) are generic — `"input"`, `"output"`, or both; required (non-empty) when `kind: generic`, forbidden otherwise | | `retry` | object | no | unset | Package-author invocation-retry override (see [retry](#retry)) | **`name`** — `axiom create node` enforces PascalCase: the first character must be uppercase, and only letters and digits are allowed. Duplicate node names in one manifest are rejected. The node's source file is the snake_case form of the name (`SummarizeText` → `nodes/summarize_text.py`). **`input` / `output`** — each must name a [message](/docs/reference/glossary#message) defined in `messages/messages.proto` or available from an imported package. `axiom validate` fails when a referenced message cannot be found. **`type`** — a node is unary unless `type: pipeline` is set. `axiom create node --type` accepts only `unary` or `pipeline` and writes `type: pipeline` to the manifest only for pipeline nodes (the key is omitted for unary). See [execution model](/docs/concepts/execution-model). **`required_secrets`** — names of tenant [secrets](/docs/reference/glossary#secret) the node reads at runtime. The declaration is **informational**: the platform does not enforce it at invocation time. A missing secret does not block a run — `secrets.Get("NAME")` simply yields a not-found result (`found = false`) at read time, which the node handles. The marketplace displays the list so users know which secrets to register. `axiom validate` emits a non-blocking warning when node source calls `secrets.Get("NAME")` with a name missing from this list. See [manage secrets](/docs/guides/manage-secrets). **`mutation_capable`** — declares that the node emits state mutations on its response. The flow compiler reads this across a flow's nodes to decide whether to enable the mutation path; the default `false` keeps a node out of that path entirely. ### Generic and Instance nodes **`kind: generic`** marks a node as a **Generic node**: a real, published, deployed connector (a database query, an HTTP call) whose input and/or output port is declared broad enough to serve every use of the node, but not yet the specific shape any one flow wants — the author can't know a particular table's columns or an API's exact response shape at publish time. A Generic node cannot be placed in a flow by itself. ```yaml nodes: - name: Query input: QueryParams # a real, concrete, GENERALIZED message — not a placeholder output: Row # declare real fields broad enough for every use of this node kind: generic # "" / "concrete" (default) | "generic" generic_ports: [input, output] # which port(s): "input", "output", or both ``` **`generic_ports`** names which port(s) of a `kind: generic` node are generic. It is required (non-empty) whenever `kind: generic`, and forbidden (must be empty) on any other node. Each entry must be `input` or `output`, with no duplicates. Genericness is signaled entirely by `kind` + `generic_ports` — **never** by the message's name; there is no required naming convention (such as a `Placeholder` suffix) for a generic port's message. `axiom validate` does warn (non-blocking) if a generic port's message declares zero fields, since a field-less message forces the node's own handler code to hand-decode raw protobuf wire bytes instead of reading ordinary named fields — declare real, concrete, generalized fields instead. An **Instance** binds a Generic node's port(s) to a real, specific type via a field mapping — created with `axiom instance create` (or the marketplace's Generic Nodes tab), not authored in `axiom.yaml`. An Instance carries no code of its own: it shares the Generic node's already-built, already-deployed image at runtime, so creating one is pure server-side type-binding — no git commit, no build, no Knative deploy. `kind: instance` can never be hand-authored in a manifest — only the registry stamps that value when an Instance is created; `axiom validate` rejects a manifest that sets it. See [Configure an Instance from a Generic node](../guides/configure-an-instance.md) for the full declare→create→reference loop (including a field whose type is another message, defined recursively, and previewing a mapping before you save), and [the HTTP API reference](./http-api.md#create-an-instance) for the underlying `POST /v1/instances` endpoint. ## retry Axiom guarantees at-least-once invocation of every node by default — this requires no configuration. `retry` is the optional package-author layer of a three-layer model (platform defaults → package author, here → flow author, `flow.yaml`'s `retry:` block): set only the fields you want to override for this node; anything you omit inherits from the platform default, and a flow author can still override this node's effective retry again per-flow. See the `axiom-package-authoring` skill for the full model and the platform defaults, and the `axiom-flow-authoring` skill for the flow-author layer. ```yaml nodes: - name: Query retry: max_attempts: 5 initial_interval_ms: 500 backoff_coefficient: 2.0 max_interval_ms: 30000 per_attempt_timeout_ms: 5000 overall_timeout_ms: 60000 non_retryable_codes: [TIMEOUT] ``` | Field | Type | Required | Purpose | |---|---|---|---| | `max_attempts` | int | no | Retry attempts before the node fails; rejected by `axiom validate`/`axiom push` with a blocking error if it exceeds the platform ceiling (10) | | `initial_interval_ms` | int | no | Backoff before the 2nd attempt | | `backoff_coefficient` | float | no | Multiplier applied to the interval after each attempt | | `max_interval_ms` | int | no | Cap on the backoff interval | | `per_attempt_timeout_ms` | int | no | Budget for a single attempt | | `overall_timeout_ms` | int | no | Total budget across all attempts | | `non_retryable_codes` | list of string | no | `NodeErrorCode` short names to exclude from retry — **`TIMEOUT`/`TRANSPORT` only** here | This layer applies only to `NODE_ERROR_TRANSPORT` / `NODE_ERROR_TIMEOUT` (platform invocation failures) — never to `NODE_ERROR_USER` (the node's own code returning/throwing an error), which is hardcoded non-retryable at every layer and is never a valid `non_retryable_codes` entry here (`axiom validate` rejects `USER`, `PANIC`, or `CANCELLED` in this field with an error). Because invocation retry is at-least-once, not exactly-once, write node handlers to be idempotent — see the idempotency guidance in the `axiom-package-authoring` skill and the SDK reference pages for [Go](/docs/reference/sdk/go), [Python](/docs/reference/sdk/python), and [TypeScript](/docs/reference/sdk/typescript). ## imports Each entry in `imports` declares a dependency on another package's message types. Do not write these entries by hand — run `axiom import [@version]`, which downloads the package's `.proto` files into `imports///` (scope `/` becomes `-`, e.g. `christian/text-ops` → `imports/christian-text-ops/1.0.0/`), adds or updates the manifest entry, and runs `axiom generate`. | Field | Type | Required | Purpose | |---|---|---|---| | `package` | string | yes | Scoped name of the imported package | | `version` | string | yes | Exact version of the imported package | | `messages` | list of string | yes | Message names imported from that package | Validation rules for imports: - An import entry with an **empty `messages` list is a hard error** and blocks publishing. Fix it by re-running `axiom import @`, which populates the list. - A message name listed here but **not found in the downloaded proto files** is a warning — it usually means a stale entry after the imported package changed. - The downloaded proto files must exist locally under `imports///`; if they are missing, re-run `axiom import @`. See [import package types](/docs/guides/import-package-types) for the full workflow. ## env Each entry in `env` describes a runtime environment variable for the package. All fields except `name` are optional. | Field | Type | Required | Default | Purpose | |---|---|---|---|---| | `name` | string | yes | — | Variable name | | `description` | string | no | `""` | What the variable controls | | `required` | bool | no | `false` | Whether the variable must be set | | `secret` | bool | no | `false` | Whether the value is sensitive | | `default` | string | no | `""` | Default value | The `env` section is declarative metadata about what the package expects; `axiom validate` does not check it. For credentials read by node code at runtime, use [secrets](/docs/guides/manage-secrets) and `required_secrets` instead — secrets are tenant-scoped and reach node code through `AxiomContext`. ## build The optional `build` section overrides how the package's container image is built. Most packages never need it — `axiom build` and `axiom push` generate a Dockerfile automatically from the package language. | Field | Type | Required | Purpose | |---|---|---|---| | `dockerfile` | string | no | Path (relative to the package root) to a custom Dockerfile used instead of the generated one | | `system_deps` | list of string | no | Extra OS-level packages installed into the generated image | | `maven_deps` | list of string | no | **(Java only)** Third-party Maven dependencies, each an exact-pinned `groupId:artifactId:version` coordinate | | `nuget_deps` | list of string | no | **(C# only)** Third-party NuGet dependencies, each an exact-pinned `PackageID:Version` coordinate | ### maven_deps `maven_deps` lets a **Java** package depend on third-party libraries from Maven Central. Each entry is a coordinate `groupId:artifactId:version`, for example `org.dmfs:lib-recur:0.17.1`. The coordinates are rendered into the `` block of the generated `pom.xml` — which is regenerated on every build, so you declare dependencies here rather than editing `pom.xml` directly (hand edits are overwritten). The same declaration is honored by the local build (`axiom build`), by `axiom push`, and by the server-side registry build, so a Java node can call the library at runtime after publish. The version must be an **exact pin** for reproducibility: `axiom validate` rejects Maven version ranges (`[1.0,2.0)`), Gradle-style dynamic versions (`0.17.+`), the `LATEST`/`RELEASE` keywords, and `-SNAPSHOT` versions, as well as any coordinate that is not exactly three colon-separated parts. `maven_deps` declared on a non-Java package is a validation error. Classifiers, exclusions, BOM imports, and custom `` are not supported today — only plain Maven-Central coordinates. ### nuget_deps `nuget_deps` is the C# equivalent: it lets a **C#** package depend on third-party libraries from NuGet.org. Each entry is a coordinate `PackageID:Version`, for example `Cronos:0.8.4`. The coordinates are rendered as `` items into the generated `service.csproj` (and the `tests/Tests.csproj` that `axiom test` runs) — both are regenerated on every build, so you declare dependencies here rather than editing them directly (hand edits are overwritten). The same declaration is honored by the local build (`axiom build`), by `axiom push`, and by the server-side registry build, so a C# node can call the library at runtime after publish. The version must be an **exact pin** for reproducibility: `axiom validate` rejects NuGet floating versions (`6.0.*`), version ranges (`[1.0,2.0)`), and any coordinate that is not exactly two colon-separated parts. `nuget_deps` declared on a non-C# package is a validation error. Custom NuGet feeds (``), private sources, and per-reference asset controls are not supported today — only plain NuGet.org coordinates. ### system_deps `system_deps` entries are installed by the generated Dockerfile as OS-level packages. For Python packages, the special value `axiom` installs the Axiom CLI binary into the image instead of an OS package. For Python packages, `axiom validate` scans node source for `subprocess` calls and emits a non-blocking warning when a node invokes a common tool that is absent from the slim base image and not listed in `build.system_deps` — without the entry, the tool would be missing from the runtime image. The scan covers a fixed list of tools: `git`, `curl`, `wget`, `ssh`, `rsync`, `zip`, `unzip`, `make`, `gcc`, `g++`, `svn`, `jq`. A tool outside that list (such as `ffmpeg`) still needs a `system_deps` entry to be present in the image — it just is not flagged by validation. ## Proto-only packages A proto-only package publishes message types for other packages to import, with no nodes and no running service. A package is treated as proto-only when either: - `type: proto-only` is set explicitly, or - the `nodes` list is empty (proto-only is inferred). For a proto-only package, the publish pipeline skips the Docker build and the service deployment, and stores only the proto definitions and package metadata. Other packages then pull its messages with `axiom import`. See [the type system](/docs/concepts/type-system). ## Which commands read and write axiom.yaml - **`axiom init `** creates the manifest with `name`, `version` (default `0.1.0`), `language` (default `go`), and your `--description`. Unless `--no-example-comment` is passed, it appends a commented example `nodes:` block showing the `required_secrets` field. - **`axiom create node `** appends a node entry (`name`, `description`, `input`, `output`, and `type: pipeline` for pipeline nodes) after scaffolding the source files. - **`axiom import [@version]`** adds or updates an `imports` entry, merging newly downloaded message names into the `messages` list. - **`axiom validate`**, **`axiom build`**, **`axiom push`**, **`axiom dev`**, and **`axiom test`** read the manifest; they must be run inside a package directory (any directory below the one containing `axiom.yaml`). When a CLI command rewrites the manifest, it re-serializes the whole file — **inline comments you wrote in axiom.yaml are not preserved** by these operations. Keep notes elsewhere if you need them to survive. ## Validation rules `axiom validate` (also run as a gate by `axiom push`) checks the manifest as its first layer. Failures block publishing unless marked as warnings: | Check | Severity | |---|---| | `name` present and scoped (`handle/package-name`, lowercase/digits/hyphens) | error | | `version` present and valid semver | error | | `language` one of the six supported values | error | | Every node's `input` and `output` resolves to a known message | error | | Every import entry has a non-empty `messages` list | error | | Imported message names exist in the downloaded proto files | warning | | `secrets.Get("NAME")` calls covered by `required_secrets` | warning | | Python `subprocess` tools covered by `build.system_deps` | warning | | `retry.max_attempts` within the platform ceiling; `retry.per_attempt_timeout_ms` not exceeding `retry.overall_timeout_ms`; `retry.non_retryable_codes` entries are `TIMEOUT`/`TRANSPORT` only | error | | `kind` is one of `""`, `"concrete"`, `"generic"` — `"instance"` can never be hand-authored | error | | `generic_ports` is non-empty (and each entry is `input`/`output`, no duplicates) when `kind: generic`; empty otherwise | error | | A `generic_ports`-listed port's message declares at least one field | warning | Later validation layers check the proto files, node function signatures, and node tests — see the [axiom validate reference](/docs/reference/cli/axiom-validate). --- ## Reference > Python SDK reference {#reference/sdk/python} > Complete reference for the ax parameter in Python nodes: structured logging with ax.log, secrets with ax.secrets, and flow reflection with ax.reflection.flow. # Python SDK reference Every Python node handler receives the platform through a single parameter: `ax`, typed as `AxiomContext`. This page is the complete reference for that surface — structured logging, secrets, flow reflection, and flow mutation. There is no pip package to install: the Axiom CLI generates the `AxiomContext` interface into your package at `gen/axiom_context.py` (regenerated by the CLI; do not edit), and every capability reaches the platform through the sidecar — node code never calls platform services directly (see [sandboxing and tenancy](/docs/concepts/sandboxing-and-tenancy)). **Prerequisites.** Examples on this page assume a Python package scaffolded with `axiom init demo/chat-tools --language python`, two messages created with `axiom create message ChatRequest --fields "session_id:string; text:string"` and `axiom create message ChatReply --fields "text:string"`, and a node created with `axiom create node AnswerQuestion --input ChatRequest --output ChatReply` (see [create a node in Python](/docs/guides/create-a-node-python)). ## Handler signatures A unary Python node is one function in `nodes/`. The function name is the snake_case of the node name, the handler file is `nodes/.py`, and the test file is `nodes/_test.py`. Local message types come from the generated `gen.messages_pb2` module: ```python # nodes/answer_question.py — generated shape of a unary Python node from gen.messages_pb2 import ChatRequest, ChatReply from gen.axiom_context import AxiomContext def answer_question(ax: AxiomContext, input: ChatRequest) -> ChatReply: """Answers a chat message.""" return ChatReply() ``` The handler may also be declared `async def` — the platform detects an awaitable result and runs it to completion. Declare your handler `async def` whenever you `await` other async platform APIs. A pipeline node (created with `--type pipeline`) is a generator: it takes an iterator of input frames and yields output frames. For the entry node of a flow running in pipeline mode, the iterator yields exactly one item: ```python # nodes/stream_replies.py — generated shape of a Python pipeline node from typing import Iterator from gen.messages_pb2 import ChatRequest, ChatReply from gen.axiom_context import AxiomContext def stream_replies(ax: AxiomContext, inputs: Iterator[ChatRequest]) -> Iterator[ChatReply]: """Streams replies for each incoming request frame.""" for inp in inputs: yield ChatReply(text=inp.text) ``` The handler's full source — docstring included — is captured at publish time and shown in the Axiom registry's **View source** panel, and it feeds the AI-generated description displayed on the node's card. Replace the generated placeholder docstring before pushing, and set the node's `description` field in `axiom.yaml` (or pass `--description` to `axiom create node`) to state the description directly. ## At-least-once invocation: design for idempotency Axiom guarantees at-least-once invocation of every node by default (see [the `axiom.yaml` reference](/docs/reference/axiom-yaml) for the retry knobs). A transient invocation failure (`NODE_ERROR_TRANSPORT` / `NODE_ERROR_TIMEOUT`) can cause the platform to call your handler again for the same execution, with the same input. Write handlers so a repeat call is safe: - Prefer naturally idempotent operations — an upsert on a stable key, a conditional write, a `PUT` instead of a bare `POST`. - For a non-idempotent side effect (charging a customer, sending an email), derive a stable idempotency key from `ax.execution_id` and de-duplicate against it downstream, or use the target system's own idempotency-key API if it has one. - Raising an exception from your handler produces `NODE_ERROR_USER`, which the platform **never** retries, at any layer — retrying a flaky call inside your own logic is your handler's job (a normal `try`/`except` retry loop around the flaky call), not a platform mechanism. This is a design responsibility, not something the platform enforces. ## AxiomContext at a glance | Attribute | Type | Purpose | |---|---|---| | `ax.log` | `AxiomLogger` | Structured logger for this invocation — use instead of `print()` | | `ax.secrets` | `AxiomSecrets` | Read-only access to tenant secrets | | `ax.reflection.flow` | `AxiomFlowReflection` | Read-only view of the running flow graph and current position | | `ax.mutation.flow` | `AxiomMutationFlow` | Append nodes and edges to the running flow (mutation-capable nodes only) | | `ax.execution_id` | `str` | ID of the current execution, injected by the platform | New platform capabilities are added to `AxiomContext` — node function signatures never change to accommodate them. ## Logging with ax.log `ax.log` has four methods, each taking a message string plus arbitrary keyword attributes: ```python # nodes/answer_question.py — inside the handler body ax.log.debug("raw input", size=len(input.text)) ax.log.info("answering", session_id=input.session_id) ax.log.warn("input truncated", limit=4096) ax.log.error("model call failed", attempt=3) ``` In local development (`axiom dev`) the logger writes concise plain text to the terminal. In production it writes JSON records carrying `level`, `msg`, `trace_id`, `span_id`, `execution_id`, and the node name plus your keyword attributes — making every log line searchable by execution and linkable to its distributed trace. Use `ax.log` instead of `print()` for anything you want visible in production. Each invocation receives its own logger instance pre-configured with that invocation's trace context — never share a logger across calls, and never construct one yourself. ## Reading secrets with ax.secrets `ax.secrets.get(name)` returns a `(value, ok)` tuple: `(value, True)` when the named secret is present, `("", False)` otherwise. Values are plaintext strings; the platform handles encryption and decryption. ```python # nodes/answer_question.py — inside the handler body api_key, ok = ax.secrets.get("OPENAI_KEY") if not ok: raise ValueError("secret OPENAI_KEY is not registered for this tenant") ``` Declare every secret name the node reads under the node's `required_secrets` list in `axiom.yaml` so users know what to register before invoking. `axiom validate` scans handler source for `ax.secrets.get("NAME")` calls and warns (without failing) when a referenced name is missing from `required_secrets`; the scan is best-effort and cannot see dynamically constructed names. Secrets are registered per tenant in the console — see [manage secrets in a flow](/docs/guides/manage-secrets). `get()` alone can't tell "revoked" from "never configured" — both return `("", False)`. Use `ax.secrets.status(name)` for that: it returns a `SecretStatus` (`AVAILABLE`, `REVOKED`, or `UNSET`). See [manage secrets in a flow](/docs/guides/manage-secrets#distinguish-revoked-from-never-configured) for the full accessor table across languages. > **Agent memory removed for beta.** There is no `ax.agent.memory` in the > beta SDK — agentic memory is out of scope for beta and has been removed > at compile time. ## Inspecting the running flow with ax.reflection.flow `ax.reflection.flow` is a read-only view of the flow's compiled graph and the current invocation's position in it. Five properties: - `nodes` — `list[ReflectionNode]`, every node placement in the graph. - `edges` — `list[ReflectionEdge]`, the forward edges. - `loop_edges` — `list[ReflectionEdge]`, the loop-back edges. - `position` — a `FlowPosition` for the current invocation. - `graph_id` — the artifact ID of the graph this node runs in (the sub-flow's own ID when running inside a sub-flow). ```python # nodes/answer_question.py — inside the handler body pos = ax.reflection.flow.position downstream = [e for e in ax.reflection.flow.edges if e.src_instance == pos.current_instance] iteration = pos.loop_iterations.get(pos.current_instance, 0) ax.log.info("graph view", nodes=len(ax.reflection.flow.nodes), downstream=len(downstream), iteration=iteration, graph_id=ax.reflection.flow.graph_id) ``` If the platform does not supply reflection data for an invocation, the view degrades gracefully: the lists are empty, `graph_id` is `""`, and `position` is all zeros — reading reflection never raises. ## Reflection data types **ReflectionNode** — one node placement: `instance_id` (`int`), `node_ulid` (`str`), `name` (`str`), `package_name` (`str`), `package_version` (`str`), `node_type` (`node`, `subflow`, or `pipeline`), `input_message_name` and `output_message_name` (fully-qualified Protocol Buffers message names), and `canvas_node_id` (`str`). **ReflectionEdge** — one edge: `src_instance` (`int`), `dst_instance` (`int`), `canvas_edge_id` (`str`), `has_condition` (`bool`), `has_adapter` (`bool`), `max_iterations` (`int` — meaningful only on entries from `loop_edges`), and `condition_summary` (a `ConditionSummary` when the edge is conditional, else `None`). `has_condition` and `has_adapter` are structural flags only — the compiled adapter recipe is not exposed. **ConditionSummary** — an agent-readable digest of a conditional edge's dispatch predicate: `field` (`str`), `op` (`str`), and `operands` (`list[str]`). For example `field="tools"`, `op="EQ"`, `operands=["ToolX"]` means the edge fires when `"ToolX"` is in the source output's `tools` field — enough for a node to make idempotent decisions (such as skipping a tool that is already wired) without parsing the compiled condition. **FlowPosition** — where this invocation sits: `current_instance` (`int`), `depth` (`int`, `0` at the root flow), `loop_iterations` (`dict[int, int]` keyed by the loop's destination instance), and `subflow_stack_graph_ids` (`list[str]`, ordered root → immediate parent). ## Adding to the running flow with ax.mutation.flow Nodes declared with `mutation_capable: true` in `axiom.yaml` (default `false`) may append nodes and edges to the running flow during their handler: - `ax.mutation.flow.add_node(package, version, canvas_position=None)` — buffer a new node placement; returns the `int` instance ID to use as the endpoint of subsequent `add_edge` calls in the same invocation. - `ax.mutation.flow.add_edge(src_instance, dst_instance, condition=None)` — buffer an edge between two instance IDs (existing or just added). Pass `condition={"op": "EQ", "field": "tools", "value": "ToolX"}` to make the edge fire only when the predicate holds on the source node's output (on a repeated field, `EQ` has membership semantics); omit `condition` for an unconditional edge. ```python # nodes/plan_tools.py — inside a mutation-capable handler body search_iid = ax.mutation.flow.add_node( package="axiom/search-tools", version="0.4.0", ) ax.mutation.flow.add_edge( src_instance=ax.reflection.flow.position.current_instance, dst_instance=search_iid, ) ``` Calls buffer locally and are applied by the platform after the handler returns. If the platform rejects a buffered mutation, the SDK surfaces an `AxiomMutationError` whose `.message` attribute holds the human-readable reason. ## Testing nodes that use AxiomContext `axiom create node` generates `nodes/_test.py` containing a `_TestContext` class — a minimal `AxiomContext` with a silent logger, secrets served from a dict, and `execution_id` set to a test value. Supply secrets your node reads via `secrets_map`: ```python # nodes/answer_question_test.py — replace the generated test function with: def test_answer_question(): ax = _TestContext(secrets_map={"OPENAI_KEY": "sk-test"}) input_msg = ChatRequest(session_id="s1", text="hi") result = answer_question(ax, input_msg) assert isinstance(result, ChatReply) ``` A plain `def` handler is called directly, as above. For an `async def` handler, run it to completion with `asyncio.run(...)` inside the test. Run tests with `axiom test`, which uses pytest for Python packages. Tests run again inside the publish build, and a failing test fails the push. `axiom validate` warns (without blocking) when a node has no test — a node with zero tests would otherwise pass silently. Assert output field values meaningfully, not just the return type. --- ## Reference > Go SDK reference {#reference/sdk/go} > Every capability on axiom.Context — the Go form of AxiomContext: structured logging, secrets, execution identity, flow reflection, and flow mutation, with full signatures. # Go SDK reference In Go, AxiomContext is the `axiom.Context` interface: the single parameter (conventionally named `ax`) through which a node handler reaches every platform capability — logging, secrets, execution identity, flow reflection, and flow mutation. There is no external SDK module to install: the `axiom` package is generated into your package directory as `axiom/context.go`, and your business logic imports only that local package and the generated `gen` message types. **Prerequisites.** Examples on this page assume a Go package scaffolded with the Axiom CLI — see [create a node in Go](/docs/guides/create-a-node-go). They use that guide's `demo/text-tools` package (module path `demo/text-tools`, messages `TextRequest{text}` and `WordCountResult{count}`); sections that need other messages state their own `axiom create message` commands. ## The handler signature A unary node is one exported function in package `nodes` with this fixed shape — `axiom validate` checks it: ```go // nodes/count_words.go package nodes import ( "context" "strings" "demo/text-tools/axiom" gen "demo/text-tools/gen" ) // CountWords splits the input text on whitespace and returns the word count. func CountWords(ctx context.Context, ax axiom.Context, input *gen.TextRequest) (*gen.WordCountResult, error) { words := strings.Fields(input.GetText()) ax.Log().Info("counted words", "count", len(words)) return &gen.WordCountResult{Count: int32(len(words))}, nil } ``` - `ctx context.Context` — standard Go context for cancellation; pass it to every `ax` call that takes one. - `ax axiom.Context` — the platform capability injection point, documented below. - `input` / return value — pointers to the generated types of the node's input and output messages. - Returning a non-nil `error` fails the invocation; the error text is reported to the platform as the node's error message. A pipeline node (declared with `--type pipeline`) receives input as a channel and emits any number of output frames through a callback: ```go // nodes/stream_counts.go func StreamCounts(ctx context.Context, ax axiom.Context, in <-chan *gen.TextRequest, emit func(*gen.WordCountResult) error) error { for input := range in { words := strings.Fields(input.GetText()) if err := emit(&gen.WordCountResult{Count: int32(len(words))}); err != nil { return err } } return nil } ``` For the entry node of a flow running in pipeline mode, the channel yields exactly one item. See [execution model](/docs/concepts/execution-model). ## At-least-once invocation: design for idempotency Axiom guarantees at-least-once invocation of every node by default (see [the `axiom.yaml` reference](/docs/reference/axiom-yaml) for the retry knobs). That means a transient invocation failure (`NODE_ERROR_TRANSPORT` / `NODE_ERROR_TIMEOUT`) can cause the platform to call your handler again for the same execution, with the same input. Write handlers so a repeat call is safe: - Prefer naturally idempotent operations — an upsert on a stable key, a conditional write (`INSERT ... ON CONFLICT DO NOTHING`), a `PUT` instead of a bare `POST`. - For a non-idempotent side effect (charging a customer, sending an email), derive a stable idempotency key from `ax.ExecutionID()` and de-duplicate against it downstream, or use the target system's own idempotency-key API if it has one. - Returning a non-nil `error` from your handler produces `NODE_ERROR_USER`, which the platform **never** retries, at any layer — retrying a flaky call inside your own logic is your handler's job (a normal `for`-loop retry around the flaky call), not a platform mechanism. This is a design responsibility, not something the platform enforces. ## Context methods at a glance `axiom.Context` has seven methods. Adding new platform capabilities never changes node function signatures — capabilities are always added as methods here. | Method | Returns | Purpose | |---|---|---| | `ax.Log()` | `Logger` | Structured logging for this invocation | | `ax.Secrets()` | `Secrets` | Read-only access to tenant secrets | | `ax.ExecutionID()` | `string` | ID of the current execution | | `ax.FlowID()` | `string` | Stable ID of the flow's compiled artifact (constant across executions) | | `ax.TenantID()` | `string` | ID of the tenant that owns this invocation | | `ax.Reflection()` | `Reflection` | Read-only view of the running flow | | `ax.Mutation()` | `Mutation` | Append-only mutation of the running flow | ## Logging `ax.Log()` returns a structured logger pre-configured for the current invocation. Use it instead of `fmt.Println` for anything you want visible in production. ```go // axiom/context.go (generated) type Logger interface { Debug(msg string, args ...any) Info(msg string, args ...any) Warn(msg string, args ...any) Error(msg string, args ...any) } ``` `args` follows Go's `log/slog` convention — alternating key, value pairs: ```go // inside any node handler ax.Log().Info("order processed", "order_id", "abc123", "total", 99.99) ``` Under `axiom dev` the logger writes concise plain-text lines to the terminal. In the deployed service each log line automatically carries the node name and `execution_id`, plus the active trace context, so production logs are searchable by execution. Concurrent invocations each receive their own `Logger` instance — never share one across calls, and never construct one yourself. ## Secrets `ax.Secrets()` returns read-only access to the tenant secrets the platform resolved for this invocation. Values are plaintext strings; encryption and decryption are the platform's job. ```go // axiom/context.go (generated) type Secrets interface { // Get returns the value for the named secret and true when present. // Returns ("", false) when the secret was not resolved for this invocation. Get(name string) (string, bool) } // SecretStatusOf reports the typed status of a declared secret: // SecretStatusAvailable, SecretStatusRevoked, or SecretStatusUnset. It is a // free helper (not a Secrets method) so the accessor stays backward-compatible. func SecretStatusOf(s Secrets, name string) SecretStatus ``` ```go // inside any node handler apiKey, ok := ax.Secrets().Get("ANTHROPIC_API_KEY") if !ok { return nil, fmt.Errorf("secret ANTHROPIC_API_KEY not configured") } ``` Use `axiom.SecretStatusOf(ax.Secrets(), name)` when you need to tell "revoked" from "never configured" apart — see [manage secrets in a flow](/docs/guides/manage-secrets#distinguish-revoked-from-never-configured). Declare every secret name the node reads under the node's `required_secrets` list in `axiom.yaml`, so users know what to register in the console before invoking — see [manage secrets in a flow](/docs/guides/manage-secrets). Secrets reach the node only through this interface; they never appear in package source. > **Agent memory removed for beta.** There is no `ax.Agent()` / > `Agent`/`AgentMemory`/`SessionMemory` surface in the beta SDK — agentic > memory is out of scope for beta and has been removed at compile time. ## Execution identity Three string accessors are declared for execution identity. They are read-only facts injected by the platform — node code cannot change them, and tenant isolation is enforced by the sidecar regardless of what the node does (see [sandboxing and tenancy](/docs/concepts/sandboxing-and-tenancy)). | Method | Meaning | |---|---| | `ax.ExecutionID()` | ID of the current execution — the same ID the HTTP API, traces, and debug events reference | | `ax.FlowID()` | Stable ID of the flow's compiled artifact — constant across executions | | `ax.TenantID()` | ID of the tenant that owns this invocation | ## Flow reflection `ax.Reflection().Flow()` returns a read-only view of the running flow's topology and the current invocation's position in it. Field-level message introspection is not exposed — `InputMessageName`/`OutputMessageName` are fully-qualified Protocol Buffers message names. ```go // nodes/describe_flow.go package nodes import ( "context" "fmt" "strings" "demo/text-tools/axiom" gen "demo/text-tools/gen" ) // DescribeFlow returns a text summary of the running flow's topology. func DescribeFlow(ctx context.Context, ax axiom.Context, input *gen.ChatRequest) (*gen.ChatReply, error) { flow := ax.Reflection().Flow() var b strings.Builder fmt.Fprintf(&b, "graph %s: %d nodes, %d edges, at instance %d\n", flow.GraphID(), len(flow.Nodes()), len(flow.Edges()), flow.Position().CurrentInstance) for _, n := range flow.Nodes() { fmt.Fprintf(&b, "[%d] %s (%s@%s) %s -> %s\n", n.InstanceID, n.Name, n.PackageName, n.PackageVersion, n.InputMessageName, n.OutputMessageName) } return &gen.ChatReply{Text: b.String()}, nil } ``` ### Reflection interfaces and types ```go // axiom/context.go (generated) type Reflection interface { Flow() FlowReflection } type FlowReflection interface { Nodes() []ReflectionNode Edges() []ReflectionEdge LoopEdges() []ReflectionEdge Position() FlowPosition GraphID() string } type ReflectionNode struct { InstanceID uint32 NodeULID string Name string PackageName string PackageVersion string NodeType string // "node" | "subflow" | "pipeline" InputMessageName string OutputMessageName string CanvasNodeID string } type ReflectionEdge struct { SrcInstance uint32 DstInstance uint32 CanvasEdgeID string HasCondition bool HasAdapter bool MaxIterations uint32 ConditionSummary *ConditionSummary } type ConditionSummary struct { Field string // dotted field path, e.g. "tools" Op string // short op name (EQ | NEQ | CONTAINS | ...) Operands []string // comparison operand(s) } type FlowPosition struct { CurrentInstance uint32 Depth uint32 // 0 at the root flow LoopIterations map[uint32]uint32 // keyed by loop-head dst_instance SubflowStackGraphIDs []string // root → immediate parent } ``` `HasCondition` and `HasAdapter` are structural flags only — the compiled condition and adapter recipes are not exposed by design. When an edge is conditional, `ConditionSummary` digests its leaf predicate (a leaf on a repeated field has ANY semantics: `Field="tools", Op="EQ", Operands=["ToolX"]` means "ToolX" is in `tools`), so a node can make idempotent decisions like skipping a tool that is already wired. `MaxIterations` is meaningful only on entries returned by `LoopEdges()`. ## Flow mutation `ax.Mutation().Flow()` lets a node add nodes and edges to the running flow mid-execution. Only nodes declared with `mutation_capable: true` in `axiom.yaml` may call it. Calls buffer locally during the handler and are submitted to the platform when the handler returns. ```go // nodes/add_tool.go — this node needs mutation_capable: true in axiom.yaml package nodes import ( "context" "demo/text-tools/axiom" gen "demo/text-tools/gen" ) // AddTool wires a calculator node into the running flow behind a conditional edge. func AddTool(ctx context.Context, ax axiom.Context, input *gen.ChatRequest) (*gen.ChatReply, error) { flow := ax.Mutation().Flow() toolInstance := flow.AddNode("demo/calculator", "1.0.0", &axiom.CanvasPosition{X: 400, Y: 200}) self := ax.Reflection().Flow().Position().CurrentInstance flow.AddEdge(self, toolInstance, &axiom.EdgeCondition{ Op: "EQ", Field: "tools", Value: "Calculator", }) return &gen.ChatReply{Text: "calculator wired in"}, nil } ``` ### Mutation interfaces and types ```go // axiom/context.go (generated) type Mutation interface { Flow() FlowMutation } type FlowMutation interface { AddNode(packageName, packageVersion string, canvasPosition *CanvasPosition) uint32 AddEdge(srcInstance, dstInstance uint32, condition *EdgeCondition) } type CanvasPosition struct { X float64 Y float64 } type EdgeCondition struct { Op string // EQ | NEQ | LT | LTE | GT | GTE | CONTAINS | ... Empty -> EQ. Field string // dotted field path on the source node's output message Value string // comparison operand (string form) } ``` - `AddNode` returns the batch-local instance ID assigned to the new node — use it as `dstInstance` in `AddEdge` calls within the same handler. `canvasPosition` is an optional layout hint; pass `nil` to omit it. - `AddEdge` with a non-nil `condition` wires a conditional dispatch edge that fires only when the predicate holds on the source node's output message (a condition on a repeated field has ANY semantics). Pass `nil` for an unconditional edge. Conditions are structural only — no CEL expressions, no adapters. ### Mutation rejection The platform validates buffered mutations after the handler returns. A rejected mutation surfaces as an error message with the deterministic prefix `axiom: mutation rejected: `; the generated `MutationError` type carries the human-readable reason after that prefix: ```go // axiom/context.go (generated) type MutationError struct{ Message string } func (e *MutationError) Error() string { return e.Message } ``` The structured engine rejection code is not exposed to node code. To stay idempotent across invocations, check the existing topology via `ax.Reflection().Flow()` (including each edge's `ConditionSummary`) before re-adding nodes or edges. ## Where the interface comes from `axiom/context.go` is generated by the Axiom CLI — `axiom create node` writes it (and regenerates it idempotently on every run), and the file carries the banner `Code generated by Axiom CLI; DO NOT EDIT`. Do not edit it: your changes are overwritten on the next scaffold. The same interface is implemented by the local `axiom dev` server and by the deployed service that `axiom push` builds, so handler code behaves identically in both. The generated test file for each node includes a `newTestContext(t)` helper for unit-testing handlers — see the testing section of [create a node in Go](/docs/guides/create-a-node-go). --- ## Reference > TypeScript SDK reference {#reference/sdk/typescript} > Complete reference for AxiomContext in TypeScript — logging, secrets, flow reflection, flow mutation, execution identifiers, and handler signatures. # TypeScript SDK reference `AxiomContext` is the single injection point for every platform capability a TypeScript node can use. It is passed as the first parameter (`ax`) to every node handler; node code never calls platform services directly — every call goes through the [sidecar](../../concepts/sandboxing-and-tenancy.md). ## Where the SDK comes from There is no npm package to install. `axiom generate` (run automatically by `axiom create`, `axiom dev`, `axiom test`, and `axiom build`) writes the full SDK into your package at `gen/axiomContext.ts`, alongside the generated message bindings (`gen/messages_pb.js`, `gen/messages_pb.d.ts`). The file carries a `// Code generated by Axiom CLI; DO NOT EDIT.` banner — edits are overwritten on the next generate. Import from it with a relative path: ```typescript // nodes/greet.ts import { AxiomContext } from '../gen/axiomContext'; ``` All interfaces on this page (`AxiomContext`, `AxiomLogger`, `AxiomSecrets`, `AxiomReflection`, `AxiomMutation`, and their supporting types) are exported from `gen/axiomContext.ts`. ## Handler signatures A unary node handler takes `AxiomContext` and one input message and returns one output message. It may be synchronous or `async` — the platform awaits the result either way. `async` is required when you await other Promise-based platform APIs: ```typescript // nodes/greet.ts import { GreetRequest, GreetReply } from '../gen/messages_pb'; import { AxiomContext } from '../gen/axiomContext'; export async function greet(ax: AxiomContext, input: GreetRequest): Promise { ax.log.info('greeting', { name: input.getName() }); const out = new GreetReply(); out.setGreeting(`Hello, ${input.getName()}!`); return out; } ``` A pipeline node handler (scaffolded with `axiom create node ... --type pipeline`) is an async generator: it consumes an `AsyncIterable` of input frames and yields output frames. When a pipeline node is the entry node of a flow, the iterable yields exactly one item. ```typescript // nodes/tokenize.ts import { GreetRequest, GreetReply } from '../gen/messages_pb'; import { AxiomContext } from '../gen/axiomContext'; export async function* tokenize( ax: AxiomContext, inputs: AsyncIterable, ): AsyncGenerator { for await (const input of inputs) { const out = new GreetReply(); out.setGreeting(`Hello, ${input.getName()}!`); yield out; } } ``` Input and output messages are `google-protobuf` classes with getter/setter accessors — see [The type system](../../concepts/type-system.md). ## AxiomContext fields Every handler receives one `AxiomContext` per invocation, with these read-only fields: | Field | Type | What it is | |---|---|---| | `ax.log` | `AxiomLogger` | Structured logger for this invocation | | `ax.secrets` | `AxiomSecrets` | Read-only tenant secrets | | `ax.executionId` | `string` | ID of the current execution | | `ax.flowId` | `string` | Stable ID of the compiled artifact (constant across executions) | | `ax.tenantId` | `string` | ID of the tenant that owns this invocation | | `ax.reflection` | `AxiomReflection` | Read-only view of the running flow (`ax.reflection.flow`) | | `ax.mutation` | `AxiomMutation` | Mutation surface for `mutation_capable` nodes (`ax.mutation.flow`) | New platform capabilities are added to `AxiomContext`, never as extra handler parameters — handler signatures stay stable across SDK versions. ## ax.log — structured logging `AxiomLogger` has four levels, each taking a message and optional structured attributes: ```typescript // nodes/greet.ts — inside a handler ax.log.debug('cache lookup', { key: 'user:42' }); ax.log.info('order processed', { order_id: 'abc123', total: 99.99 }); ax.log.warn('retrying upstream call', { attempt: 2 }); ax.log.error('upstream failed', { status: 502 }); ``` The interface: ```typescript // gen/axiomContext.ts (generated — shown for reference) export interface AxiomLogger { debug(msg: string, attrs?: Record): void; info(msg: string, attrs?: Record): void; warn(msg: string, attrs?: Record): void; error(msg: string, attrs?: Record): void; } ``` Use `ax.log` instead of `console.log()`. In local development (`axiom dev`) it writes concise plain text to the terminal. In production it writes JSON with `trace_id`, `span_id`, and `execution_id` baked into every line, making logs searchable by execution and linkable to the distributed trace. ## ax.secrets — read secrets `ax.secrets.get(name)` returns a `[value, ok]` tuple: `[value, true]` when the named secret is present, `['', false]` otherwise. Values are plaintext strings; the platform handles encryption and decryption. ```typescript // nodes/greet.ts — inside a handler const [apiKey, ok] = ax.secrets.get('OPENAI_API_KEY'); if (!ok) { ax.log.warn('OPENAI_API_KEY is not configured'); } ``` Secrets are tenant-scoped, stored in the console, and resolved by the platform at invocation time — never hardcode credentials in node source. Declare each secret a node reads under `required_secrets` in `axiom.yaml` so it is validated at publish time; see [Manage secrets in a flow](../../guides/manage-secrets.md). `get()` alone can't tell "revoked" from "never configured" — both return `['', false]`. Use the `secretStatusOf(ax.secrets, name)` helper (imported from the generated `axiomContext`) for that: it returns a `SecretStatus` (`Available`, `Revoked`, or `Unset`). See [Manage secrets in a flow](../../guides/manage-secrets.md#distinguish-revoked-from-never-configured) for the full accessor table across languages. > **Agent memory removed for beta.** There is no `ax.agent` / `ax.agent.memory` > in the beta SDK — agentic memory is out of scope for beta and has been > removed at compile time. ## ax.reflection.flow — flow reflection `ax.reflection.flow` is a read-only view of the running flow's graph and the current invocation's position in it: ```typescript // nodes/router.ts — inside a handler: what runs after me? const pos = ax.reflection.flow.position; const downstream = ax.reflection.flow.edges .filter(e => e.srcInstance === pos.currentInstance); ``` - `nodes: ReflectionNode[]` — every node placement. Each has `instanceId`, `nodeUlid`, `name`, `packageName`, `packageVersion`, `nodeType` (`'node' | 'subflow' | 'pipeline'`), `inputMessageName` / `outputMessageName` (fully-qualified Protocol Buffers message names), and `canvasNodeId`. - `edges: ReflectionEdge[]` and `loopEdges: ReflectionEdge[]` — forward and loop edges. Each has `srcInstance`, `dstInstance`, `canvasEdgeId`, `hasCondition`, `hasAdapter`, `maxIterations` (meaningful only on `loopEdges` entries), and an optional `conditionSummary`. - `conditionSummary?: ConditionSummary` — when an edge is conditional, a readable digest of its dispatch predicate: `field`, `op`, and `operands`. For example `field: 'tools'`, `op: 'EQ'`, `operands: ['ToolX']` means the edge fires when `'ToolX'` is in the repeated `tools` field. Lets a node make idempotent decisions (such as skipping a tool that is already wired). - `position: FlowPosition` — `currentInstance`, `depth` (0 at the root flow), `loopIterations` (keyed by the loop head's `dstInstance`), and `subflowStackGraphIds` (root flow first, immediate parent last). - `graphId: string` — the running graph's ID. Reflection is structural only: compiled edge adapters and compiled conditions are not exposed, only the `hasAdapter` / `hasCondition` flags and the condition summary. ## ax.mutation.flow — mutate the running flow Nodes declared with `mutation_capable: true` on their entry in `axiom.yaml` can append nodes and edges to the running flow: ```typescript // nodes/addtool.ts — inside a mutation-capable handler const toolInstance = ax.mutation.flow.addNode('my-org/tools', '1.2.0', { x: 400, y: 200 }); ax.mutation.flow.addEdge( ax.reflection.flow.position.currentInstance, toolInstance, { op: 'EQ', field: 'tools', value: 'ToolX' }, // optional condition ); ``` - `addNode(packageName, packageVersion, canvasPosition?)` — buffer a new node placement; returns the instance ID assigned to it, numbered after the nodes already in the running flow. The optional `canvasPosition` (`{ x, y }`) is a canvas placement hint. Use the returned ID in `addEdge` calls within the same handler. - `addEdge(srcInstance, dstInstance, condition?)` — buffer a new edge. Omit `condition` for an unconditional edge. Pass an `EdgeCondition` — `op` (`'EQ' | 'NEQ' | 'LT' | 'LTE' | 'GT' | 'GTE' | 'CONTAINS'`; empty string means `EQ`), `field` (dotted path on the source node's output message), `value` (operand in string form) — for a conditional dispatch edge. A condition on a repeated field matches when any element matches. Mutation is append-only and buffered: calls record locally during the handler and are attached to the node's response when it returns — nothing changes mid-handler. The platform validates buffered mutations after the handler returns; a rejected mutation fails the execution with an error message carrying the deterministic prefix `axiom: mutation rejected: ` followed by the human-readable reason. `AxiomMutationError` (an `Error` subclass exported from `gen/axiomContext.ts`) is the SDK's type for that rejection; the structured engine rejection code is not exposed to node code. ## Error handling Throw a plain `Error` (or any subclass) to fail the invocation: the generated service wrapper catches anything thrown (or a rejected Promise) and reports the error message to the platform as the node's failure reason. There is no Axiom-specific error type to throw from handler code. ```typescript // nodes/greet.ts — inside a handler const [apiKey, ok] = ax.secrets.get('OPENAI_API_KEY'); if (!ok) { throw new Error('OPENAI_API_KEY is not registered for this tenant'); } ``` See [Debug a flow](../../guides/debug-a-flow.md) for how failed executions surface in the canvas. A thrown error here reports as `NODE_ERROR_USER`, which the platform **never** retries at any layer — retrying a flaky call inside your own logic (a downstream API blip) is your handler's job, using a normal `try`/`catch` retry loop, not a platform mechanism. ## At-least-once invocation: design for idempotency Axiom guarantees at-least-once invocation of every node by default (see [the `axiom.yaml` reference](/docs/reference/axiom-yaml) for the retry knobs). A transient invocation failure (`NODE_ERROR_TRANSPORT` / `NODE_ERROR_TIMEOUT`) can cause the platform to call your handler again for the same execution, with the same input. Write handlers so a repeat call is safe: - Prefer naturally idempotent operations — an upsert on a stable key, a conditional write, a `PUT` instead of a bare `POST`. - For a non-idempotent side effect (charging a customer, sending an email), derive a stable idempotency key from `ax.executionId` and de-duplicate against it downstream, or use the target system's own idempotency-key API if it has one. This is a design responsibility, not something the platform enforces. ## Mock AxiomContext in tests `axiom create node` scaffolds `nodes/_test.ts` with a ready-made `testContext: AxiomContext`: a silent logger, a secrets store that returns `['', false]` for every name, empty reflection, and a do-nothing mutation mock. Pass it straight to your handler — `await` the result when the handler is `async`, as `greet` is above: ```typescript // nodes/greet_test.ts — a jest test using the scaffolded testContext it('greets by name', async () => { const input = new GreetRequest(); input.setName('Ada'); const result = await greet(testContext, input); expect(result).toBeInstanceOf(GreetReply); expect(result.getGreeting()).toBe('Hello, Ada!'); }); ``` Because every field of `AxiomContext` is an interface, override exactly the capability under test — for example, replace `secrets` with `{ get: (name) => name === 'OPENAI_API_KEY' ? ['test-key', true] : ['', false] }` or swap the mutation mock for a recorder that pushes `addNode` / `addEdge` arguments into an array you assert on. Run the suite with `axiom test` — see [Create a node in TypeScript](../../guides/create-a-node-typescript.md). --- ## Reference > HTTP API reference {#reference/http-api} > Every invocation endpoint — flow invoke, SSE streaming, single-node calls, pre-warm — plus the Client Builder management API, Instance creation, adapter-mapping preview, with authentication, request and response fields, rate limits and daily quotas, and the structured error envelope. # HTTP API reference This page is the reference for Axiom's invocation HTTP surface: invoking a flow (`POST /v1/flows/invoke`), streaming a pipeline-mode flow (`POST /v1/flows/invoke/stream`), invoking a single node, pre-warming a flow (`POST /v1/flows/warm`), the error envelope, and rate limits — plus the [Client Builder management API](#client-builder-management-api) that `axiom client` drives. For a guided walkthrough, start with [Invoke a flow via API](../getting-started/invoke-via-api.md). For the exact input and output schema of *your* flow or package, use the [live OpenAPI specs](#live-openapi-specs) — they are generated from the compiled artifact and are always current. ## Base URL and authentication All invocation endpoints are served by the Axiom gateway at `https://api.axiomide.com` under the `/invocations` path prefix. For example: ```text POST https://api.axiomide.com/invocations/v1/flows/invoke ``` Every request must carry an API key as a bearer token: ```text Authorization: Bearer ``` API keys are 64-character hex strings created under **Console → API Keys** in the app, or minted automatically by `axiom login` (named `cli`). The raw key is shown exactly once at creation; the platform stores only its SHA-256 hash. See [Create and manage API keys](../guides/api-keys.md). The key resolves to your tenant. Every execution it starts is scoped to that tenant — the platform enforces isolation, so a key can never invoke another tenant's flows or read another tenant's data ([Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)). A missing, invalid, or revoked key gets HTTP 401 with the body: ```json {"error": "unauthorized"} ``` Revocation takes effect on the key's very next use — there is no key cache. ## Rate limits Two per-tenant token buckets apply to authenticated requests: - **All routes:** 100 requests/second, burst 200. - **Invocation routes** (everything under `/invocations/`): an additional 5 requests/second, burst 10 — a beta guardrail. The invocation burst limit is enforced at ingress admission as well as at the gateway, so it applies even on the direct-to-ingress routing the `axiom` CLI defaults to in development (which bypasses the gateway entirely). Exceeding either returns HTTP 429 with a `Retry-After: 1` header and the body `{"error":"rate limit exceeded"}`. ## Daily quotas On top of the per-second rate limits, each tenant has two daily budgets. Both reset at midnight UTC. The beta defaults are: - **2,000 invocations per day.** An invocation is one authenticated request that triggers compute: a flow invoke (plain or streaming), a single-node call, a paused-flow resume (including resume webhooks), or a debug-session fork. - **500 executions per day.** An execution is one flow run. Most invocations start exactly one, but executions a flow spawns internally — child executions from parallel branches, runtime graph mutations, debug forks — each draw from the same budget. A flow that spawns 10 parallel branches costs 10 executions. A request that would exceed either budget is rejected before any compute runs, with HTTP 429, a `Retry-After` header, and a structured body whose `retry_after_seconds` counts down to the next UTC midnight: ```json { "error": "quota_exceeded", "message": "daily invocations quota exceeded (cap 2000)", "retry_after_seconds": 74380 } ``` If a running flow exhausts the execution budget mid-run (for example when spawning a large number of parallel branches), the flow fails with the same `quota exceeded` message as its error. The defaults are per-tenant and the operator can raise or lower them for your tenant — if you hit them with a legitimate workload, get in touch. Two related responses you may also see on invocation routes: | Status | Body `error` | Meaning | |---|---|---| | 403 | `tenant_suspended` | Your tenant has been disabled by the operator. No invocations or resumes are accepted. Contact the operator. | | 503 | `quota_unavailable` | The quota service could not be reached, so the request was rejected as a safety measure. Transient — retry after `retry_after_seconds` (5 s). | ## Invoke a flow `POST /v1/flows/invoke` executes a compiled artifact once and returns the result inline (with `wait`) or an execution ID immediately (without). ```bash curl -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD", "input": { "text": "hello" }, "wait": true }' ``` ### Request fields | Field | Type | Required | Meaning | |---|---|---|---| | `graph_id` | string | yes | The compiled artifact to execute. Get it from the **Use via API** dialog or the flow's [live OpenAPI spec](#live-openapi-specs); editing a flow produces a new artifact with a new ID. | | `input` | object | one of `input`/`payload` | The entry node's input message as a JSON object. The platform converts it to the typed message, and decodes the result back to JSON. | | `payload` | string (base64) | one of `input`/`payload` | The entry node's input message as protobuf-encoded bytes, base64-encoded. For callers that already speak protobuf. `input` takes precedence if both are set. | | `wait` | boolean | no | `true` blocks until the execution completes and populates `result`. Default `false`: the response returns immediately with just `accepted` and `execution_id`. | | `timeout_seconds` | integer | no | How long a `wait`ing call blocks before giving up (default 30). | | `config_id` | string | no | Named flow config profile to apply; when omitted the default hierarchy applies (tenant default → flow default). See [Flow configs](../guides/flow-configs.md). | | `debug_session_id` | string | no | Streams per-node debug events for this execution to the named debug session. Can also be sent as the `X-Debug-Session-Id` header, which takes precedence over the body field. See [Debug a flow](../guides/debug-a-flow.md). | | `idempotency_key` | string | no | Dedups retries: a call with the same key from the same tenant within 5 minutes of the first returns that first call's `execution_id` instead of executing the flow again — at most one execution per key, even if the retry lands while the first call is still in flight. Can also be sent as the `Idempotency-Key` header, which takes precedence over the body field. When omitted, the platform falls back to a fingerprint of `graph_id`, `config_id`, and the payload/`input`, so byte-identical retries still dedup even if you never set this field. | ### Response An accepted invocation returns HTTP 202: ```json { "accepted": true, "execution_id": "4bf92f3577b34da6a3ce929d0e0e4736", "result": { "success": true, "output": { "text": "hello" }, "completed_at": 1765432100000 } } ``` - `execution_id` — a 32-character hex ID assigned when the request is accepted. It is also the execution's trace ID, so one ID references the execution everywhere: results, debug events, and traces. - `result` — present only with `wait: true`. `result.output` is the terminal node's output message decoded to JSON (when you invoked with `input`); `result.payload` carries base64-encoded protobuf bytes instead when you invoked with `payload` or when JSON decoding was not possible. - `result.error_message` — present with `wait: true` when the execution was accepted and delivered but ran to a failure (`result.success` is `false`), for example because the input did not match the flow's start-node schema. The same diagnostic is also copied to the top-level `error_message` so a caller can read it without descending into `result`. The response is still HTTP 202 (the request was accepted); the failure is informational, not a transport error. - Without `wait`, the body is just `{"accepted": true, "execution_id": "..."}` and the flow runs asynchronously. Failures return a non-202 status with an error body — see [Error envelope](#error-envelope). ## Stream a flow over SSE `POST /v1/flows/invoke/stream` invokes a flow in pipeline mode and streams result frames back as Server-Sent Events. Use it for flows that emit a sequence of output frames rather than a single result ([Execution model](../concepts/execution-model.md)). ```bash curl -N -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke/stream' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD", "input": { "text": "hello" } }' ``` The request body accepts `graph_id`, `input` (or `payload`), `timeout_seconds`, `config_id`, and `debug_session_id` exactly as in [Invoke a flow](#invoke-a-flow). `wait` does not apply — frames are always delivered as they are produced. The response is `Content-Type: text/event-stream`. Each event is one `data:` line of JSON: ```text data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":0,"payload":{"text":"chunk 1"},"is_final":false} data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":1,"payload":{"text":"chunk 2"},"is_final":true,"success":true} ``` Frame fields: | Field | Meaning | |---|---| | `execution_id` | Same ID as the unary endpoint — also the trace ID. | | `frame_index` | Position of this frame in the stream, starting at 0. | | `payload` | The terminal node's output message for this frame, decoded to JSON. Omitted when the frame carries no decodable payload. | | `is_final` | `true` on the last frame; the stream ends after it. | | `success` | `true` on the final frame of a successful execution. On failure the field is omitted entirely (it is never serialized as `false`) — treat a final frame without `success: true` as failed and read `error`. | | `error` | Error message when a frame reports failure. | The stream times out after 30 seconds by default; set `timeout_seconds` to extend it. A timeout ends the stream with a final frame whose `error` is `"timeout waiting for pipeline result"`. ## Invoke a single node A published node can be called directly, without composing a flow. This is the endpoint that Client Builder SDKs use — one method per node. ```bash curl -X POST 'https://api.axiomide.com/invocations/v1/nodes/axiom-official/pdf-chunker/1.0.0/Chunk' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com/doc.pdf" }' ``` - **Path forms:** `POST /v1/nodes/{owner}/{package}/{version}/{node}` (name-based, as in the example) or `POST /v1/nodes/{node_ulid}` (the node's 26-character registry ID). - **Request body:** the node's input message as a plain JSON object — no envelope. - **Response:** HTTP 200 with the node's output message as JSON. A node that returns an error gets HTTP 422 with `{"error_message": "..."}`; an unknown or undeployed node gets HTTP 404. A payload that doesn't match the node's current input schema is rejected with HTTP 400: an unknown or renamed field name (rejected by JSON decoding), or an enum field set to an ordinal that names no declared value, yields `{"execution_id": "...", "error_message": "invalid value for : field \"\" carries enum ordinal , which names no declared value ..."}`. Re-creating a payload recorded against an older schema version is the usual cause — re-record it against the current schema. - **Streaming nodes:** a pipeline-mode node (or any request with `Accept: text/event-stream`) streams its output frames as Server-Sent Events instead of a single JSON response. A binary-protobuf variant exists at `POST /v1/nodes/invoke` with the JSON body `{"node_id": "...", "payload": ""}`, returning `{"success": ..., "payload": ..., "error_message": ...}` — for callers that serialize the messages themselves. The payload is validated against the node's current input schema before it is forwarded: bytes whose wire shape doesn't match (an undeclared field number, or a wire type that conflicts with the declared field kind) are rejected with HTTP 400 and `{"success": false, "error_message": "payload does not match the node's current input schema: ..."}`, as is any payload whose node schema can't be resolved. ## Pre-warm a flow `POST /v1/flows/warm` primes a flow's pods ahead of time — a scale-from-zero nudge with no business logic run — so a subsequent real [invoke](#invoke-a-flow) doesn't pay a cold start. Call it, for example, right before you expect a burst of traffic, or after deploying a flow that has been idle. ```bash curl -X POST 'https://api.axiomide.com/invocations/v1/flows/warm' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD"}' ``` A successful warm returns HTTP 202: ```json {"warmed": true, "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD"} ``` `warmed: true` means the warm was dispatched, not that pods are already serving — it's fire-and-forget, the same way a cold start on invoke is not something you wait on. A flow with nothing deployed (for example, one built entirely from built-in nodes) is a legitimate no-op: you still get `warmed: true` back. Only the node packages your tenant is entitled to use are warmed; this only matters if a flow's saved graph somehow references a package outside your tenant's access, in which case that package is silently skipped rather than warmed. ### Request fields | Field | Type | Required | Meaning | |---|---|---|---| | `graph_id` | string | yes | The compiled artifact to warm — the same id you'd pass to [invoke](#invoke-a-flow). Must match `[A-Za-z0-9_-]+`; anything else is a 400. | Two other body fields, `tenant_id` and `flow_owner`, are accepted but **always ignored** — see the next section. Don't rely on either to select which tenant a warm applies to. ### Authentication and ownership Pre-warm authenticates exactly like every other invocation route: your bearer API key resolves to your tenant (see [Base URL and authentication](#base-url-and-authentication)). That tenant — never anything in the request body — is what the platform checks the `graph_id` against: a `graph_id` your tenant does not own returns HTTP 403, the same ownership check invoke performs. An unknown `graph_id` returns 404. Either way, no warm is dispatched — a warm request can never be used to spin up capacity for another tenant's flow. The `tenant_id` and `flow_owner` body fields exist in the request schema only so a forged value there has nowhere to go — the platform never reads them. Setting either has no effect; your key's tenant is always the one enforced. ### Rate limits and quotas Pre-warm shares the same per-tenant budgets as [invoke](#invoke-a-flow) — it does not have a separate limit: - The [rate limits](#rate-limits) above (100 req/s overall, 5 req/s on invocation routes) apply identically. - Each warm debits **one unit of the daily invocation quota** (the same 2,000/day budget a flow invoke, single-node call, or resume consumes — see [Daily quotas](#daily-quotas)). It does **not** touch the separate executions budget, since warming runs no flow. A tenant that is suspended, or has exhausted its invocation quota, gets the same 403 `tenant_suspended` / 429 `quota_exceeded` responses invoke returns — see [Error envelope](#error-envelope). ### Errors specific to pre-warm | Status | `error` class | When | |---|---|---| | 400 | `invalid_request` | `graph_id` is missing or doesn't match `[A-Za-z0-9_-]+`. | | 403 | `forbidden` | The authenticated tenant does not own `graph_id`. | | 404 | `not_found` | `graph_id` doesn't resolve to any flow. | | 503 | `warm_unavailable` | The warm dispatcher isn't configured on this deployment; `retry_after_seconds: 5`. | All other statuses (401, 429, 403 `tenant_suspended`, 503 `quota_unavailable`) match [Error envelope](#error-envelope) exactly. ## Client Builder management API These routes create and manage [clients](../concepts/nodes-packages-flows.md) — tenant-scoped bundles of nodes and flows that compile into a typed SDK — and back the `axiom client` CLI (see [Build a client SDK](../guides/build-a-client-sdk.md)). They're a separate management surface from the invocation endpoints above: served under the **`/api`** path prefix on the same gateway (`https://api.axiomide.com`), not `/invocations`. ```text POST https://api.axiomide.com/api/v1/clients ``` Authentication is the same bearer API key as every other route. Mutating calls (`POST`, `PUT`, `DELETE`) additionally require a key with the **`write`** scope — a read-only-scoped key gets HTTP 403 with `{"error": "forbidden: token lacks 'write' scope"}`. A request that reaches the registry without a resolved tenant (for example, calling the registry directly instead of through the gateway) gets HTTP 401 with `{"error": "missing tenant context"}`. | Method | Path | Description | |---|---|---| | `POST` | `/v1/clients` | Create a client. | | `GET` | `/v1/clients` | List the tenant's clients. | | `GET` | `/v1/clients/{id}` | Get one client, including its members and versions. | | `PUT` | `/v1/clients/{id}` | Replace a client's name/description/default language. | | `DELETE` | `/v1/clients/{id}` | Delete a client (cascades to its members and versions). | | `POST` | `/v1/clients/{id}/members` | Add one member (a node or a flow). | | `PUT` | `/v1/clients/{id}/members` | Replace the full member set in one call. | | `DELETE` | `/v1/clients/{id}/members/{memberId}` | Remove one member. | | `POST` | `/v1/clients/{id}/build` | Start a build: a new version, or additional languages on an existing one. | | `GET` | `/v1/clients/{id}/versions` | List every version+language build row. | | `GET` | `/v1/clients/{id}/versions/{version}?language={language}` | Get one version+language build row. | | `GET` | `/v1/clients/{id}/versions/{version}/download?language={language}` | Download the built SDK zip. | ### Create / get / list a client ```bash curl -X POST 'https://api.axiomide.com/api/v1/clients' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"name": "billing", "description": "Billing SDK", "default_language": "python"}' ``` Request fields: `name` (string, required, unique per tenant — 409 `{"error": "a client with that name already exists"}` on collision), `description` (string, optional), `default_language` (string, optional — must be one of `go`, `python`, `rust`, `java`, `typescript`, `csharp`, else 400 `{"error": "unsupported language: "}`). `PUT` takes the same three fields to replace them wholesale. Response (`201` for create, `200` for get/list/update): ```json { "id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8", "tenant_id": "01J8Z3K9Q2R4S6T8V0W2X4Y700", "name": "billing", "slug": "billing", "description": "Billing SDK", "default_language": "python", "latest_version": 0, "created_at": "2026-07-04T12:00:00Z", "updated_at": "2026-07-04T12:00:00Z", "members": [], "versions": [], "member_count": 0, "version_count": 0 } ``` `GET /v1/clients` returns this same shape as a JSON array (never `null`, even with zero clients); `GET /v1/clients/{id}` is the only call that populates `members`/`versions` — the list form always reports them empty and relies on `member_count`/`version_count` instead. `GET /v1/clients/{id}` returns 404 `{"error": "client not found"}` for an unknown or foreign-tenant ID. ### Add / list / remove a member ```bash curl -X POST 'https://api.axiomide.com/api/v1/clients/01J8Z3K9Q2R4S6T8V0W2X4Y6Z8/members' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"type": "node", "package": "acme/billing-utils", "version": "1.2.0", "node_name": "Summarize", "alias": "summarize"}' ``` Request fields: `type` (`"node"` or `"flow"`, required), and for `type: "node"` — `package` (required), `version` (optional; empty or `"latest"` pins the package's current latest version server-side), `node_name` or `node_ulid` (one identifies the node); for `type: "flow"` — `artifact_id` (required, the saved flow's compiled artifact ID). `alias` is optional on the wire (derived from the node name if omitted) but must be unique within the client — a collision returns 409 `{"error": "a member with that alias already exists — pass a distinct alias"}`. `PUT /v1/clients/{id}/members` takes `{"members": [...]}` with the same per-member shape and replaces the entire set transactionally. Response (`201` for add, `200` for `PUT`/list): ```json { "id": "01J8Z3K9Q2R4S6T8V0W2X4Y700", "client_id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8", "member_type": "node", "alias": "summarize", "package_name": "acme/billing-utils", "package_version": "1.2.0", "node_ulid": "01J8Z3K9Q2R4S6T8V0W2X4Y701", "created_at": "2026-07-04T12:00:00Z" } ``` `package_name`/`package_version`/`node_ulid` are omitted for a flow member, which instead carries `artifact_id`. `DELETE .../members/{memberId}` returns `204 No Content` whether or not the member existed. ### Build a version and check status ```bash curl -X POST 'https://api.axiomide.com/api/v1/clients/01J8Z3K9Q2R4S6T8V0W2X4Y6Z8/build' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"languages": ["python", "go"]}' ``` Request fields: `languages` (array of strings; falls back to the client's `default_language` if omitted, deduplicated, each validated against the same six-language set — 400 `{"error": "unsupported language: "}` for any unknown entry) and `version` (integer; `0` or omitted cuts a **new** version from the client's current member set — 400 `{"error": "client has no members — add at least one node or flow"}` if it has none; a positive value adds the given languages to that existing version's frozen member snapshot instead — 404 `{"error": "version not found"}` if it doesn't resolve). The call returns immediately with HTTP 202 and one row per requested language, each starting in `pending` (build/upload happen asynchronously in the background): ```json [ { "id": "01J8Z3K9Q2R4S6T8V0W2X4Y702", "client_id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8", "version": 1, "language": "python", "status": "pending", "created_at": "2026-07-04T12:00:00Z" } ] ``` Poll `GET /v1/clients/{id}/versions` (all rows) or `GET /v1/clients/{id}/versions/{version}?language={language}` (one row — the `language` query param is required, 400 `{"error": "language query param is required"}` without it) until `status` reaches `succeeded` or `failed`; a failed row's `error` field carries the failure message. No API key is ever embedded in a build (see [Call the SDK](../guides/build-a-client-sdk.md#call-the-sdk)) — the generated SDK always reads `AXIOM_API_KEY` from its caller's environment. ### Download a built SDK ```bash curl 'https://api.axiomide.com/api/v1/clients/01J8Z3K9Q2R4S6T8V0W2X4Y6Z8/versions/1/download?language=python' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -o billing-sdk-py.zip ``` Both `version` (path) and `language` (query, required) select the exact build row. `200 OK` streams `Content-Type: application/zip` with `Content-Disposition: attachment; filename="-sdk-.zip"`. A row that hasn't reached `succeeded` returns 409 `{"error": "build is not ready (status: )"}` instead of a body; an unknown version/language pair returns 404 `{"error": "version not found"}`. ## Instance creation API This route materializes an **Instance** — a [Generic node](../concepts/nodes-packages-flows.md#generic-node-and-instance-typed-at-use-time-not-at-publish-time)'s port(s) bound to a real type — entirely server-side (no git commit, no build, no Knative deploy) and backs `axiom instance create` (see the `axiom-instance-authoring` skill). Like the Client Builder routes above, it's served under the **`/api`** path prefix, and mutating calls require a bearer API key with the **`write`** scope. ```text POST https://api.axiomide.com/api/v1/instances ``` ### Create an Instance ```bash curl -X POST 'https://api.axiomide.com/api/v1/instances' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "generic_package": "acme/postgres-connector", "generic_node": "Query", "generic_version": "1.2.0", "name": "your-handle/orders-query", "output": { "message_name": "OrderSummary", "fields": {"id": "int64", "total": "double", "status": "string"} } }' ``` Request fields: `generic_package` / `generic_node` (required) locate the Generic node to bind. `generic_version` is optional — empty or `"latest"` resolves and **pins** whatever is latest at creation time (it does not track "latest" going forward). `name` (required) is the Instance's scoped name, `your-handle/instance-name`; the scope must equal the caller's own handle — a mismatched scope gets 403. At least one of `output` / `input` is required, matching whichever port(s) the Generic node declared generic — each is `{"message_name": string, "fields": {name: fieldDef}}` with at least one field. A `fieldDef` value is either a bare scalar-kind string (as in the example above — `string, int32, int64, uint32, uint64, double, float, bool, bytes`) or an object for anything richer: `{"kind": "", "repeated": true}`, `{"kind": "", "optional": true}` (mutually exclusive — proto3 forbids `repeated optional`), or `{"kind": "message", "message": {"message_name": "...", "fields": {...}}}` for a field whose type is itself another message, defined inline — recursively, so that nested message's own fields may in turn include a `message`-kind field. Every message in the tree — the top-level facade and every nested one, however deep — is synthesized in the same transaction that creates the Instance: nothing is written if the request fails validation at any level, and nothing exists as an intermediate row. See [Configure an Instance from a Generic node](../guides/configure-an-instance.md#nested-fields-with-a-fields-file) for a full worked example. Response (`201`) is the created Instance row: ```json { "package_id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8", "node_ulid": "01J8Z3K9Q2R4S6T8V0W2X4Y700", "name": "your-handle/orders-query", "version": "1", "scope": "your-handle", "owner_tenant_id": "01J8Z3K9Q2R4S6T8V0W2X4Y701", "visibility": 0, "generic_package": "acme/postgres-connector", "generic_version": "1.2.0", "generic_node_ulid": "01J8Z3K9Q2R4S6T8V0W2X4Y702", "output_message": "OrderSummary" } ``` `input_message` is present instead of (or alongside) `output_message` when the corresponding port was bound. Errors: 400 for a missing/malformed field (`{"error": "generic_package and generic_node are required"}`, `"name is required"`, `"at least one of output or input must be bound"`, an invalid message/field identifier, a name collision in the draft tree, or binding a node that isn't `kind: generic`), 403 if the scope doesn't match the caller's handle, 409 `{"error": "an instance with that name already exists"}` on a duplicate name, and 413 `{"error": "request body too large"}` over the body-size cap below. **Both an unknown Generic package and one the caller can't read return the identical 404 `{"error": "generic package not found"}`** — a denial can't be used as an existence oracle for another tenant's private package. ### Draft-tree limits A request's `output`/`input` field tree — the top-level facade plus every nested message — is bounded so a request fails fast with a clear error instead of an unbounded compile: | Limit | Bound | |---|---| | Nesting depth (top-level message counts as depth 1) | 10 | | Distinct messages in one tree (both ports combined) | 100 | | Fields on any one message | 200 | | Request body size | 1 MiB | A name reused for two different shapes within one request 400s as a collision; the identical name **and** shape drafted twice (for example, the same nested message referenced from both `output` and `input`) is a harmless dedup, not an error. These same limits apply to a structured draft sent to `POST /adapters/preview` below. ## Preview an adapter mapping `POST /adapters/preview` runs a field mapping (a single-edge adapter, or a multi-source compose mapping) against sample JSON and returns the exact value it produces — the same engine (`PrepareAdapter`/`ExecuteAdapter`, CEL, transforms) the registry compiler and worker use at real compile/dispatch time, not an approximation. It backs the canvas's live mapping preview and `axiom instance preview`. Same auth posture as the Instance route above: served under `/api`, bearer API key required. ```text POST https://api.axiomide.com/api/adapters/preview ``` ```bash curl -X POST 'https://api.axiomide.com/api/adapters/preview' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "dst_msg_name": "OrderSummary", "sources": [ {"edge_id": "e1", "msg_name": "Order", "sample": {"amount": 42.5}} ], "adapter": {"total": "$.amount"} }' ``` Request fields: `dst_msg_name` (required) names the mapping's target message. `sources` (required, at least one entry) is each incident edge's `edge_id` + `msg_name` + sample `sample` JSON payload. Exactly one of `adapter` (a single-edge DSL map, target field → expression) or `compose_mapping` (`edge_adapters` + `compose_plan`, for a multi-source compose consumer) is required; `adapter` requires exactly one `sources` entry. Any `msg_name` slot — `dst_msg_name` or a `sources[]` entry — accepts **either** a bare registry message name (a JSON string, as above) **or** an inline structured draft in place of it: the identical `{"message_name": ..., "fields": {...}}` tree shape the Instance-creation `fields` block uses (including nested `message`-kind fields and `repeated`/`optional` modifiers). A draft is rendered into `.proto` text and compiled transiently for that one request only — never persisted — so a mapping can be previewed against a message that doesn't exist in the registry yet: ```json { "dst_msg_name": {"message_name": "OrderSummary", "fields": {"total": "double"}}, "sources": [{"edge_id": "e1", "msg_name": "Order", "sample": {"amount": 42.5}}], "adapter": {"total": "$.amount"} } ``` Response (`200`) is always `{"output": }` or `{"error": ""}`, never a 4xx for a mapping-level failure (bad expression, type mismatch, unresolvable field) — that's a property of the in-progress mapping, not a malformed request: ```json {"output": {"total": 42.5}} ``` A malformed request body, an unresolvable `msg_name` not found in the registry, or exceeding the [draft-tree limits](#draft-tree-limits) above is a 4xx with a plain-text body, not the `{"error": ...}` JSON envelope other routes on this page use. ## Error envelope Invocation failures with a known cause return a structured body with a stable, machine-readable `error` class: ```json { "error": "payload_too_large", "message": "payload exceeds hard cap: actual=18874368 max=16777216", "max_bytes": 16777216, "actual_bytes": 18874368 } ``` | Status | `error` class | Extra fields | When | |---|---|---|---| | 400 | — | `{"accepted": false, "error_message": "invalid request body: ..."}` | The request body is not valid JSON. | | 401 | `unauthorized` | — | Missing, invalid, or revoked API key. | | 413 | `payload_too_large` | `max_bytes`, `actual_bytes` (when the rejecting code path knows the sizes) | The input payload exceeds the platform's 16 MiB hard cap. | | 422 | — | `{"error_message": "..."}` | Single-node invocation only: the node itself returned an error. | | 429 | `rate limit exceeded` | `Retry-After: 1` header | Per-tenant rate limit exceeded. | | 429 | `quota_exceeded` | `retry_after_seconds` to next UTC midnight, mirrored in the `Retry-After` header | Per-tenant [daily quota](#daily-quotas) (invocations or executions) exhausted. | | 403 | `tenant_suspended` | — | Your tenant has been disabled by the operator. | | 503 | `quota_unavailable` | `retry_after_seconds: 5` | The quota service is unreachable; the request is rejected as a safety measure. Retry with backoff. | | 503 | `upstream_unavailable` | `retry_after_seconds: 5` | The flow could not be queued; a platform dependency was temporarily unavailable. Retry with backoff. | | 503 | `blob_storage_unavailable` | `retry_after_seconds: 5` | A large input payload could not be stored; a platform dependency was temporarily unavailable. Retry with backoff. | | 500 | — | `{"accepted": false, "error_message": "..."}` | Any failure that does not classify into the rows above (for example, a missing `graph_id`). | Errors inside a successful HTTP exchange surface differently: a `wait`ed invoke returns 202 with `result.success: false`, and a stream delivers a final frame with `error` set and no `success: true` (the frame's `success` field is omitted on failure, never serialized as `false`). The [error catalog](../reference/error-catalog.md) lists the error messages themselves. ## Live OpenAPI specs Hand-maintained docs cannot know your flow's input schema — the live, generated OpenAPI 3.0 specs can. Use them as the authoritative request and response schemas: - **Per flow:** `GET /api/graphs/{artifact_id}/openapi.json` (authenticated, same bearer key). Generated on demand from the compiled artifact: the entry node's input message is the documented input schema, the terminal node's output message is the result schema, and `graph_id` is pinned to that exact artifact. Pipeline-mode flows document `/v1/flows/invoke/stream`; unary flows document `/v1/flows/invoke`. The same spec backs the **Open interactive docs** button in the flow inspector's **API** section — see [Use the interactive API docs](../guides/use-interactive-api-docs.md). - **Per package:** `GET /api/packages/{name}@{version}/openapi.json` (public, no auth). One `POST /v1/nodes/{owner}/{package}/{version}/{node}` operation per node, with input and output schemas and examples. An interactive try-it page for the same spec is served at `GET /api/packages/{name}@{version}/docs`. --- ## Reference > CEL expressions {#reference/cel-expressions} > Every place a CEL expression appears in Axiom — the compose-mapping formula bar, the edge-adapter cel() transform, a gate condition's CEL op, and a HITL node's interrupt_payload_template — the value/srcField bindings, the inputs.. multi-source syntax, the built-in string functions, and Axiom's two custom functions toUpper/toLower. # CEL expressions Axiom uses [Common Expression Language](https://cel.dev) (CEL) — via [`cel-go`](https://github.com/google/cel-go) — anywhere a mapping needs more than a straight field pick: computing a target field from one or more source fields, or gating an edge with a boolean condition. This page documents the CEL surface available to you: what identifiers are bound, the multi-source `inputs..` syntax, the built-in string functions, and Axiom's two custom functions. ## Where you write CEL - **The compose-mapping formula bar.** When a consumer node has more than one incident edge (a join), select a target field and the **ƒx** formula bar becomes the one place to type its binding. Typing a bare field name (e.g. `email`) collapses to a plain pick if it is unambiguous; typing anything else — `email != "" ? email : full_name`, a multi-source expression, a string-function call — is stored and evaluated as a CEL expression. Click the **?** next to **ƒx** to reopen this page from the panel. - **The single-source edge adapter.** On a single-source edge, each mapping's right-hand side is a single bare CEL expression evaluated against the source message (or a plain field pick when it names a source field). Write the expression directly — `int(value)`, `text + "!"`, `toUpper(text)` — with no pipe wrapper. See the [error catalog](./error-catalog.md#edge-adapter-cel-errors) for how a failing adapter reports its error. - **An edge's gate condition, scoped to one field.** Set `op: CEL` *and* `field: ` on a `flow.yaml` condition (or the equivalent graph JSON) to evaluate the expression against that field's own value, bound to `value` (same convention as the single-source edge adapter above) — the condition passes iff the expression returns `true`. There is no field-less/whole-message CEL condition today: a `CEL` op with no `field` set fails to compile (`field "" not found in message ...`) rather than evaluating against the whole input — so a `flow.yaml` `op: CEL` block always needs `field` too. (The canvas gate editor's own "Use CEL expression" toggle doesn't currently expose a field to scope to, so a condition authored that way hits the same compile failure — use the row-based comparison operators in the canvas editor instead until that toggle is field-aware.) - **A HITL node's `interrupt_payload_template`.** Evaluated once when the pause fires, with `value` bound to the *whole* node input message (not a single field) — `value` alone reproduces the whole input, `value.text` extracts one field, and normal CEL string concatenation (`"approve order " + value.id`) builds a custom string. No source-field alias here since there is no single source field to alias. ## The `value` binding and source-field aliases Every CEL expression has a variable named `value` bound to its input — the field (or, in a pipeline, the running value after any prior transforms). When the adapter or binding also knows the source field's name, that name is declared as a second variable bound to the *same* value, so an expression written naturally against the field's own name works without having to know about `value` at all: ```text value == "" # always works email == "" # works too, when the source field is named `email` ``` If the source message happens to have a field literally named `value`, the real field wins — Axiom does not silently return the wrong type just to keep the placeholder name meaningful. ## Picking a whole message-typed field A field pick doesn't have to name a scalar — it can name a whole **message-typed** field, singular or repeated, and copy it across the edge verbatim. This is the basis of the *canonical-envelope* pattern: a node that outputs a shared `Image` (or `Document`, `Graph`, …) envelope wires straight into a node that consumes the same envelope with a one-line pick. ```yaml # source node outputs `annotated` (an Image message); consumer takes `image` (also Image) adapter: annotated: image # whole-message pick — copies the Image verbatim ``` The one rule: a whole-message pick is a **byte-for-byte copy**, so it is only valid when the source field and the destination field are the **same message type**. Picking one message type into a *different* message field — even a structurally identical one from another package that doesn't resolve to the same type — is a **compile-time** error, not a silent misread: ```text adapter: field pick "image" maps message type "pkgA.Image" into differently-typed message field "annotated" (type "pkgB.Image") … reconstruct the destination in CEL by mapping each leaf field explicitly (e.g. {"annotated.": "image."}) ``` To bridge two *different* message types, reconstruct the destination in CEL, field by field (`annotated.width: image.width`, …) or with a CEL object literal — the same explicit-reshape rule every cross-type mapping follows. A cross-type message pick never compiles green and then fails at run. ## Multi-source composition: `inputs..` A join's consumer can have several incident edges, each identified by its canvas edge id. A CEL expression in the compose-mapping formula bar addresses any field on any incident edge with: ```text inputs.. ``` `` is dotted for nested fields (`inputs.edge-a.reviewer.email`). Axiom builds the CEL type environment from each edge's declared source message, so referencing an edge id that isn't actually incident to this join, or a field path that doesn't exist on that edge's message, is a **compile-time** error — it fails before any execution runs, not partway through one. An edge id that isn't a legal CEL identifier (for example one containing a hyphen from the canvas) can't be referenced this way; wire it with a plain pick instead. The formula bar accepts a bare field name as shorthand — `email` — and qualifies it to the full `inputs..` form automatically as long as exactly one incident edge has a field with that name. An expression that touches more than one source field always displays and stores the fully qualified form, since that is the only form `cel-go`'s checker (and Axiom's wire-recovery parser) can resolve unambiguously. If an incident edge hasn't produced a payload yet, every field on it reads as its type's zero value (empty string, `0`, `false`, …) rather than failing the expression. ## String functions (`cel-go` `ext.Strings()`) Axiom enables the full `cel-go` strings extension, called on a string value with member-call syntax (`value.trim()`), plus the two-argument `join`/ `split`/`replace`/`substring` overloads: | Function | Signature | Example | |---|---|---| | `charAt` | `string.charAt(int) -> string` | `value.charAt(0)` | | `indexOf` | `string.indexOf(string) -> int`, `string.indexOf(string, int) -> int` | `value.indexOf("@")` | | `lastIndexOf` | `string.lastIndexOf(string) -> int`, `string.lastIndexOf(string, int) -> int` | `value.lastIndexOf(".")` | | `lowerAscii` | `string.lowerAscii() -> string` | `value.lowerAscii()` | | `upperAscii` | `string.upperAscii() -> string` | `value.upperAscii()` | | `replace` | `string.replace(string, string) -> string`, `string.replace(string, string, int) -> string` | `value.replace("-", "_")` | | `split` | `string.split(string) -> list`, `string.split(string, int) -> list` | `value.split(",")` | | `substring` | `string.substring(int) -> string`, `string.substring(int, int) -> string` | `value.substring(0, 5)` | | `trim` | `string.trim() -> string` | `value.trim()` | | `join` | `list.join() -> string`, `list.join(string) -> string` | `value.split(",").join(" \| ")` | | `reverse` | `string.reverse() -> string` | `value.reverse()` | | `format` | `string.format(list) -> string` | `"%s wins".format([name])` | `lowerAscii`/`upperAscii` only affect ASCII bytes — for Unicode-aware uppercasing/lowercasing, use Axiom's `toUpper`/`toLower` below instead. ## Axiom custom functions: `toUpper` / `toLower` Axiom registers two custom global CEL functions alongside the `cel-go` extension — called as plain functions, not member calls: ```text toUpper(value) # "hello" -> "HELLO" toLower(value) + " world" # "HELLO" -> "hello world" ``` Both take exactly one string argument and return a string; calling either with a non-string argument is a runtime CEL error (`toUpper requires string` / `toLower requires string`). These are Go's `strings.ToUpper`/`strings.ToLower` under the hood (full Unicode case folding, not the ASCII-only `ext.Strings()` equivalents above). > `toUpper` and `toLower` are the only two custom functions Axiom adds on top > of the standard CEL library documented below. ## Function catalog Everything below is enabled everywhere CEL runs in Axiom — the ƒx formula bar, the `cel()` adapter transform, gate conditions, and HITL templates all share one environment (`env_version: v3`), so this catalog is the single surface. The same list drives the ƒx bar's autocomplete/function palette and the `axiom flow mapping-scope` CLI verb, both fed live from `GET /api/cel/manifest`. Only pure, deterministic functions are enabled: nothing here does I/O, produces randomness, or reads the clock (that guarantee is what keeps preview-equals-runtime and durable replay sound). Named functions are grouped by family; **operators** (`+`, `==`, `in`, `?:`, …) are CEL syntax and are not listed. | Family | Functions | Notes | |---|---|---| | `optionals` | `first`, `hasValue`, `last`, `optional.none`, `optional.of`, `optional.ofNonZeroValue`, `optional.unwrap`, `or`, `orValue`, `unwrapOpt`, `value` | Optional-value plumbing — required by (and enabled with) `regex.extract`'s `optional` return. | | `strings` | `charAt`, `format`, `indexOf`, `join`, `lastIndexOf`, `lowerAscii`, `replace`, `reverse`, `split`, `strings.quote`, `substring`, `trim`, `upperAscii` | String member helpers (`value.trim()`) plus `list.join` — full signatures in the [table above](#string-functions-cel-go-extstrings). | | `math` | `math.abs`, `math.bitAnd`, `math.bitNot`, `math.bitOr`, `math.bitShiftLeft`, `math.bitShiftRight`, `math.bitXor`, `math.ceil`, `math.floor`, `math.isFinite`, `math.isInf`, `math.isNaN`, `math.round`, `math.sign`, `math.sqrt`, `math.trunc` | Rounding, absolute value, square root, and bitwise operations on numeric values (`math.ceil(value)`). | | `lists` | `distinct`, `flatten`, `lists.range`, `slice`, `sort` | List member helpers for de-duplicating, flattening, ordering, windowing, and generating ranges of repeated fields. | | `sets` | `sets.contains`, `sets.equivalent`, `sets.intersects` | Set-semantics comparisons over two lists (`sets.contains(value, ["a", "b"])`). | | `encoders` | `base64.decode`, `base64.encode`, `json.encode` | Base64 between `bytes` and `string`, and `json.encode` for a value → JSON string. | | `regex` | `regex.extract`, `regex.extractAll`, `regex.replace` | RE2 regular expressions: extract the first match (an optional), all matches, or replace matches. | | `toUpper/toLower/parseJSON/toJson` | `parseJSON`, `toJson`, `toLower`, `toUpper` | Axiom's custom functions: Unicode-correct `toUpper`/`toLower`, and the schema-free JSON pair `.parseJSON()` (Tekton-style receiver method: parse a JSON string to a traversable value, integers kept precise) / `toJson` (serialize with sorted keys, byte-stable across builds). | ### Macros Macros are expanded at compile time (they are not first-class functions). The token is what the manifest reports; the call form is how you write it: | Macro | Call form | What it does | |---|---|---| | `all` | `list.all(x, pred)` | true iff every element satisfies the predicate (two-var `list.all(i, x, pred)` also available) | | `bind` | `cel.bind(name, init, expr)` | bind a computed value to a name once and reuse it in `expr` | | `exists` | `list.exists(x, pred)` | true iff at least one element satisfies the predicate | | `existsOne` | `list.exists_one(x, pred)` | true iff exactly one element satisfies the predicate | | `exists_one` | `list.exists_one(x, pred)` | legacy spelling of the exactly-one quantifier | | `filter` | `list.filter(x, pred)` | the sublist of elements satisfying the predicate | | `greatest` | `math.greatest(a, b, …)` | the maximum of the arguments (or of a single list argument) | | `has` | `has(msg.field)` | test whether a (possibly nested) message field is set | | `least` | `math.least(a, b, …)` | the minimum of the arguments (or of a single list argument) | | `map` | `list.map(x, expr)` | project each element through an expression (`list.map(x, pred, expr)` filters first) | | `optFlatMap` | `opt.optFlatMap(x, expr)` | like optMap but `expr` itself returns an optional (no double-wrapping) | | `optMap` | `opt.optMap(x, expr)` | map an optional's value through `expr` if present, else stay empty | | `sortBy` | `list.sortBy(x, key)` | the list sorted ascending by each element's computed key | | `transformList` | `list.transformList(i, x, expr)` | index-and-value list comprehension (`list.transformList(i, x, pred, expr)` filters first) | | `transformMap` | `map.transformMap(k, v, expr)` | rebuild a map, transforming each value (keys preserved) | | `transformMapEntry` | `map.transformMapEntry(k, v, expr)` | rebuild a map, transforming each entry into new key/value pairs | ### stdlib (always on)
Standard built-in functions — conversions, collection/string membership, size, regex `matches`, and the timestamp accessors — enabled everywhere, plus the standard operators and comprehension macros above. | Family | Functions | Notes | |---|---|---| | `stdlib` | `bool`, `bytes`, `contains`, `double`, `duration`, `dyn`, `endsWith`, `getDate`, `getDayOfMonth`, `getDayOfWeek`, `getDayOfYear`, `getFullYear`, `getHours`, `getMilliseconds`, `getMinutes`, `getMonth`, `getSeconds`, `in`, `int`, `matches`, `size`, `startsWith`, `string`, `timestamp`, `type`, `uint` | Conversions (`int`, `double`, `string`, `bytes`, `bool`, `timestamp`, `duration`, `dyn`, `type`), membership/size (`in`, `contains`, `size`, `startsWith`, `endsWith`, `matches`), and timestamp accessors (`getFullYear`, `getHours`, …). |
Two ways to explore this catalog interactively instead of reading it here: - **In the canvas:** the ƒx bar's **ƒ( )** button opens the function palette (grouped by the same families); typing in the bar autocompletes both in-scope fields and these functions. - **From the CLI / an agent:** `axiom flow mapping-scope [--edge ] [--json]` prints the functions *and* the exact variables in scope for a specific edge's mapping. ## Errors A CEL expression that fails to parse or compile, or whose type checker rejects a reference, is reported before the flow runs. A CEL expression that raises at evaluation time (a custom function given the wrong type, a runtime error from `cel-go`) surfaces on the failing edge. See [Edge adapter (CEL) errors](./error-catalog.md#edge-adapter-cel-errors) and the `CEL evaluation failed` / `CEL parse error` rows in [Flow run errors in the editor](./error-catalog.md#flow-run-errors-in-the-editor). --- ## Reference > Error catalog {#reference/error-catalog} > Every user-facing Axiom error — flow run failures in the editor, edge transform errors, and HTTP API error responses — with what causes each one and how to fix it. # Error catalog This page lists the errors Axiom shows users, grouped by where you see them: in the editor when a flow run fails, and in HTTP responses when you call the API. Each entry gives the exact error shape, the cause, and the fix. ## How errors name nodes and edges Flow run errors identify nodes by their **canvas id** — the per-placement id you see on the node card in the editor (for example `id-bad` or `node-1716492048192`) — never by the marketplace node name. This matters when a flow contains two placements of the same node: the error names the specific placement that failed, so you can find it on the canvas directly. Edges are named by quoting both endpoints. An edge error reads: ```text edge "id-bad" → "echo-merge": EvaluateEdge adapter: transform TRANSFORM_PREFIX failed: prefix needs a string to prepend, e.g. prefix("hi ") ``` Here `id-bad` and `echo-merge` are the canvas ids of the edge's source and destination nodes. When a failure is attributed to a specific edge or node, the result panel at the bottom of the editor also shows the failure kind (for example `EDGE_ADAPTER`) and a **Show on canvas** button. Clicking it selects the offending edge — or node, when the failure has no edge attribution — on the canvas so you can open and fix it. See [Debug a flow](../guides/debug-a-flow.md) for the full debugging workflow. ## Flow run errors in the editor When an execution fails, the flow status pill above the canvas shows **Failed**, the result panel header shows **Execution Failed**, and the panel's **Output** tab contains the error text. The errors you can see: | Error | Cause | Fix | |---|---|---| | `edge "" → "": EvaluateEdge adapter: ` | The edge's adapter (a bare CEL expression) failed to compile or evaluate at runtime — a parse error, a type mismatch, or an eval error. | Open the edge and fix the CEL expression. See [Edge adapter (CEL) errors](#edge-adapter-cel-errors). | | `edge "" → "": EvaluateEdge condition: ` | The edge's condition expression failed to evaluate (for example `CEL evaluation failed: …`). | Fix the condition expression on that edge. | | `invalid JSON for : ` | The JSON input you submitted does not match the entry node's input message — unknown field, wrong type. | Match the field names and types of the entry node's input message. See [Type system](../concepts/type-system.md). | | `graph produced no terminal result` | The execution ended without the terminal node producing output — typically every outgoing edge condition evaluated to false somewhere along the path. | Check edge conditions; make sure at least one path reaches the terminal node for this input. | | `exceeded max steps (1000)` | The execution dispatched more than 1000 node steps — almost always a loop that never meets its exit condition. | Fix the loop's exit condition. | | `... daily executions quota exceeded (cap )` | The flow tried to spawn child executions (parallel branches, runtime mutation) after the [daily execution quota](../reference/http-api.md#daily-quotas) ran out; the whole flow fails. | Wait for the midnight-UTC reset, or ask the operator for a higher limit. | | `downstream join failed: ` | A node feeding a join failed, and the join's failure policy aborted the flow. | Fix the failing branch, or relax the join's failure policy. | | `open pipeline stream for node ""`, `send to node ""`, `recv from node ""` | In pipeline mode, the platform lost the connection to that node's container. | Usually transient — re-run; if it persists, check the node's logs. | When your own node code raises an exception, the handler's error message is surfaced verbatim: it appears on the `NODE_FAILED` event in the result panel's **Execution** tab, attributed to the failing placement's canvas id. ## Edge adapter (CEL) errors An edge adapter's right-hand side is a bare CEL expression (or a plain field pick) evaluated against the source message. A failing expression produces a CEL error inside an edge error (see above). The failure modes: | Stage | Error | Meaning | |---|---|---| | Parse | `CEL parse error: ` | The expression is not syntactically valid CEL. | | Type-check / compile | `CEL compilation failed: ` (e.g. no matching overload, undeclared reference) | The expression is well-formed but ill-typed — e.g. `text + 1` mixes string and int, or it references a field/function that is not in scope. | | Eval | `CEL evaluation failed: ` | The expression compiled but failed against this input — e.g. an index out of range or a division by zero. | CEL string helpers such as `toUpper`, `toLower`, `trim`, `size`, and `join` are available in the adapter scope; call them directly (`toUpper(text)`), not through a pipe. A destination field path (the mapping *key*) that contains `(` or `)` is rejected at compile time as a malformed field name. **Whole message-typed field picks** copy the message byte-for-byte, so they are only valid between the **same** message type. These are rejected at **compile time** (never at run) — the fix is to reconstruct the destination in CEL, field by field: | Error | Meaning | Fix | |---|---|---| | `adapter: field pick "" maps message type "" into differently-typed message field "" (type "") …` | A whole-message pick across two *different* message types — a verbatim byte copy would misread the wire. | Map each leaf field explicitly in CEL (`".field": ".field"`), or use a CEL object literal, so the reshape is declared. | | `adapter: field pick "" maps a value into field "" … cannot convert between a message and a scalar …` | A pick tried to bridge a message field and a scalar field. | Write a CEL expression that constructs the destination shape. | | `adapter: field pick "" maps a message into a field "" … requires matching cardinality …` | A whole-message pick with mismatched cardinality (a repeated message into a singular field, or vice versa). | Reshape in CEL (e.g. index a single element, or `map(…)` a list). | ## HTTP API errors when invoking a flow Errors from `POST /invocations/v1/flows/invoke` and its streaming variant. For the full request/response contract see the [HTTP API reference](../reference/http-api.md). ### Authentication, rate limiting, and quotas | Status | Body | Cause | Fix | |---|---|---|---| | 401 | `{"error":"unauthorized"}` | Missing, malformed, revoked, or expired credentials on any authenticated route. | Send a valid key in the `Authorization: Bearer` header — see [Create and manage API keys](../guides/api-keys.md). | | 429 | `{"error":"rate limit exceeded"}` | Your tenant exceeded the per-tenant invocation rate limit (per-second burst). The response carries a `Retry-After: 1` header. | Back off and retry after the indicated delay. | | 429 | `{"error":"quota_exceeded", "retry_after_seconds": ...}` | Your tenant exhausted one of its [daily quotas](../reference/http-api.md#daily-quotas) — invocations or executions. The `message` names which; `retry_after_seconds` counts down to the reset at midnight UTC. | Wait for the daily reset, or ask the operator to raise your tenant's limit. | | 403 | `{"error":"tenant_suspended"}` | Your tenant has been disabled by the operator. All invocations and resumes are rejected. | Contact the operator. | | 503 | `{"error":"quota_unavailable", "retry_after_seconds": 5}` | The quota service could not be reached, so the request was rejected as a safety measure. | Transient — retry after the indicated delay. | ### Structured invoke errors Invoke failures with a known cause return a structured body: ```json { "error": "payload_too_large", "message": "payload exceeds hard cap: actual=17825792 max=16777216", "max_bytes": 16777216, "actual_bytes": 17825792 } ``` | Status | `error` class | Cause | Fix | |---|---|---|---| | 413 | `payload_too_large` | The input exceeds the 16 MiB hard cap (`max_bytes`/`actual_bytes` populated). | Shrink the input payload. | | 503 | `upstream_unavailable` | The flow could not be queued; a platform dependency was temporarily unavailable. `retry_after_seconds: 5`. | Transient — retry after the indicated delay. | | 503 | `blob_storage_unavailable` | A large input payload could not be stored; a platform dependency was temporarily unavailable. `retry_after_seconds: 5`. | Transient — retry after the indicated delay. | Failures that don't classify return `500` with `{"accepted": false, "error_message": ""}`. ### Timeouts With `"wait": true`, if the execution does not complete within the timeout (default 30 seconds), the response is still `202` — the execution keeps running: ```json { "accepted": true, "execution_id": "4bf92f3577b34da6a3ce929d0e0e4736", "error_message": "timeout waiting for flow result (30s)" } ``` Set `"timeout_seconds"` to wait longer, or look the `execution_id` up later in execution history. On the streaming endpoint, a timeout ends the stream with a final frame whose `error` is `"timeout waiting for pipeline result"`; a flow that fails mid-stream ends with a final frame whose `error` carries the flow run error described above. A request that is accepted and delivered but whose execution runs to a failure (for example, the input did not match the flow's start-node schema) returns HTTP `202` with `"result":{"success":false,...}` and surfaces the worker's diagnostic in both `result.error_message` and the top-level `error_message` — so a synchronous caller can tell a delivered failure apart from a timeout or transport error without polling execution history. ## HTTP API errors when invoking a single node Errors from the per-node JSON endpoint `POST /invocations/v1/nodes/{owner}/{pkg}/{version}/{node}` (also accepts a node id in place of the name path): | Status | Body | Cause | Fix | |---|---|---|---| | 404 | `{"error_message":"node not found or not yet deployed"}` (or `node not found: ` for a name path that doesn't resolve) | The node id or `{owner}/{pkg}/{version}/{node}` path doesn't match a published, deployed node. | Check the package name, version, and node name against the marketplace listing. | | 400 | `{"error_message":"invalid JSON for : "}` | The JSON body doesn't match the node's input message. | Match the input message's field names and types. | | 400 | `{"execution_id":"...","error_message":"invalid value for : field \"\" carries enum ordinal , which names no declared value ..."}` | An enum field is set to an ordinal that is not a declared value of its enum — typically a payload recorded before the field was an enum (or against a different schema version). | Re-record the payload against the node's current schema. | | 422 | `{"error_message":""}` | The node ran and its handler returned an error — `error_message` is the handler's own message. | Fix the input, or the node code. | | 502 | `{"error_message":"could not connect to node"}` / `"calling node: "` / `"sending to node: "` | The platform could not reach the node's container. | Usually transient (cold start) — retry; if it persists, check the node's deployment. | The binary-protobuf variant `POST /invocations/v1/nodes/invoke` validates its base64 `payload` against the node's current input schema before forwarding: bytes whose wire shape doesn't match (an undeclared field number, or a wire type conflicting with the declared kind), or any payload whose node schema can't be resolved, return HTTP 400 with `{"success":false,"error_message":"payload does not match the node's current input schema: "}`. ## HTTP API errors when pre-warming a flow Errors from `POST /invocations/v1/flows/warm` (see [Pre-warm a flow](../reference/http-api.md#pre-warm-a-flow)). Authentication, rate-limit, and quota failures return the same bodies as [invoking a flow](#authentication-rate-limiting-and-quotas) above — a warm debits the same daily invocation quota an invoke does. Pre-warm adds these ownership and availability cases: | Status | Body | Cause | Fix | |---|---|---|---| | 400 | `{"error":"invalid_request","message":"..."}` | `graph_id` is missing or doesn't match `[A-Za-z0-9_-]+`. | Pass a valid `graph_id`. | | 403 | `{"error":"forbidden","message":"not authorized to warm this flow"}` | The authenticated tenant does not own `graph_id`. | Warm a flow your tenant owns; a foreign `graph_id` is never accepted, even to pre-warm it. | | 404 | `{"error":"not_found","message":"flow not found"}` | `graph_id` doesn't resolve to any flow. | Check the id against the flow's **Use via API** dialog. | | 503 | `{"error":"warm_unavailable","message":"...","retry_after_seconds":5}` | The warm dispatcher isn't configured on this deployment. | Transient/deployment-specific — retry after the indicated delay, or invoke the flow directly instead. | ## Execution history and debug API errors Errors from the execution-history and debugging endpoints (used by [Debug a flow](../guides/debug-a-flow.md)): - Execution history and event requests return `404` with `{"error":"execution not found"}` when the execution id doesn't exist for your tenant. - `GET /invocations/v1/executions/{id}/checkpoints/{checkpoint_id}` returns `404` with `{"error":"checkpoint_not_found_or_expired","detail":"checkpoint may have been evicted by TTL policy (default 30d)"}` — checkpoints expire after a retention window (default 30 days), so debug a recent execution instead. - Agent memory operations return standard statuses: `404` when a memory entry or session is not found, `400` for invalid arguments, `403` for permission denied, `401` for unauthenticated requests, and `429` when resource limits are exhausted. All endpoints are tenant-scoped: a valid id belonging to another tenant behaves exactly like a nonexistent one (`404`), never `403` — see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md). --- ## Reference > Glossary {#reference/glossary} > Canonical definitions for every Axiom term — the single authority for terminology used across the docs, the CLI, the canvas, and the API. # Glossary This page is the authority for Axiom terminology. Every concept has exactly one name; the docs never use a synonym ("component", "workflow", "graph") where one of these terms applies. ## Node A **node** is the single unit of compute in Axiom: a user-written handler function with a typed input message and a typed output message, both defined in Protocol Buffers. Node code runs in an isolated container and interacts with the platform only through the sidecar. Nodes are declared in a package's `axiom.yaml` and become available to flows once the package is published. A node is either *unary* (one input message in, one output message out per invocation) or a *pipeline* node (a streaming generator that emits a sequence of output frames). ## Generic node A **Generic node** is a published, deployed node whose input and/or output port is declared with generalized fields — broad enough to cover every use of the node, but not the specific shape any one flow needs — because the author can't know that shape at publish time (a database query's columns, an HTTP call's response fields). Declared with `kind: generic` and `generic_ports` in `axiom.yaml`. A Generic node cannot be placed in a flow by itself; see **Instance**. ## Instance An **Instance** binds a [Generic node](#generic-node)'s port(s) to a real, specific type via a field mapping, created with `axiom instance create` (or the marketplace's Generic Nodes tab) — never hand-authored in `axiom.yaml`. A bound field may itself be another message, defined inline and recursively, with `repeated`/`optional` modifiers — see [Configure an Instance from a Generic node](../guides/configure-an-instance.md). It carries no code of its own: it shares the Generic node's already-built, already-deployed image, so creating one triggers no git commit, no build, and no Knative deploy. Once created, an Instance is placed in a flow exactly like a concrete node. ## Package A **package** is the unit of publishing: a versioned bundle of node definitions and Protocol Buffers message types, described by an `axiom.yaml` manifest (name, version, language, nodes, imports). Running `axiom push` publishes a package to the marketplace, where its nodes can be placed into flows. A *proto-only* package contains no nodes — only message types for other packages to import. ## Flow A **flow** is a directed graph of nodes composed in the canvas (or via the API). An edge from node A to node B means "A's output message is B's input message"; edge type compatibility is checked at compile time. Invoking a flow always means executing its compiled artifact, never an editable draft. ## Message A **message** is a Protocol Buffers message: the only data type that crosses a node boundary. Every node input, node output, and edge payload is a message. Message types are defined in `.proto` files inside packages and may be imported across packages. ## Execution An **execution** is a single run of a flow, from invocation to completion, identified by an execution ID that the platform assigns when the request is accepted. The execution ID is threaded through every hop — traces, debug events, results, and the HTTP API all reference it. ## Entry node The **entry node** is the node where an execution starts: the flow's single node with no incoming edges. The entry node's input message defines the flow's input schema — it is what callers send when invoking the flow via the API. ## Terminal node The **terminal node** is the node where an execution ends: the flow's single node with no outgoing edges. Its output message defines the flow's result schema; an execution completes when the terminal node finishes. ## Compiled artifact A **compiled artifact** is the immutable, optimized representation of a flow that the platform produces at compile time and that workers execute. Compile validates edge type compatibility and resolves every node placement; once compiled, an artifact never changes — editing a flow produces a new artifact. ## Sidecar The **sidecar** is the platform proxy that runs alongside every node container and is the node's only channel to the platform. All platform capabilities — logging, secrets, memory, telemetry — reach node code through the sidecar, and the sidecar enforces tenant isolation. Node code never calls platform services directly. ## Tenant A **tenant** is an isolated account scope. Every execution carries the tenant identity resolved from the authenticated API key, and the platform — not node code — enforces that one tenant's data (secrets, memory, results) is never visible to another. ## Pipeline mode **Pipeline mode** is the streaming execution mode of a flow: instead of one request producing one response (*unary* mode), frames flow continuously between nodes and results are delivered to the caller progressively as a stream. A flow runs in pipeline mode when it contains pipeline nodes. ## Session A **session** is the scope of one conversation or interaction in agent memory, identified by a session ID that always comes from the caller's typed input message — the platform never infers or injects it. Episodic memory (the record of what happened) is scoped to a session. ## Consolidation **Consolidation** is the background process that promotes episodic memories (session-scoped records of what happened) into durable semantic facts that persist across sessions — for example, turning repeated conversation turns into "this user prefers TypeScript." ## Flow config A **flow config** is a named profile of parameter values (and secret references) attached to a flow. Callers select a config by name at invocation time; the platform resolves the profile and injects the resolved values into the execution, so the same flow runs with different settings — say, `staging` and `production` — without recompiling. ## Secret A **secret** is a named, tenant-scoped credential (an API key, a token) stored in the console and read by node code through `AxiomContext` at runtime. Secrets never appear in package source or flow definitions; a node declares the secret names it needs in `axiom.yaml` (`required_secrets`) so users know what to register before invoking. ## Client A **client** is a named, account-scoped bundle of marketplace nodes and flows, assembled in **Console → Client Builder**, that compiles into a typed SDK. Each bundled node or flow is a *member* with a unique *alias* that becomes its method name in the SDK. Building a client produces an immutable, numbered *version* — a frozen snapshot of its members — that can be compiled into any of six languages (Python, Go, TypeScript, Java, C#, Rust). See [Build a client SDK](../guides/build-a-client-sdk.md). --- ## Releases > Axiom CLI v0.2.0 release notes {#releases/cli-v0.2.0} > What's new in the v0.2.0 Axiom CLI: the new axiom client command group, breaking changes, and other changes since v0.1.1. # Axiom CLI v0.2.0 This release adds the `axiom client` command group for building typed SDKs, a typed `axiom instance create` wrapper, and live mapping preview, along with one breaking change (the `axiom db` command group was removed). ## Highlights - **New `axiom client` command group** — create and manage Client Builder clients (typed SDKs generated from marketplace nodes/flows): `create`, `list`, `get`, `add`/`members`/`remove-member`, `build`, `status --watch`, `download`, and `sync` (a declarative, kubectl-apply-style reconciler driven by an `axiom-client.yaml` manifest). See [Build a client SDK](../guides/build-a-client-sdk.md) and [reference/cli/axiom-client.md](../reference/cli/axiom-client.md). - **`axiom instance create`** — a typed CLI wrapper that type-binds a Generic node into a concrete Instance (no build, no deploy). - **`axiom flow preview`** — preview the exact value an edge adapter or compose mapping produces for sample input, without a full compile+run round-trip. ## Breaking changes - **The entire `axiom db` command group has been removed.** `axiom db gen` and `axiom db check` (schema introspection, inference, and drift-checking for the DB-schema-import path) are gone, along with the corresponding frontend "Import from axiom db gen" affordance. Result schemas are now hand-authored — either in the result-schema editor in the console, or by hand-typing the `result_schema:` block in `axiom.yaml`. There is no replacement command — scripts or docs invoking `axiom db gen` / `axiom db check` will need to switch to manual authoring. ## Other changes since v0.1.1 - **Generic/Instance nodes:** `axiom create node --generic` authoring support (a node schema field plus author-time validation), the `axiom instance create` wrapper above, and end-to-end support from `axiom create node --generic` → `axiom instance create` → reference in a flow → compile + run. - **Reliability/retry authoring:** `axiom validate` now checks a package-author `retry:` section in `axiom.yaml`; the overall-timeout budget and `retry_policy` gained a compiler/CLI authoring surface; the `node_timeout_seconds` field was removed entirely in favor of flow config. The docs now include idempotency guidance for at-least-once delivery. If you were passing `--timeout` / setting `node_timeout_seconds`, remove it — the field no longer exists. - **Failure-edge / compensation authoring:** `Edge.OnError` authoring for error-sourced routing, flow.yaml authoring for `result_schema` facades, and the compensation-link authoring path (`compensation_node_name`). - **Live mapping preview:** `axiom flow preview` (above) plus the underlying live-preview support for edge adapters and compose plans. - **Skills:** a new `axiom-client-authoring` Claude Code skill; the `axiom-package-authoring` and `axiom-flow-authoring` skills were updated for retry-config authoring, config-derived result-schema typing, and the `axiom db` removal. - **Docs:** user docs and Claude Code skills refreshed for generic/instance nodes, and a broken "Open interactive docs" (OpenAPI explorer) link fixed. - **Dependencies:** `aws-sdk-go-v2` bumped to clear `GO-2026-5764`. ---