## 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.1.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, secrets, and memory 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. ## Point the CLI at your deployment The CLI reads two environment variables to locate your Axiom deployment. Your Axiom host provides the values for these variables in your account settings or deployment configuration: | Variable | Purpose | |---|---| | `AXIOM_API_URL` | Platform API (login, push, search) | | `AXIOM_INGRESS_URL` | Flow invocation endpoint (base origin, no path) | ```bash # e.g. in ~/.bashrc or your CI environment export AXIOM_API_URL="https://api.example-axiom-host.com" export AXIOM_INGRESS_URL="https://flows.example-axiom-host.com" ``` `AXIOM_INGRESS_URL` is the base origin only — no `/invocations` or other path suffix. Set these before running `axiom login`: the stored credential is just the API key, so switching `AXIOM_API_URL` to a different deployment requires logging in again against that deployment. ## 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), `ax.secrets.get(name)` for tenant secrets (returns a `(value, found)` tuple — see [manage secrets](/docs/guides/manage-secrets)), and `ax.agent.memory` for durable agent memory (declare the function `async def` to `await` memory calls — see [memory](/docs/concepts/memory)). 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; each push overwrites the previous one, which is how you iterate. 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 (e.g. `https://app.axiomide.com`); the CLI already knows this value and prints the editor URL for you after `axiom push`/`axiom login`. Open `/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 on your deployment's origin — `/invocations/v1/flows/invoke`, or `/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://flows.example-axiom-host.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://flows.example-axiom-host.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, memory) arrive through a single injected context argument, `AxiomContext`: ```python # nodes/process_order.py from gen.messages_pb2 import OrderRequest, OrderConfirmation from gen.axiom_context import AxiomContext def process_order(ax: AxiomContext, input: OrderRequest) -> OrderConfirmation: """Accepts an order request and returns a confirmation for it.""" return OrderConfirmation(order_id=input.order_id, accepted=True) ``` The same shape holds in every supported language — in a Go package (`axiom init acme/orders --language go`), the same node looks like this: ```go // nodes/process_order.go package nodes import ( "context" "acme/orders/axiom" gen "acme/orders/gen" ) // ProcessOrder accepts an order request and returns a confirmation for it. func ProcessOrder(ctx context.Context, ax axiom.Context, input *gen.OrderRequest) (*gen.OrderConfirmation, error) { return &gen.OrderConfirmation{OrderId: input.OrderId, Accepted: true}, nil } ``` `axiom create node ` scaffolds the implementation file and a matching test file in `nodes/`, and adds the node to `axiom.yaml`. The doc comment attached to the function (the Python docstring, or the Go comment directly above `func`) is extracted at publish time and shown in the registry as the node's description. By default a node is *unary*: one input message in, one output message out per invocation. Declaring `type: pipeline` in `axiom.yaml` makes it a streaming generator that emits a sequence of output frames instead — see the [execution model](./execution-model.md). Node code is sandboxed: it runs in a container, talks to the platform only through the sidecar, and cannot reach other tenants' data — see [sandboxing and tenancy](./sandboxing-and-tenancy.md). Per-language guides: [Python](../guides/create-a-node-python.md), [Go](../guides/create-a-node-go.md), [TypeScript](../guides/create-a-node-typescript.md), [Rust](../guides/create-a-node-rust.md), [Java](../guides/create-a-node-java.md), [C#](../guides/create-a-node-csharp.md). ## Message: the data contract between nodes Every node input, node output, and edge payload is a Protocol Buffers message. A package's messages all live in one file, `messages/messages.proto`, so they can reference each other without import statements. Scaffold one with the CLI: ```bash # Run inside the package directory (where axiom.yaml lives) axiom create message OrderRequest --fields "order_id:string; total:double" ``` `--fields` accepts canonical proto3 (`string order_id = 1`), proto3 without field numbers, or `name:type` shorthand; field numbers are auto-assigned when omitted. After adding the message, `axiom generate` runs automatically to produce language bindings in `gen/`. Comments written directly above a message or field (no blank line between) are extracted at publish time and shown in the registry as documentation. Messages can be shared across packages: a package declares `imports` in `axiom.yaml` and pulls the proto definitions down with `axiom import`. The [type system](./type-system.md) page covers contracts and compatibility; [import types from another package](../guides/import-package-types.md) covers the workflow. ## Package: the unit of publishing A package is a versioned, single-language bundle of nodes and message types, described by an `axiom.yaml` manifest. `axiom init ` creates the package directory (named after the part of the package name following the last `/`) with `axiom.yaml`, `messages/`, `nodes/`, and `gen/`. Supported languages: `go`, `python`, `rust`, `java`, `typescript`, `csharp`. ```yaml # axiom.yaml name: acme/orders version: 0.1.0 language: python description: Order intake and confirmation nodes nodes: - name: ProcessOrder input: OrderRequest output: OrderConfirmation ``` Each node entry names its input and output messages; optional fields include `type: pipeline` (streaming node) and `required_secrets` (the secret names the node reads at runtime, displayed in the marketplace). The full schema is in the [axiom.yaml reference](../reference/axiom-yaml.md). A package with no nodes (or with `type: proto-only`) is a *proto-only* package: it carries only message types for other packages to import, and publishing it skips the container build and deployment entirely. Deployment is two-stage. `axiom push` validates the package and pushes it **tenant-private**: only your tenant sees it, and pushing the same version again overwrites the previous push, so you can iterate. Publishing — with `axiom publish @` (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:///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. 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://flows.example-axiom-host.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` — optional `{"url": "...", "headers": {...}}`; for asynchronous runs the result is delivered by HTTP POST to that URL instead. - `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://flows.example-axiom-host.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 | | 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. 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. 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. --- ## 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, using agent memory, 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`, `ax.secrets`, and `ax.agent.memory` 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; across executions, use agent memory (see [Agent memory](../concepts/memory.md)). ## 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. - **Use agent memory** — `ax.agent.memory`, durable across executions, scoped to the flow and tenant. - **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, memory — is accessed through `ax`. Node code that attempts to dial platform endpoints directly will not reach them. - **Choose or forge its tenant.** The tenant and flow identity used for memory access are fixed on the sidecar when the package is deployed — they live in the sidecar container, not yours. The memory proxy overwrites the tenant and flow fields on every memory call, so whatever values node code puts there are ignored. - **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 three checkpoints, none 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)). 3. **At the sidecar.** The sidecar's memory proxy stamps the deploy-time tenant and flow identity onto every memory read and write, discarding anything node code supplied. Agent memory therefore cannot cross tenant boundaries even if a node tries. 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. ## 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. - Agent memory is isolated per tenant and scoped per flow — no other tenant, and no other flow, reads your flow's memory. - One tenant's executions never observe another tenant's payloads, secrets, or memory. - 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. --- ## Concepts > Agent memory {#concepts/memory} > How flows remember things across executions: sessions, episodic conversation history, consolidated semantic memories, and the axiom memory CLI. # Agent memory ## What agent memory is Agent memory is Axiom's built-in durable store that lets a flow remember things across executions. Each execution of a flow is otherwise stateless: a node receives its input message, returns its output message, and the execution context is gone. Memory fills that gap — a customer-support flow can recall what a user said yesterday, and accumulate facts about its task over many executions. Node code reaches memory through one entry point: `ax.agent.memory` on `AxiomContext`. There is nothing to provision and no connection string to manage. Memory is scoped to the flow: every execution of the same flow shares one memory store, and different flows have isolated stores. Tenant isolation is enforced by the platform, not by your code — the sidecar stamps your tenant and flow identity onto every memory call, and whatever a node puts in those fields is ignored (see [Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)). Outside node code, the `axiom memory` CLI lists, inspects, searches, and deletes memory, and the flow debugger shows memory reads and writes live as a flow runs. ## The three memory tiers Axiom separates "memory" into three tiers with different lifetimes: ### Working memory: the typed payload The message flowing through the flow *is* the working memory. If a node needs conversation context within a single execution, that context belongs in its input and output message types. This tier is explicit, typed by Protocol Buffers, and needs no memory API at all. See [Type system](../concepts/type-system.md). ### Episodic memory: conversation history per session Episodic memory is the time-ordered record of conversation turns within a session. Each turn has a `role` (`user`, `assistant`, `tool`, or `system`), a `content` string, and a timestamp. Node code appends turns and reads back the last N turns through the session API (below). Episodic memory is scoped to `(tenant, flow, session)` and is retained for a platform-configured period (90 days by default). ### Semantic memory: long-term facts per flow Semantic memory is the flow's long-term knowledge: concise factual statements distilled from episodic history by a background consolidation process (below). Each entry carries an `importance` score between 0 and 1 and a `confidence` score. Semantic memory is scoped to `(tenant, flow)` — it persists across sessions and executions — and is retrieved by semantic search. Node code can also write facts directly with `ax.agent.memory.write(...)`. ## Sessions A session groups related conversation turns — typically one user's conversation thread. Your node code chooses the session ID (a string, usually taken from the input message) and addresses it with `ax.agent.memory.session(session_id)`. The session object gives you: - `history.append(role=..., content=...)` — record a turn. - `history.last(n)` — read the most recent `n` turns. - `search(query, limit=5)` / `write(content, importance=0.5)` — session-scoped memory entries. - `end()` — formally close the session and trigger consolidation. Sessions are not opened or declared anywhere; appending the first turn to a new session ID creates it. A session can also be closed from outside node code with `axiom memory end `. ## How consolidation works Consolidation turns a session's episodic history into permanent semantic memory. It runs when a session is ended — either node code calls `session.end()` or you run `axiom memory end `. When a session ends, the platform reads its conversation history, extracts distinct facts and preferences as concise statements, and stores them as flow-scoped semantic memories. Near-duplicate facts are merged rather than duplicated, so the same piece of information does not accumulate as redundant entries over time. Consolidation is non-destructive — the conversation history remains readable after it runs. Extracted facts typically appear within a minute of ending a session. Until a session has been ended and consolidated, `axiom memory show` reports no semantic memories for it (facts written directly with `write(...)` are the exception — they appear immediately). ## How memory search ranks results `ax.agent.memory.search(query)` (and `axiom memory search`) searches the flow's memories and returns ranked results. The search combines keyword matching with semantic similarity, so relevant entries surface whether the query uses the exact same words or a different phrasing. Every returned entry includes its relevance `score`. The CLI also reports the retrieval method and the query latency in milliseconds. ## Read and write memory from node code Memory is available in every SDK language through `AxiomContext`. The example below is Python (see the per-language guides for the others); it assumes a package whose messages define `ChatRequest` with `session_id` and `text` fields and `ChatReply` with a `text` field. Declare the handler `async def` to `await` the memory APIs. ```python # nodes/chat_agent.py — node file in a Python package (axiom create node ChatAgent) from gen.messages_pb2 import ChatRequest, ChatReply from gen.axiom_context import AxiomContext async def chat_agent(ax: AxiomContext, input: ChatRequest) -> ChatReply: """Replies using recent conversation turns and long-term facts.""" session = ax.agent.memory.session(input.session_id) # Episodic: record this turn, then load recent history. await session.history.append(role="user", content=input.text) recent = await session.history.last(20) # Semantic: retrieve facts consolidated from earlier sessions of this flow. facts = await ax.agent.memory.search(input.text, limit=5) reply = f"I see {len(recent)} recent turn(s) and {len(facts)} relevant memorie(s)." await session.history.append(role="assistant", content=reply) return ChatReply(text=reply) ``` `ax.agent.memory.write(content, importance=0.5)` stores a flow-scoped fact directly and returns the new memory's ID; `session.write(...)` does the same at session scope. You never pass tenant or flow identifiers — the platform injects them. ## Inspect memory with the CLI The `axiom memory` command group inspects and manages agent memory from your terminal. All commands require an active login (`axiom login`); memory data lives in the Axiom platform, with no local state. ```bash # Requires: axiom CLI installed and logged in (axiom login) axiom memory ls # flows that have memory axiom memory ls --flow # sessions for one flow axiom memory show # conversation + semantic memories axiom memory search --flow "the query" # semantic search over a flow's memories axiom memory end # close a session, trigger consolidation axiom memory rm # delete one session axiom memory rm --flow --all # delete all memory for a flow ``` `rm` deletes permanently and `end` closes a session; both ask for confirmation unless you pass `--yes`. For a worked walkthrough, see [Inspect agent memory](../guides/inspect-agent-memory.md); for every flag, see the [`axiom memory` CLI reference](../reference/cli/axiom-memory.md). While debugging a run, the flow debugger also displays each memory read and write as it happens — see [Debug a flow](../guides/debug-a-flow.md). --- ## 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` — for example with the `ax.agent.memory` 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.agent.memory` — durable agent memory; see [Agent memory](../concepts/memory.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 in-memory `AxiomContext` with a no-op logger, a secrets map you can populate (`_TestContext(secrets_map={"OPENAI_KEY": "sk-test"})`), and stub agent memory. 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. ## 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 push overwrites the previous one — which lets you iterate on deployed code before publishing. When you are satisfied, publish the package through 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)), agent memory (`ax.Agent().Memory()` — see [memory](/docs/concepts/memory)), 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 push overwrites the previous one — 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 use the Promise-based memory API (`await ax.agent.memory.session(id).history().last(20)`). ## 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.agent.memory` — session history and agent memory (see [Agent memory](../concepts/memory.md)). - `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. 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, publish through 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 the only way node code reaches the platform — there is no direct network access from a node (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.agent().memory()` — agent memory: session-scoped history and semantic search/write (see [memory](../concepts/memory.md)). - `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 push overwrites the previous one — so you can iterate on deployed code. 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, agent memory, 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.agent().memory()` — durable agent memory: `search(query, limit)`, `write(content, importance)`, and per-session scope via `session("s1")` with conversation history (`history().last(20)`, `history().append(role, content)`). See [Memory](../concepts/memory.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). ## 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, memory returns 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; 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). - **Agent memory** — `ax.Agent().Memory()` exposes `Search(query, limit)` and `Write(content, importance)`, plus `Session(sessionId)` for session-scoped memory with conversation `History()` (`Last(n)`, `Append(role, content)`) and `End()`. See [memory](/docs/concepts/memory). - **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. ## 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 overwrites the previous push. 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. 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#. ## 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 > 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. - **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"}`. ## 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 — node timeout and 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, a node timeout and optional 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)). ## Set the tenant default node timeout The shortest path to changing config for every flow at once: 1. Click **Console** in the top header, then **Flow Configs** in the console sub-nav (the page is at `/console/flow-configs`). 2. Leave the **Flow** picker on **Tenant default (all flows)** — that is the default selection. 3. Edit **Default node timeout (seconds)**. The field accepts 1–3600 and saves when you click away. The platform default is **300 seconds** per node invocation. The tenant default replaces it for all of your flows; a per-flow profile overrides both for one flow. ## 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. Set **Node timeout (s)**. 5. 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**. 6. Click **Create**. Each profile row lists its name, its timeout, 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:///invocations/v1/flows/invoke" \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "graph_id": "", "input": {"name": "Ada"}, "wait": true, "config_id": "load-test" }' ``` ## How config values are resolved At invocation time the platform merges up to four layers, later layers overriding earlier ones parameter by parameter: 1. **Platform defaults** — `node_timeout_seconds: 300`. 2. **Tenant default** — your config named `default` that is not scoped to any flow (the one the Flow Configs page edits under "Tenant default"). 3. **Flow default** — the invoked flow's profile named `default`, when one exists. 4. **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. - **`node_timeout_seconds` is consumed by the platform, not by your code.** The worker bounds each node invocation with it; a node that exceeds the timeout fails that invocation. If the value is missing or invalid, the 300-second default applies. - **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. --- ## 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 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 /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 `/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 host with your deployment's origin and 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://app.example-axiom-host.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, 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 you assemble in the console — 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. 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` | ## 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 # Replace with your Axiom host and with your compiled flow's id curl -X POST '/invocations/v1/flows/invoke' \ -H "Authorization: Bearer $AXIOM_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"graph_id": "", "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 > Inspect agent memory {#guides/inspect-agent-memory} > List, inspect, search, close, and delete agent memory sessions for your flows with the axiom memory CLI commands. # Inspect agent memory When a flow's node code uses agent memory (`ax.agent.memory` in the SDK), the platform stores conversation turns and extracted facts per session. The `axiom memory` command group lets you see exactly what a flow has remembered, search it, close sessions to trigger consolidation, and delete data — all from the terminal. ## Prerequisites - The Axiom CLI installed and logged in (`axiom login`) — see [Installation](../getting-started/installation.md). Every `axiom memory` command requires an active login; memory data lives in the Axiom platform, so there is no local state to manage. - A flow whose node code writes agent memory. If you don't have one yet, see [Where agent memory comes from](#where-agent-memory-comes-from) below and the [memory concept page](../concepts/memory.md). You only ever see memory belonging to your own tenant — isolation is enforced by the platform, not by the CLI. ## List flows and sessions that have memory Run `axiom memory ls` with no flags to see every flow that has at least one memory session: ```bash axiom memory ls ``` ```text FLOW ID SESSIONS LAST ACTIVE 01JX4Q4VJ4N4CW9V1T08CYJWWG 2 2026-06-06 14:02:11 UTC ``` The table shows one row per flow with its session count and the most recent activity timestamp (UTC). Sessions written without a flow ID are grouped under `(no flow)`. Add `--flow` to drill into one flow's sessions: ```bash axiom memory ls --flow 01JX4Q4VJ4N4CW9V1T08CYJWWG ``` ```text SESSION ID TURNS LAST ACTIVE support-thread-42 14 2026-06-06 14:02:11 UTC ``` Each row is a session with its conversation turn count. Use the session IDs from this table with `axiom memory show`, `axiom memory end`, and `axiom memory rm`. ## Show a session's conversation and semantic memories Run `axiom memory show ` to print the session's full conversation history followed by its extracted semantic memories: ```bash axiom memory show support-thread-42 ``` ```text • Session support-thread-42 Conversation USER 2026-06-06 14:01:58 UTC What export formats do you support? ASSISTANT 2026-06-06 14:02:11 UTC CSV and JSON are both supported. · 2 turn(s) total Semantic memories TYPE IMPORTANCE CONTENT semantic 0.80 User works with CSV and JSON exports. · 1 entr(ies) found ``` In the conversation section, each turn shows its role (`USER`, `ASSISTANT`, or `TOOL`), a UTC timestamp, and the content. Turns produced by a tool call are prefixed with the tool name in brackets, e.g. `[web-search] ...`. The semantic memories table lists each entry's type (`episodic`, `semantic`, or `procedural`), its importance (a 0–1 weight), and its content (truncated to 80 characters for display). If the section says "No semantic memories yet", the session has history but has not been consolidated — see [Close a session and trigger consolidation](#close-a-session-and-trigger-consolidation). ## Search a flow's memories Run `axiom memory search` with a required `--flow` flag and exactly one query argument (quote multi-word queries): ```bash axiom memory search --flow 01JX4Q4VJ4N4CW9V1T08CYJWWG "preferred output format" ``` ```text CONTENT IMPORTANCE SCORE User works with CSV and JSON exports. 0.80 0.0214 · 1 result(s) — method: hybrid, latency: 42ms ``` The search is hybrid: it combines keyword matching with semantic similarity, so results match on both exact wording and meaning. Each result's `SCORE` combines that query relevance with the entry's importance and recency, so an old low-importance match ranks below a fresh important one. The footer reports the total number of matches, the retrieval method, and the query latency in milliseconds. ## Close a session and trigger consolidation Run `axiom memory end ` when a conversation is complete: ```bash axiom memory end support-thread-42 ``` ```text End session support-thread-42 and trigger consolidation? [y/N] y ✓ Session support-thread-42 ended. Consolidation enqueued. ``` Ending a session enqueues it for consolidation: a background job reads the session's conversation history, extracts durable semantic facts from it, deduplicates them against existing entries, and stores them as flow-scoped semantic memories. Those facts are what `axiom memory show` lists under "Semantic memories" and what `axiom memory search` finds. Two things to know: - **It is non-destructive.** The conversation history remains accessible after the session ends. - **It is asynchronous.** Semantic memories appear once the background job has run, not at the moment the command returns. Pass `--yes` to skip the confirmation prompt (for scripts): ```bash axiom memory end support-thread-42 --yes ``` ## Delete a session or all memory for a flow Run `axiom memory rm ` to permanently delete one session — its conversation history and its memory entries: ```bash axiom memory rm support-thread-42 ``` ```text Permanently delete session support-thread-42? [y/N] y ✓ Deleted session support-thread-42 (14 record(s) removed). ``` To delete every session and memory entry for a flow at once, pass `--flow` together with `--all` (and no session ID): ```bash axiom memory rm --flow 01JX4Q4VJ4N4CW9V1T08CYJWWG --all ``` ```text Permanently delete ALL memory for flow 01JX4Q4VJ4N4CW9V1T08CYJWWG? [y/N] y ✓ Deleted all memory for flow 01JX4Q4VJ4N4CW9V1T08CYJWWG (37 record(s) removed). ``` Deletion is permanent — there is no soft-delete or undo. `--all` requires `--flow`; running `axiom memory rm` with neither a session ID nor `--flow --all` is an error. Both forms ask for confirmation first and report how many records were removed; pass `--yes` to skip the prompt. ## Where agent memory comes from The `axiom memory` commands read data that node code wrote through `AxiomContext`. A node opens a session by ID and appends conversation turns to it — the session ID is whatever string the node chooses, which is why session IDs in `axiom memory ls --flow` output look like your application's identifiers rather than platform-generated ones: ```python # nodes/chat.py — a Python node that writes the memory this guide inspects from gen.messages_pb2 import ChatRequest, ChatReply from gen.axiom_context import AxiomContext async def chat(ax: AxiomContext, input: ChatRequest) -> ChatReply: session = ax.agent.memory.session(input.session_id) await session.history.append(role="user", content=input.text) turns = await session.history.last(20) return ChatReply(reply=f"history has {len(turns)} turn(s)") ``` Beyond `history.append` and `history.last(n)`, node code can store a fact directly with `session.write(content, importance=0.5)`, search with `session.search(query, limit=5)`, and close the session from inside the flow with `session.end()` — the in-code equivalent of `axiom memory end`. See the [Python SDK reference](../reference/sdk/python.md) for the full interface and the [memory concept page](../concepts/memory.md) for how sessions, consolidation, and memory types fit together. By default the CLI sends memory requests to the platform's public API endpoint. Set the `AXIOM_MEMORY_URL` environment variable to point the commands at a different base URL (useful when testing against a local development environment). --- ## Guides > Author packages and flows with Claude Code {#guides/author-with-claude-code} > Install Axiom's authoring Skills into Claude Code with axiom skills install so your agent knows the create→validate→push→publish loop and drives the axiom CLI through the shell. # Author packages and flows with Claude Code Axiom lets [Claude Code](https://www.anthropic.com/claude-code) author packages and flows for you through **Skills** — Markdown contracts that teach the agent the authoring 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 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-package-authoring → .claude/skills/axiom-package-authoring ✓ axiom-flow-authoring → .claude/skills/axiom-flow-authoring Installed 2 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-package-authoring # axiom-flow-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, fan-out, and nested subflows. | ## 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-flow-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. --- ## 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 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 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 login](./axiom-login.md) | Authenticate with the Axiom platform | | [axiom memory](./axiom-memory.md) | Inspect and manage agent memory for your flows | | [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 authoring Skills for Claude Code | | [axiom test](./axiom-test.md) | Run language-native tests with axiom validation | | [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 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 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 login](./axiom-login.md) — Authenticate with the Axiom platform - [axiom memory](./axiom-memory.md) — Inspect and manage agent memory for your flows - [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 authoring Skills for Claude Code - [axiom test](./axiom-test.md) — Run language-native tests with axiom validation - [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 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) | | `--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 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 new](./axiom-flow-new.md) | Scaffold a starter flow.yaml | | [axiom flow publish](./axiom-flow-publish.md) | Publish a compiled flow 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 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 new](./axiom-flow-new.md) — Scaffold a starter flow.yaml - [axiom flow publish](./axiom-flow-publish.md) — Publish a compiled flow 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 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. ADR-086. ## 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 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 publish {#reference/cli/axiom-flow-publish} > Publish a compiled flow to the public marketplace # axiom flow publish Publish a compiled flow to the public marketplace Publish a compiled flow — the artifact id printed by "axiom flow compile" — to the public marketplace. Publishing sets the flow's visibility to MARKETPLACE_FREE: every Axiom tenant can then discover it, fork it, and import it as a subflow. This is CLI parity with the editor's Publish action. Publishing is immutable — a published flow cannot be edited or unpublished. Iterate with "axiom flow compile" (which produces a fresh artifact), then publish the new artifact when you are satisfied. ```text axiom flow publish 01J… axiom flow publish 01J… --yes # skip the prompt (CI) ``` Requires a prior "axiom login". ADR-086. ## 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 ). ADR-086. ## 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 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. Requires a prior "axiom login". Examples: ```text axiom import payments axiom import payments@2.0.1 ``` ## 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 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 memory {#reference/cli/axiom-memory} > Inspect and manage agent memory for your flows # axiom memory Inspect and manage agent memory for your flows Inspect and manage agent memory sessions for your Axiom flows. All commands require an active login (run "axiom login" to authenticate). Memory data lives in the Axiom platform — there is no local state. Examples: ```text axiom memory ls # list flows that have memory axiom memory ls --flow # list sessions for a flow axiom memory show # show conversation + semantic memories axiom memory search --flow # semantic search over memories axiom memory end # close a session (triggers consolidation) axiom memory rm # delete a session axiom memory rm --flow --all # delete all memory for a flow ``` ## Usage ```sh axiom memory [flags] axiom memory [command] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for memory | ## Subcommands | Command | Description | |---|---| | [axiom memory end](./axiom-memory-end.md) | Close a session and trigger memory consolidation | | [axiom memory ls](./axiom-memory-ls.md) | List flows with memory, or sessions for a specific flow | | [axiom memory rm](./axiom-memory-rm.md) | Delete a session or all memory for a flow | | [axiom memory search](./axiom-memory-search.md) | Semantic search over memories for a flow | | [axiom memory show](./axiom-memory-show.md) | Show conversation history and semantic memories for a session | ## See also - [axiom](./axiom.md) — Axiom CLI — build and push node packages - [axiom memory end](./axiom-memory-end.md) — Close a session and trigger memory consolidation - [axiom memory ls](./axiom-memory-ls.md) — List flows with memory, or sessions for a specific flow - [axiom memory rm](./axiom-memory-rm.md) — Delete a session or all memory for a flow - [axiom memory search](./axiom-memory-search.md) — Semantic search over memories for a flow - [axiom memory show](./axiom-memory-show.md) — Show conversation history and semantic memories for a session - Guide: [Inspect agent memory](../../guides/inspect-agent-memory.md) --- ## Reference > axiom memory end {#reference/cli/axiom-memory-end} > Close a session and trigger memory consolidation # axiom memory end Close a session and trigger memory consolidation Formally closes a session and enqueues it for memory consolidation. Consolidation extracts semantic facts from the conversation history. This is a non-destructive operation — the history remains accessible. Use --yes to skip the confirmation prompt. Example: ```text axiom memory end 01HXYZ1234ABCDEF ``` ## Usage ```sh axiom memory end [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for end | | `--yes` | | bool | | skip confirmation prompt | ## See also - [axiom memory](./axiom-memory.md) — Inspect and manage agent memory for your flows - Guide: [Inspect agent memory](../../guides/inspect-agent-memory.md) --- ## Reference > axiom memory ls {#reference/cli/axiom-memory-ls} > List flows with memory, or sessions for a specific flow # axiom memory ls List flows with memory, or sessions for a specific flow Without --flow: lists all flows that have at least one memory session. With --flow : lists sessions for that flow. Examples: ```text axiom memory ls axiom memory ls --flow my-flow-id ``` ## Usage ```sh axiom memory ls [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--flow` | | string | | flow ID to list sessions for | | `--help` | `-h` | bool | | help for ls | ## See also - [axiom memory](./axiom-memory.md) — Inspect and manage agent memory for your flows - Guide: [Inspect agent memory](../../guides/inspect-agent-memory.md) --- ## Reference > axiom memory rm {#reference/cli/axiom-memory-rm} > Delete a session or all memory for a flow # axiom memory rm Delete a session or all memory for a flow Delete a session or, with --flow and --all, delete all memory for a flow. ```text axiom memory rm deletes a single session axiom memory rm --flow --all deletes all memory for the flow ``` All data is permanently deleted. Use --yes to skip the confirmation prompt. Examples: ```text axiom memory rm 01HXYZ1234ABCDEF axiom memory rm --flow my-flow-id --all ``` ## Usage ```sh axiom memory rm [] [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--all` | | bool | | delete all memory for the specified flow | | `--flow` | | string | | flow ID (use with --all to delete all flow memory) | | `--help` | `-h` | bool | | help for rm | | `--yes` | | bool | | skip confirmation prompt | ## See also - [axiom memory](./axiom-memory.md) — Inspect and manage agent memory for your flows - Guide: [Inspect agent memory](../../guides/inspect-agent-memory.md) --- ## Reference > axiom memory search {#reference/cli/axiom-memory-search} > Semantic search over memories for a flow # axiom memory search Semantic search over memories for a flow Performs a semantic/hybrid search over all memories associated with a flow. The --flow flag is required. Results are printed as a table with content, importance score, and retrieval score. Example: ```text axiom memory search --flow my-flow-id "preferred output format" ``` ## Usage ```sh axiom memory search --flow [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--flow` | | string | | flow ID to search within (required) | | `--help` | `-h` | bool | | help for search | ## See also - [axiom memory](./axiom-memory.md) — Inspect and manage agent memory for your flows - Guide: [Inspect agent memory](../../guides/inspect-agent-memory.md) --- ## Reference > axiom memory show {#reference/cli/axiom-memory-show} > Show conversation history and semantic memories for a session # axiom memory show Show conversation history and semantic memories for a session Displays the full conversation turn history followed by a "Semantic memories" section listing extracted facts. Example: ```text axiom memory show 01HXYZ1234ABCDEF ``` ## Usage ```sh axiom memory show [flags] ``` ## Flags | Flag | Shorthand | Type | Default | Description | |---|---|---|---|---| | `--help` | `-h` | bool | | help for show | ## See also - [axiom memory](./axiom-memory.md) — Inspect and manage agent memory for your flows - Guide: [Inspect agent memory](../../guides/inspect-agent-memory.md) --- ## 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; each push overwrites the previous one. This lets you iterate on code and test it deployed before making it public. 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 | |---|---|---|---|---| | `--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, or messages in the Axiom marketplace. When called without a query, lists recently published packages. Use --type to search nodes 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 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, 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 authoring Skills for Claude Code # axiom skills Manage the Axiom authoring Skills for Claude Code Install the Axiom authoring 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 two: ```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 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 authoring 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 authoring Skills for Claude Code --- ## 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 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 ``` 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 | **`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. ## 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 | **`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 | 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, agent memory with ax.agent.memory, 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, agent memory, 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` the `ax.agent.memory` 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. ## 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.agent.memory` | `AxiomAgentMemory` | Durable agent memory, scoped to flow and tenant | | `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). ## Agent memory with ax.agent.memory All memory read and write methods are coroutines — `await` them, and declare the handler `async def`. (`session(...)` itself is a plain call — no `await`.) Memory is scoped to the flow and tenant; you never pass tenant or flow identifiers, and any a node supplies are ignored by the sidecar. For what the tiers mean and how consolidation works, see [agent memory](/docs/concepts/memory). ### Flow-scoped operations - `await ax.agent.memory.search(query, limit=5)` — semantic search over the flow's memories; returns a list of `MemoryEntry`. - `await ax.agent.memory.write(content, importance=0.5)` — store a flow-scoped fact; returns the new memory's ID. ### Session operations `ax.agent.memory.session(session_id)` addresses one session (the ID comes from your input message — the platform never infers it). The session object provides: - `await session.history.last(n)` — the most recent `n` conversation turns, as a list of `ConversationTurn`. - `await session.history.append(role=..., content=...)` — record a turn; both arguments are keyword-only, and `role` is one of `user`, `assistant`, `tool`, or `system`. - `await session.search(query, limit=5)` / `await session.write(content, importance=0.5)` — session-scoped memory entries. - `await session.end()` — formally close the session and trigger consolidation. ### Complete example ```python # nodes/answer_question.py from gen.messages_pb2 import ChatRequest, ChatReply from gen.axiom_context import AxiomContext async def answer_question(ax: AxiomContext, input: ChatRequest) -> ChatReply: """Answers a chat message using session history and flow memory.""" session = ax.agent.memory.session(input.session_id) recent = await session.history.last(20) facts = await ax.agent.memory.search(input.text, limit=5) ax.log.info("context loaded", turns=len(recent), facts=len(facts)) reply = f"You said: {input.text}" # replace with your model call await session.history.append(role="user", content=input.text) await session.history.append(role="assistant", content=reply) return ChatReply(text=reply) ``` ### Failure behavior Memory operations never raise. When the memory service is unavailable, reads return empty lists, writes return an empty string, and `append` / `end` are no-ops — your node keeps running without memory rather than failing the execution. ## Memory data types `search` returns `MemoryEntry` objects; `history.last` returns `ConversationTurn` objects. **MemoryEntry** | Field | Type | Meaning | |---|---|---| | `id` | `str` | Memory entry ID | | `content` | `str` | The stored statement | | `scope_level` | `str` | `tenant`, `flow`, or `session` | | `memory_type` | `str` | `episodic`, `semantic`, or `procedural` | | `importance` | `float` | Importance score | | `confidence` | `float` | Confidence score | | `score` | `float` | Relevance score — populated on retrieval only | | `created_at` | `int` | Unix milliseconds | **ConversationTurn** | Field | Type | Meaning | |---|---|---| | `id` | `str` | Turn ID | | `session_id` | `str` | Session the turn belongs to | | `role` | `str` | `user`, `assistant`, `tool`, or `system` | | `content` | `str` | Turn text | | `created_at` | `int` | Unix milliseconds | | `tool_name` | `str` | Tool name, for tool turns | | `tool_call_id` | `str` | Tool call correlation ID | ## 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, no-op memory, 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: import asyncio def test_answer_question(): ax = _TestContext(secrets_map={"OPENAI_KEY": "sk-test"}) input_msg = ChatRequest(session_id="s1", text="hi") result = asyncio.run(answer_question(ax, input_msg)) assert isinstance(result, ChatReply) assert result.text == "You said: hi" ``` A plain `def` handler is called directly instead — the generated test stub already does that; `asyncio.run` is needed only for `async def` handlers. The `_TestContext` memory operations are awaitable no-ops, so handlers that use `ax.agent.memory` run unchanged in unit tests. 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, agent memory, 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, agent memory, 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). ## Context methods at a glance `axiom.Context` has eight 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.Agent()` | `Agent` | Agent capabilities — today, memory | | `ax.ExecutionID()` | `string` | ID of the current execution | | `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) } ``` ```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") } ``` 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 `ax.Agent().Memory()` is the entry point to the memory layer (see [memory](/docs/concepts/memory) for the model). Memory is namespaced under `Agent()` because it is an agent capability, separate from general-purpose utilities like logging. This example needs messages with a session ID field — run these in the package directory first: ```bash axiom create message ChatRequest --fields "session_id:string;text:string" axiom create message ChatReply --fields "text:string" axiom create node Remember --input ChatRequest --output ChatReply --type unary ``` ```go // nodes/remember.go package nodes import ( "context" "fmt" "demo/text-tools/axiom" gen "demo/text-tools/gen" ) // Remember appends the user turn to session history and recalls related memories. func Remember(ctx context.Context, ax axiom.Context, input *gen.ChatRequest) (*gen.ChatReply, error) { session := ax.Agent().Memory().Session(input.GetSessionId()) if err := session.History().Append(ctx, "user", input.GetText()); err != nil { return nil, err } turns, err := session.History().Last(ctx, 20) if err != nil { return nil, err } memories, err := session.Search(ctx, input.GetText(), 5) if err != nil { return nil, err } return &gen.ChatReply{ Text: fmt.Sprintf("%d turns in session, %d related memories", len(turns), len(memories)), }, nil } ``` The session ID always comes from the typed input message — the platform never infers it. ### Memory interfaces ```go // axiom/context.go (generated) type Agent interface { Memory() AgentMemory } type AgentMemory interface { // Session returns memory scoped to a specific session ID. Session(sessionID string) SessionMemory // Search queries semantic memory across all sessions for this flow. Search(ctx context.Context, query string, limit int) ([]MemoryEntry, error) // Write stores a durable memory at the flow scope (persists across sessions). Write(ctx context.Context, content string, importance float32) (string, error) } type SessionMemory interface { // Search finds the most relevant memories for this session and its flow. Search(ctx context.Context, query string, limit int) ([]MemoryEntry, error) // Write stores a semantic memory scoped to this session. Write(ctx context.Context, content string, importance float32) (string, error) // History returns the episodic history accessor for this session. History() SessionHistory // End formally closes the session and triggers consolidation. End(ctx context.Context) error } type SessionHistory interface { // Last returns the most recent n turns, oldest-first (for LLM context injection). Last(ctx context.Context, n int) ([]ConversationTurn, error) // Append adds a new turn to this session's history. // role is "user" | "assistant" | "tool" | "system" Append(ctx context.Context, role, content string) error } ``` `Write` returns the ID of the stored memory. `End` triggers consolidation — the promotion of episodic records into durable semantic facts. ### Memory data types ```go // axiom/context.go (generated) type ConversationTurn struct { ID string SessionID string Role string // "user" | "assistant" | "tool" | "system" Content string ToolName string ToolCallID string CreatedAt int64 // unix millis } type MemoryEntry struct { ID string ScopeLevel string MemoryType string Content string Importance float32 Confidence float32 Score float32 // relevance score, populated on retrieval CreatedAt int64 } ``` ## 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.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, agent memory, 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`, `AxiomAgent`, `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 use the Promise-based memory API: ```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.agent` | `AxiomAgent` | Agent capabilities; today exposes `ax.agent.memory` | | `ax.executionId` | `string` | UUID of the current execution | | `ax.flowId` | `string` | Stable ID of the compiled artifact (constant across executions) | | `ax.tenantId` | `string` | UUID 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). ## ax.agent.memory — agent memory `ax.agent.memory` is the agent memory layer: session-scoped conversation history and memory, plus cross-session (flow-scope) search and writes. All memory methods return Promises, so handlers that use them must be `async`. See [Agent memory](../../concepts/memory.md) for the model (sessions, scopes, consolidation). ### Flow-scope methods ```typescript // nodes/recall.ts — inside an async handler const entries = await ax.agent.memory.search('prior decisions', 5); const memoryId = await ax.agent.memory.write('User prefers TypeScript', 0.8); const session = ax.agent.memory.session(input.getSessionId()); ``` - `search(query, limit?)` — semantic search across all sessions at flow scope; returns `MemoryEntry[]`. `limit` defaults to 10. - `write(content, importance?)` — write a flow-scope memory entry; returns the memory ID. `importance` defaults to 0.5. - `session(sessionId)` — returns an `AxiomSessionMemory` handle scoped to one session. The session ID is a string your node code chooses — typically a field on your typed input message; the platform never infers it. ### Session-scope methods ```typescript // nodes/chat.ts — inside an async handler const session = ax.agent.memory.session(input.getSessionId()); const turns = await session.history().last(20); // ConversationTurn[] await session.history().append('user', input.getText()); // record a turn const prefs = await session.search('user preferences'); // MemoryEntry[] const id = await session.write('User prefers dark mode', 0.8); await session.end(); // close the session and trigger consolidation ``` - `history().last(n)` — the last `n` conversation turns for this session. - `history().append(role, content)` — append a turn; `role` is one of `'user' | 'assistant' | 'tool' | 'system'`. - `search(query, limit?)` / `write(content, importance?)` — like the flow-scope versions, but scoped to this session. - `end()` — formally close the session and trigger consolidation. ### Memory record types `ConversationTurn`: `id`, `sessionId`, `role`, `content`, `createdAt` (Unix milliseconds), plus optional `toolName`, `toolCallId`, and `metadata`. `MemoryEntry`: `id`, `tenantId`, `flowId`, `sessionId`, `scopeLevel` (`'tenant' | 'flow' | 'session'`), `memoryType` (`'episodic' | 'semantic' | 'procedural'`), `content`, `importance`, `confidence`, `createdAt`, `expiresAt`, plus optional `sourceId`, `score` (set on search results), and `metadata`. In environments without the memory service (unit tests, local runs), the generated runtime substitutes no-op implementations: reads resolve to empty arrays, writes resolve to an empty string — calls never throw for that reason. ## 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. ## 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, no-op memory, 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 — 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, the error envelope, and rate limits. 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 on your deployment's origin under the `/invocations` path prefix. For example: ```text POST https:///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. 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 — parallel fan-out branches that run as child executions, runtime graph mutations, debug forks — each draw from the same budget. A fan-out into 10 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 at a large fan-out), 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://flows.example-axiom-host.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). | ### 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. - 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://flows.example-axiom-host.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://flows.example-axiom-host.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. - **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. ## 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 > 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: transform TRANSFORM_ failed: ` | A transform in the edge's adapter failed at runtime — wrong argument count, a non-numeric argument, or an input value it cannot convert. | Open the edge and fix the transform. See [Edge transform errors](#edge-transform-errors) for every transform's error. | | `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 (fan-out, 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 transform errors Transforms run inside edge adapters; at runtime a failing transform produces `transform TRANSFORM_ failed: ` inside an edge error (see above). The messages per transform function, as written in adapter expressions: | Function | Error | Meaning | |---|---|---| | `mult` | `multiply needs a multiplier value, e.g. multiply(2)` | No argument given. | | `mult` | `multiply: "" is not a number` | Argument is not numeric. | | `add` | `add needs a value to add, e.g. add(5)` | No argument given. | | `add` | `add: "" is not a number` | Argument is not numeric. | | `mult`, `add` | `cannot convert to float` | The input field's value is not numeric. | | `suffix` | `suffix needs a string to append, e.g. suffix(" world")` | No argument given. | | `prefix` | `prefix needs a string to prepend, e.g. prefix("hi ")` | No argument given. | | `cel` | `cel requires 1 or 2 args (expression[, srcField])` | Wrong argument count. | | `cel` | `CEL parse error: ` / `CEL evaluation failed: ` | The CEL expression is invalid, or failed against this input. | | `replace` | `replace requires 2 args (from, to)` | Wrong argument count. | | `replace` | ``replace: `from` is required`` | Empty `from` argument. | | `regex_replace` | `regex_replace requires 2 args (pattern, replacement)` | Wrong argument count. | | `regex_replace` | ``regex_replace: `pattern` is required`` | Empty pattern. | | `regex_replace` | `regex_replace: invalid pattern: ` | The pattern is not a valid regular expression. | | `slice` | `slice requires 2 args (start, end)` | Wrong argument count. | | `split` | `split requires 2 args (sep, index)` | Wrong argument count. | | `split` | ``split: `separator` is required`` | Empty separator. | | `split` | `split: index out of range` | Fewer parts than the index asks for. | | `default` | `default requires 1 arg` | Wrong argument count. | | `cast` | `cast requires 1 arg (target kind)` | Wrong argument count. | | `cast` | `cast: "" is not a number` / `cast: cannot convert to ` / `cast: unsupported target kind ""` | The value cannot be converted to the requested kind, or the kind is not one cast supports. | `upper`, `lower`, `trim`, and `length` take no required arguments and do not fail at runtime. ## 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. ## 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. | | 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. | ## 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). ## 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). ---