---
title: "Build a client SDK"
description: "Bundle marketplace nodes and flows into a client — in Console → Client Builder or headlessly with `axiom client` — build it into a typed SDK in up to six languages, and call it with an API key from the environment."
category: guide
surfaces: [console, cli, sdk]
languages: [python, go, typescript, java, csharp, rust]
related: [guides/api-keys, getting-started/invoke-via-api, concepts/nodes-packages-flows, concepts/type-system, reference/glossary, reference/http-api]
last_reviewed: 2026-07-04
---

# Build a client SDK

The Client Builder turns a [client](../reference/glossary.md#client) — a
named bundle of marketplace nodes and flows — into a typed SDK in any of
six languages: Python, Go, TypeScript, Java, C#, and Rust. The SDK uses
real Protocol Buffers types and exposes one method per bundled node or
flow. You can build a client two ways:

- **Console** — assemble a client by hand in **Console → Client Builder**.
  Covered first below.
- **CLI** — headlessly, with the `axiom client` command group (no browser).
  Covered in [Build a client with the CLI](#build-a-client-with-the-cli),
  including the declarative `axiom-client.yaml` + `axiom client sync`
  workflow, which is the primary way to keep a consumer app's SDK in sync.

Both paths drive the same tenant-scoped clients, members, and versions —
a client created in the console can be synced from a manifest, and one
created by the CLI shows up in the console. This guide creates a client,
adds members from the marketplace, builds and downloads an SDK, and calls
it.

## Prerequisites

- An Axiom account you can sign in to the editor with.
- Something to bundle: a published package (yours via `axiom push`, or any
  marketplace package) and/or a saved flow owned by your account.
- An API key for runtime calls — the generated SDK authenticates every
  request with one (see [API keys](../guides/api-keys.md)).

## Create a client

1. In the app, go to **Console → Client Builder**
   (`/console/client-builder`): click **Console** in the top header —
   Client Builder is the first entry in the console sub-nav. It is also
   reachable from the command palette (⌘K, then "Client Builder").
2. Click **New client**.
3. Enter a **Name** — the name your code will know the SDK by, such as
   `billing` — and click **Create client**.

A client starts empty, and no language is chosen at creation: you pick the
languages per build, and you can build the same client in more languages
later.

## Add nodes and flows

A client's **members** are the nodes and flows its SDK exposes — each
member becomes one method. In the client's detail view:

1. Click **+ Add nodes & flows**. The page splits in two: the client's
   configuration on the left, the marketplace on the right.
2. Pick members in the marketplace panel:
   - **Packages tab** — click **+ Add all** on a package card to take every
     node in the package, or open the package and add nodes one at a time.
     A version dropdown on the card (and in the package detail) selects
     which package version to add.
   - **Flows tab** — click **+ Add** on a flow. Flows already in the client
     show **In client**.
3. Picked members collect in the tray at the bottom of the panel. Each has
   an **alias** — the method name the SDK will generate — prefilled from
   the node or flow name and editable inline. Aliases must be non-empty and
   unique within the client.
4. Click **Add N members**.

The **Members** list groups node members by their package, with a
**Remove all** per package and a **Remove** per member; flow members are
listed individually. Removing members changes only future builds — an
already-built version is never altered (see the next section).

## Build a version

1. In the **Build a version** section, select one or more languages —
   Python, Go, TypeScript, Java, C#, Rust. Every language you select shares
   **one version** of the bundle.
2. Click **Build**.

The build allocates the next version number, snapshots the client's current
members (with their pinned package versions), and compiles each selected
language from that snapshot. The version appears in the **Versions** list
with a per-language status — `pending`, `building`, then `succeeded` (or
`failed`, with the error shown inline); the page refreshes statuses
automatically. A version is an immutable snapshot: editing the client's
members later never changes an existing version, only the next build.

To add a language to an existing version, use the **+ language…** dropdown
under that version. It compiles the new language from the version's frozen
snapshot, so SDKs of the same version match exactly even if the client's
members have changed since.

## Download and install the SDK

Each succeeded language row has a **Download** button that saves a ZIP
(for a client named `billing`, `billing-sdk-py.zip`). The ZIP is a
ready-to-build package for the language's standard toolchain. Every SDK
ships with a `README.md` covering install, authentication, and the full
method list, and a `manifest.json` recording exactly which nodes, flows,
and package versions the version contains.

| Language | Typed messages come from | Install/build |
|---|---|---|
| Python | pregenerated protobuf modules (needs `protobuf>=4.0`) | `pip install -e .` |
| Go | pregenerated protoc-gen-go types | `go mod tidy` |
| TypeScript | protobufjs at runtime | `npm install && npm run build` |
| Java | protobuf-java, compiled by Maven | `mvn package` |
| C# | Google.Protobuf, compiled by Grpc.Tools | `dotnet build` |
| Rust | prost + pbjson, compiled by Cargo | `cargo build` |

## Build a client with the CLI

Everything above — create, add members, build, download — is also available
headlessly with `axiom client`, a normal CLI wrapper around the same
Client Builder the console drives. Requires a prior `axiom login`. There
are two ways to use it:

- **Declarative (primary path)** — commit an `axiom-client.yaml` manifest
  to the *consumer* app's repo and run `axiom client sync` to reconcile.
  This is the one to reach for by default, the same way you'd reach for
  `package.json` or a Terraform plan rather than hand-installing
  dependencies.
- **Imperative verbs (escape hatches)** — `create`, `add`, `members`,
  `remove-member`, `build`, `status`, `list`, `get`, `download` — for
  one-off exploration or scripting a step `sync` doesn't cover.

If you're working with Claude Code, install the `axiom-client-authoring`
skill (`axiom skills install`) — it encodes this same workflow and its
sharp edges so the agent can author the manifest and run `sync` for you.

### The declarative workflow: axiom-client.yaml + axiom client sync

Add `axiom-client.yaml` at the root of the app that will import the
generated SDK — not this platform's own repo:

```yaml
# axiom-client.yaml
client: billing                    # required; created if no client with this name/slug/id exists yet
description: Billing SDK           # only used when creating; not reconciled onto an existing client
languages: [python, go]            # SDK languages sync builds; six supported: go, python, rust, java, typescript, csharp

members:
  - package: acme/billing-utils@1.2.0   # handle/name@version; omit the version to pin @latest
    node: Summarize                     # node name within the package
    alias: summarize                    # required, unique per manifest — becomes the SDK method name
  - flow: 01J8Z3K9Q2R4S6T8V0W2X4Y6Z8     # artifact id of a saved/compiled flow
    alias: run_pipeline
```

Each member is either a `package` + `node` (a marketplace node) or a
`flow` (a saved flow's artifact ID) — exactly one of the two, plus a
required, manifest-unique `alias`. Then reconcile:

```bash
axiom client sync                      # reads ./axiom-client.yaml
axiom client sync --file path/to/f.yaml
axiom client sync --json               # machine-readable result
```

Every run reconciles the server to match the manifest exactly — `kubectl
apply` / Terraform-plan semantics, not a one-time bootstrap script:

- creates the client (by its `client:` name) if it doesn't exist yet
- adds every manifest member missing from the server
- **removes every server member no longer listed in the manifest**
- builds every manifest language not yet covered by the client's current
  build state — if membership changed since the last build, this cuts a
  brand-new version (an existing version is an immutable snapshot, so it
  can never be reused once membership drifts); otherwise it only tops up
  the missing languages onto the current latest version

A converged run — the manifest unchanged since the last sync — makes
**no mutating calls at all**: `sync` is idempotent and safe to re-run
from a script, a pre-commit hook, or CI. An omitted package version pins
`@latest` server-side; the resolved pin is printed to stderr so an
unattended run stays reproducible.

Once `sync` reports a build, download and install it exactly as in
[Download and install the SDK](#download-and-install-the-sdk) above:

```bash
axiom client download <client-id> --version 2 --lang python -o ./sdks
```

`download` prints the install step for the language it just fetched
(`pip install -e .`, `go mod tidy`, and so on — see the table above) and
refuses to overwrite an existing file at its destination path.

### Imperative verbs (escape hatches)

Use these for one-off exploration, or when scripting a step outside the
declarative loop. Every command that takes a `<client-id>` (as opposed to
`get`, which also accepts a name or slug) wants the literal ID a client
was created or listed with — get it from `create --json`, `list`, or
`get`.

```bash
# create a client
axiom client create billing --description "Billing SDK" --language python

# add a member: a marketplace node, or a saved flow
axiom client add <client-id> --package acme/billing-utils@1.2.0 --node Summarize --alias summarize
axiom client add <client-id> --flow <artifact-id> --alias run_pipeline

# list/inspect members
axiom client members <client-id>
axiom client remove-member <client-id> <member-id>

# build and watch a version
axiom client build <client-id> --lang python,go        # cut a new version, build both
axiom client build <client-id> --lang rust --version 3  # add a language to version 3
axiom client status <client-id> --version 2 --watch     # poll until every language finishes

# list clients, or inspect one by name, slug, or id
axiom client list
axiom client get billing

# download a succeeded build
axiom client download <client-id> --version 2 --lang python -o ./sdks
```

#### Flag reference

Each row is the exact flag set a subcommand registers, besides the
built-in `-h`/`--help`. It mirrors the CLI's own `--help` output.

| Command | Flags |
|---|---|
| `axiom client create <name>` | `--description`, `--language`, `--json` |
| `axiom client list` | `--json` |
| `axiom client get <client>` | `--json` |
| `axiom client add <client-id>` | `--package`, `--node`, `--flow`, `--alias`, `--json` |
| `axiom client members <client-id>` | `--json` |
| `axiom client remove-member <client-id> <member-id>` | _none_ |
| `axiom client build <client-id>` | `--lang`, `--version`, `--json` |
| `axiom client status <client-id>` | `--version`, `--watch`, `--json` |
| `axiom client download <client-id>` | `--version`, `--lang`, `--output` (`-o`) |
| `axiom client sync` | `--file`, `--json` |

So `--json` is available on every subcommand except `download` (it always
writes a file, never JSON) and `remove-member` (registers no flags at
all). `status` (with or without `--watch`) exits non-zero if any language
in the requested version failed.

## Call the SDK

No API key is embedded in a generated SDK. Every language's client reads
the `AXIOM_API_KEY` environment variable at construction (or takes the key
as an explicit constructor argument) and sends it as a bearer token on
every request:

```bash
export AXIOM_API_KEY="<your 64-character 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).
