---
title: "Set per-flow config values"
description: "Create flow config profiles in the console — secret overrides — pick one in the Run dialog or via config_id, and see how resolved values reach nodes."
category: guide
surfaces: [console, canvas, http-api, sdk]
related: [guides/manage-secrets, getting-started/first-flow, getting-started/invoke-via-api, concepts/execution-model, reference/http-api]
last_reviewed: 2026-07-10
---

# Set per-flow config values

A **flow config** is a named set of runtime parameters for a flow — today,
secret overrides. Configs are not part of the flow itself: they are resolved
at invocation time and injected into the run, so changing one affects the
next run without editing or re-saving the flow. A config scoped to one flow
is called a **profile**; the **tenant default** applies to every flow you own.

Prerequisites: you are logged in to the Axiom editor and have saved at least
one flow ([Push and run your first flow](../getting-started/first-flow.md)).

## Create a per-flow profile

On the Flow Configs page (`/console/flow-configs`):

1. Pick your flow in the **Flow** picker. With no profiles yet, the page
   shows "No profiles for this flow yet."
2. Click **+ New profile**.
3. Enter a **Profile name** (left blank, it becomes `default` — a flow's
   `default` profile is applied automatically; see "How config values are
   resolved" for the exact layering and its one exception).
4. Optionally add **Secret overrides**: click **+ Add override**, pick a
   **Secret name** from your existing secrets (or type a name when you have
   none yet), enter a **Value**, and click **Save override**.
5. Click **Create**.

Each profile row lists its name and the names of its secret overrides, with
**Edit** and **Delete** buttons. Override values are write-only: the page
(and the API behind it) returns override names, never values.

Two more ways to reach the same controls:

- **From the editor**: with nothing selected on the canvas, the inspector
  shows a **Run configuration** card containing the same profile list and
  create form for the open flow, plus a link to the tenant default.
- **Deep link**: `/console/flow-configs?graph=<flow id>` preselects that
  flow in the picker.

To manage the secrets themselves (the tenant-wide values that overrides
replace), see [Manage secrets in a flow](manage-secrets.md).

## Pick a profile when you run

In the editor's Run dialog, a **Config profile** selector appears once any
config applies to the flow — a per-flow profile or the tenant default. It
defaults to **Tenant default**; pick a profile to run with its values
instead. The selection applies to that run only.

When invoking over HTTP, set `config_id` in the request body. It accepts
either a profile name (for the flow being invoked) or a profile ID; when
omitted, the default hierarchy applies (tenant default, then the flow's
`default` profile). See [Invoke a flow via the API](../getting-started/invoke-via-api.md)
for the endpoint, authentication, and `graph_id`.

```bash
# Run the flow with the "load-test" profile (assumes AXIOM_API_KEY is exported)
curl -X POST "https://api.axiomide.com/invocations/v1/flows/invoke" \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD",
    "input": {"name": "Ada"},
    "wait": true,
    "config_id": "load-test"
  }'
```

## How config values are resolved

At invocation time the platform merges up to three layers, later layers
overriding earlier ones parameter by parameter:

1. **Tenant default** — your config named `default` that is not scoped to
   any flow (the one the Flow Configs page edits under "Tenant default").
2. **Flow default** — the invoked flow's profile named `default`, when one
   exists.
3. **Selected profile** — the profile named in the Run dialog or in
   `config_id`, when it isn't `default`.

Secret overrides merge across the same layers, with the later layer winning
for a given secret name. One exception: selecting a profile by its ID (which
is what the Run dialog sends) applies that profile directly on top of the
tenant default — the flow's `default` profile is not layered in between.

## How nodes receive config values

Resolution happens before the flow starts: the merged parameters and secret
values are injected into the run's variables, which travel with the
execution to every node.

- **Secret overrides are invisible to node code.** A node reads secrets
  through `AxiomContext` exactly as described in
  [Manage secrets in a flow](manage-secrets.md); when the run's profile
  overrides a secret, the same read returns the override value instead of
  the tenant-wide one. No code change is needed to support per-profile
  values:

```python
# nodes/summarize.py — gets the tenant-wide OPENAI_KEY, or the selected
# profile's override of it, through the same call
from gen.messages_pb2 import SummarizeRequest, SummarizeReply
from gen.axiom_context import AxiomContext


def summarize(ax: AxiomContext, input: SummarizeRequest) -> SummarizeReply:
    api_key, found = ax.secrets.get("OPENAI_KEY")
    if not found:
        ax.log.error("OPENAI_KEY is not set for this tenant")
        return SummarizeReply()
    ax.log.info("key resolved for this run")  # never log the value itself
    return SummarizeReply()
```

This snippet assumes a Python package with `SummarizeRequest` and
`SummarizeReply` messages and a `Summarize` node — see
[Create a node in Python](create-a-node-python.md) for the scaffold.

## Configure per-node retry and backoff

Retry and backoff work differently from everything else on this page: they
aren't a config profile resolved at invocation time — they're a **per-node
policy compiled into the flow itself**, authored in the node's `retry:`
block in `flow.yaml` (or the canvas **Reliability** tab), the same way you
author edges and node placement. A node with no retry policy keeps today's
behavior: any failure ends the execution (subject to
[failure edges](../concepts/execution-model.md#failure-edges-and-typed-errors)
and [compensation](../concepts/execution-model.md#compensation-saga-rollback)).

### Author a policy

```yaml
nodes:
  - id: call_provider
    node: acme/http-connector.CallProvider
    retry:
      max_attempts: 5
      initial_interval_ms: 1000
      backoff_coefficient: 2.0
      max_interval_ms: 60000
      overall_timeout_ms: 300000
      non_retryable_codes: [USER]
```

The same fields live on the node's **Reliability** tab in the canvas, with a
live preview of the resulting attempt schedule.

### How backoff is computed

- `max_attempts` caps the total number of attempts, including the first.
  `0` (or omitting the field) means "use the platform default" — 3
  attempts — and any value is clamped to a hard platform ceiling of 10, to
  bound retry storms.
- Between attempts, the wait grows exponentially:
  `initial_interval_ms × backoff_coefficient ^ (attempt - 1)`, capped at
  `max_interval_ms` so a fast-growing backoff never waits longer than the
  ceiling.
- Retries never hold a worker slot. On a retryable failure the worker
  durably records the attempt and releases the slot immediately; a
  background sweeper re-dispatches the node when its backoff elapses. This
  is why a connector can wait minutes for a rate limit to clear without
  starving other work on the same worker.

### Per-attempt and overall timeouts

- `per_attempt_timeout_ms` bounds a single attempt — it can only narrow, never
  widen, the node's existing timeout.
- `overall_timeout_ms` bounds the total time spent across every attempt. Once
  it's spent, retries stop even
  if `max_attempts` hasn't been reached — a node exhausted this way falls
  through to `Failed` (or a failure edge) exactly like exhausting attempts.

### Retryable vs. non-retryable failures

Every failure is classified into a typed error code before the policy is
consulted: `TRANSPORT` and `TIMEOUT` are retryable by default (a dropped
connection or a slow upstream is worth retrying); `USER` (a deterministic
business failure — retrying it just fails the same way again), `PANIC`, and
`CANCELLED` are not. `non_retryable_codes` can only remove a code from
retrying — it can never grant retryability to `USER`, `PANIC`, or
`CANCELLED`.

### Scope: unary nodes only

Retry policy applies to **unary** nodes only. Pipeline nodes (streaming,
`type: pipeline` in `axiom.yaml`) and heartbeat-based liveness are not
retried in this release — a pipeline node's `retry:` block, if set, is
ignored.

### Side-effecting nodes must be idempotent

A retried node runs again from the top — the platform has no way to know
whether the previous attempt's side effect (a charge, a write, a
provisioned resource) actually landed before it failed. Give any node that
calls an external system, especially marketplace connectors, an idempotency
key or an equivalent check-before-write guard.

Marketplace connectors are a particular case worth calling out explicitly:
a connector is a generic engine (HTTP, database, queue) re-typed per use — a
`kind: generic` **Generic node** typed once, server-side, by an **Instance**
(see the [axiom.yaml reference](../reference/axiom-yaml.md#generic-and-instance-nodes)),
not a hand-authored node with its own idempotency logic baked in. The retry
policy has no visibility into what the underlying call actually does, so it cannot
guarantee exactly-once delivery to the external system on your behalf — the
idempotency key or check-before-write guard above is still yours to provide
at the connector's configured operation.
