---
title: "Configure an Instance from a Generic node"
description: "Bind a Generic node's port to a real type in the console wizard or with axiom instance create — author nested message-typed fields, toggle repeated/optional, and preview a mapping's exact output before you save."
category: guide
surfaces: [console, cli, http-api]
related: [concepts/nodes-packages-flows, reference/axiom-yaml, reference/cli/axiom-instance-create, reference/cli/axiom-instance-preview, reference/http-api, reference/glossary]
last_reviewed: 2026-08-03
---

# Configure an Instance from a Generic node

An [Instance](../reference/glossary.md#instance) binds a
[Generic node](../reference/glossary.md#generic-node)'s port(s) to a real,
specific message type. The shortest path — one flat, scalar-only message per
port — is covered in [`axiom instance create`](../reference/cli/axiom-instance-create.md)
and the marketplace's Generic Nodes tab. This guide covers the richer
authoring model both surfaces also support: a field whose type is itself
another message, defined inline and recursively; `repeated`/`optional`
field modifiers; and previewing a mapping's exact output before you commit
to a shape.

**Nothing you author while configuring an Instance becomes a real, queryable
registry message until you save (console) or run `axiom instance create`
(CLI) for the whole Instance.** Closing the wizard, hitting Cancel at any
nesting level, or a failed create leaves nothing behind — every nested
message you draft is synthesized transactionally, in the same request that
creates the Instance, never as a separate call per message.

## Which ports you have to bind

**You bind the ports you want to re-type. Any port you leave unbound keeps
the Generic node's own concrete message, passed through unchanged.**

The Generic node keeps executing underneath — an Instance is a facade over
it, not a replacement for it. So an unbound port is not a hole: it is the
Generic's published contract, with no reshape applied in either direction.
Three combinations, all valid:

| What you bind | The Instance's input | The Instance's output |
| --- | --- | --- |
| Both ports | your input facade | your output facade |
| Input only | your input facade | the Generic's own output message, verbatim |
| Output only | the Generic's own input message, verbatim | your output facade |

At least one port must be bound — an Instance that re-types nothing is just
the Generic node.

This holds whether or not the Generic *declared* the port in
`generic_ports`. A node that declares `generic_ports: [input]` and returns a
fully concrete output is a perfectly good thing to instantiate: you re-type
its input, and its output arrives exactly as the node published it. You do
not need a facade you have no reason to write.

### What an inherited port means when you use the node

- **Invoking.** `axiom invoke your-handle/pkg/Node` returns the Generic's
  message, in the Generic's field names, as JSON — the same bytes the
  Generic node itself would return.
- **Composing.** `axiom info your-handle/pkg` reports the inherited message
  name, so a flow edge wires from it like any other typed port.
- **Mapping.** `--output-map`/`--input-map` apply to a port you bind. There
  is nothing to map on an inherited port; passing a map for an unbound port
  is an error.

### Combinations that are refused at create time

Two combinations cannot be served at invoke time, so `axiom instance create`
rejects them with a `400` naming the reason rather than accepting them:

- Leaving a port unbound when the Generic node declares **no concrete
  message for that port either** — there is nothing to inherit.
- Naming a facade message the same as a message in the Generic node's own
  package **while some port is inherited**. Both sets of messages are then
  compiled together, and a shared name resolves ambiguously. Rename the
  facade message, or bind both ports so nothing is inherited.

## Prerequisites

- You are logged in to the Axiom app, and the CLI has run `axiom login`
  ([Installation](../getting-started/installation.md)).
- A published **Generic node** exists to instantiate — a package with
  `kind: generic` and at least one `generic_ports` entry (see
  [Generic and Instance nodes](../reference/axiom-yaml.md#generic-and-instance-nodes)).
  Browse one via the marketplace's **Generic Nodes** tab.

## Configure an Instance in the console

Open the wizard from the marketplace's Generic Nodes tab (**Create
Instance** on a node's detail page) or by navigating to `/instances/new`.
Each generic port (output, input, or both) gets its own column with two
steps: **Select message**, then **Map fields**.

### Author a message with nested fields

In Step 1, **Create new message** opens a form: a message name, then a list
of fields. Each field has a name and a type — either a scalar (`string`,
`int32`, `int64`, `uint32`, `uint64`, `double`, `float`, `bool`, `bytes`) or
`message`, a reference to another message.

Picking `message` opens a picker with its own **Create new message**
button — authoring a nested message reuses the exact same form, recursively.
There is no fixed depth limit in the UI; author as many levels as the shape
actually needs (the server enforces a generous cap — see
[Limits on a drafted message tree](#limits-on-a-drafted-message-tree)
below). Today, a nested field's `message` type can only be an inline,
freshly authored message — picking an already-published message as a nested
field's type is not yet supported end to end (it would carry no field
definitions of its own, and the request is rejected). Author the nested
shape inline instead.

Each field also has independent **repeated** and **optional** toggles.
They're mutually exclusive — proto3 forbids `repeated optional` — so
turning one on clears the other.

A **Proto preview** toggle on the form shows the exact `.proto` text your
current name/fields/modifiers produce, live, recomputed on every keystroke —
useful for confirming a nested shape's rendered syntax before committing.

### Map fields and preview the mapping

Step 2 renders the same field-mapping panel used for ordinary flow edges, so
target fields bind to source values by name, CEL expression, or literal.
Its collapsible **Preview** section runs your mapping against sample JSON
you type per source, showing the *exact* value it currently produces or the
mapping error blocking it — the same engine the registry and worker use at
real compile/dispatch time, not an approximation. This works identically
whether every message involved is already real or one is a message you just
authored in Step 1: an as-yet-undrafted message is sent to the preview
endpoint as an inline definition instead of a registry name, so you can
iterate on a brand-new nested shape and see real output before anything is
saved.

### Name and save

Once every declared generic port shows **resolved**, name the Instance
(`your-handle/instance-name`) and click **Save instance**. The whole
draft tree — the top-level facade for each port plus every nested message,
however deep — is synthesized in one transaction; a validation failure at
any level leaves nothing behind.

## Configure an Instance with the CLI

`axiom instance create` supports the same two authoring shapes as the
console, chosen per port.

### Flat fields (the common case)

`--output-field`/`--input-field name=kind` (repeatable) covers a flat,
scalar-only message — see
[`axiom instance create`](../reference/cli/axiom-instance-create.md) for the
full flag reference. This shape is unchanged and always available.

Document each field with `--output-field-description`/
`--input-field-description name=text` (repeatable), and the message itself
with `--output-message-description`/`--input-message-description`:

```bash
axiom instance create acme/orders-query \
  --generic-package acme/postgres-query --generic-node "Postgres Query" \
  --output-message OrderSummary \
  --output-message-description "A one-line summary of one order." \
  --output-field id=int64 --output-field total=double \
  --output-field-description "id=The order's primary key." \
  --output-field-description "total=Order total in minor units, including tax."
```

A facade is the node's public interface: a caller sees only the field names,
their types, and these sentences. Say what the field means, plus its
format/units/allowed values where they aren't obvious. A description whose
name doesn't match a declared `--output-field` is rejected locally rather
than silently dropped, so a typo can't quietly lose documentation.

### Nested fields with a fields-file

For repeated/optional modifiers, or a field whose type is another message
(recursively), pass `--output-fields-file`/`--input-fields-file` instead: a
path to a JSON file shaped `{"message_name": "...", "fields": {...}}`. Each
value in `fields` is either:

- a bare scalar-kind string (`"total": "double"`) — identical to the flat
  shape, and
- a JSON object for anything richer: `{"kind": "<scalar>", "repeated":
  true}`, `{"kind": "<scalar>", "optional": true}`, or `{"kind": "message",
  "message": {"message_name": "...", "fields": {...}}}` for a nested,
  not-yet-real message.

Any object-form field, and the file itself, may carry a `"description"` —
that field's or message's documentation. Descriptions never change the wire
shape; they surface in the node's schema, its generated docs, and the
console's field tooltips.

`--output-fields-file` is mutually exclusive with `--output-message`/
`--output-field` on the same port — pick one shape of input per port.
`--input-fields-file` is likewise exclusive with `--input-message`/
`--input-field`. The two ports are independent: the output port can use a
fields-file while the input port uses flat flags, or vice versa.

The batch [`--from` manifest](../reference/cli/axiom-instance-create.md)
accepts the identical rich field form **inline**: a node's per-port `fields:`
value is either a bare scalar-kind string (`count: int64`) or the same object
(`{kind: string, repeated: true}`, `{kind: <scalar>, optional: true}`, or a
`{kind: message, message: {...}}` nested tree). A `repeated`/nested facade
field goes straight in the manifest — no separate fields-file append needed.

A worked example — an `OrderSummary` output facade with a top-level
`repeated` field and a nested `Customer` message:

```json
{
  "message_name": "OrderSummary",
  "description": "A one-line summary of one order.",
  "fields": {
    "id": "int64",
    "tags": { "kind": "string", "repeated": true, "description": "Free-form labels on the order." },
    "customer": {
      "kind": "message",
      "description": "Who placed the order.",
      "message": {
        "message_name": "Customer",
        "description": "The ordering customer.",
        "fields": {
          "name": { "kind": "string", "description": "Full name as given at checkout." },
          "age": { "kind": "int32", "optional": true, "description": "Age in years; absent if not collected." }
        }
      }
    }
  }
}
```

Save that as `order-summary.fields.json` and pass it in:

```bash
axiom instance create acme/orders-query \
  --generic-package acme/postgres-query --generic-node "Postgres Query" \
  --version 0.1.0 \
  --package-description "Typed Postgres orders query" --description "Return an order summary by id" \
  --output-fields-file ./order-summary.fields.json
```

`--version` is the author-chosen shell package version (semver; defaults to
`0.1.0`, and an append inherits the package's version). `--package-description` /
`--description` set the marketplace text for the package and this node — an
Instance has no `axiom.yaml`, so set them or it lists blank.

`OrderSummary` and `Customer` are both synthesized in the same request that
creates `acme/orders-query` — neither exists as a registry row if the
request fails validation at any level.

### Preview a mapping before you create

`axiom instance preview` wraps `POST /adapters/preview` verbatim — the same
engine the registry compiler and worker use at real dispatch time, run
against sample JSON you supply, with no live invoke. It reads a request body
from `--request-file` (or stdin):

```json
{
  "dst_msg_name": "OrderSummary",
  "sources": [
    {"edge_id": "e1", "msg_name": "Order", "sample": {"id": 1, "amount": 42.5}}
  ],
  "adapter": {"total": "$.amount"}
}
```

Exactly one of `adapter` (a single-edge mapping) or `compose_mapping` (a
multi-source compose consumer) is required. Any `msg_name` slot —
`dst_msg_name`, or a `sources[]` entry — accepts a bare registry message name
(a JSON string, as above) **or** an inline draft in place of it: the
identical `{"message_name": ..., "fields": {...}}` shape the fields-file
above uses, letting you preview a mapping against a message you haven't
created yet:

```json
{
  "dst_msg_name": {"message_name": "OrderSummary", "fields": {"total": "double"}},
  "sources": [{"edge_id": "e1", "msg_name": "Order", "sample": {"amount": 42.5}}],
  "adapter": {"total": "$.amount"}
}
```

```bash
axiom instance preview --request-file ./preview-request.json
```

A mapping-level failure (bad expression, type mismatch, unresolvable field)
prints as an error and exits non-zero — it's a property of the mapping
you're iterating on, not a malformed request. Once the output looks right,
reuse the exact same message shape as the fields-file input to
`axiom instance create`. See
[`axiom instance preview`](../reference/cli/axiom-instance-preview.md) for
the full flag reference.

## Declare secrets an Instance needs

An Instance that calls a keyed API (an HTTP connector needing an API key, for
example) declares the secret **names** it reads at runtime, the same way an
ordinary node does with `required_secrets` in `axiom.yaml`. Since ADR-170,
this is delivered to any invoking tenant that has configured a value under
that name — regardless of who authored the package — and enforced at
compile/invoke time: an invoker who hasn't set a declared secret gets a
clear, fail-closed error instead of the flow silently running with a
not-found result.

In the CLI, pass `--required-secret NAME` (repeatable) to `axiom instance
create`:

```bash
axiom instance create nadia/openai/ChatCompletion \
  --generic-package nadia/http-tools --generic-node Request --generic-version 0.2.0 \
  --required-secret OPENAI_API_KEY \
  --description "Chat completion" --package-description "OpenAI connector"
```

In a `--from` manifest, add `required_secrets` under the node:

```yaml
nodes:
  - node: ChatCompletion
    description: Chat completion
    required_secrets: [OPENAI_API_KEY]
    input: { message: ChatRequest, fields: { prompt: string } }
    output: { message: ChatReply, fields: { text: string } }
```

Names only — never values. A secret's value is set by a human in the
**console's Secrets page**; there is no CLI or agent write path for a secret
value (deliberate: an agent driving the CLI must never be able to author a
user's credentials). See [Manage secrets in a flow](./manage-secrets.md).

## See what an Instance actually does before trusting it with a secret

`axiom instance inspect <handle>/<package>/<node>` is a transparency viewer:
it shows the exact pinned Generic node an Instance runs as (an Instance
carries no code of its own), that Generic's real source, the compiled
request/response adapter mappings in both directions, and the instance's
declared `required_secrets` — in one bundle, so you can see exactly where a
secret ends up (which header, which query parameter) before you configure
one for it.

```bash
axiom instance inspect nadia/openai/ChatCompletion
axiom instance inspect nadia/openai/ChatCompletion --json
```

This is a viewer, not an automated audit or a gate — it surfaces facts (the
same bundle is shown in the marketplace website and the editor's node panel)
and leaves the trust decision to you.

## Limits on a drafted message tree

Both surfaces synthesize (console Save) or accept (CLI preview/create) a
draft tree under the same bounds, so a request fails fast with a clear error
instead of an unbounded compile:

| Limit | Bound |
|---|---|
| Nesting depth (top-level message counts as depth 1) | 10 |
| Distinct messages in one tree (both ports combined) | 100 |
| Fields on any one message | 200 |
| Request body size | 1 MiB |

A name reused for two different shapes within one request is rejected as a
collision; drafting the identical name and shape twice (for example, the
same nested message referenced from both the output and input port) is a
harmless dedup, not an error.
