Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

Set per-flow config values

Create flow config profiles in the console, pick one in the Run dialog or via config_id, and see how resolved parameters reach nodes.

View as Markdown

A flow config is a named set of runtime parameters for a flow — today, parameters and secret REFERENCES (names). 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).

Secret overrides were REMOVED in ADR-179 (2026-08-02). A config could previously swap a secret's VALUE for a whole flow run. That is incompatible with per-node secret grants: a grant pins consent to specific code, while an override could silently redirect WHICH key that consented code received — and it was name-scoped per config, not per node, so it bypassed the very binding the grant establishes. Profiles keep parameters, secret_refs (names), and profile selection; secret VALUES now reach a node only through the granted delivery path. See Manage secrets.

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. Click Create.

Each profile row lists its name, with Edit and Delete buttons. Override values are write-only: the page

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 secret_refs replace), see Manage secrets in a flow.

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 for the endpoint, authentication, and graph_id.

# 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.

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.

# nodes/summarize.py — gets the tenant-wide OPENAI_KEY, or the selected
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 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 and compensation).

Author a policy

nodes:
  - alias: call_provider
    package: acme/http-connector@1.0.0
    node: CallProvider
    config:
      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 policy lives under the node's config: block, alongside join, hitl, gate, and composeretry: is not a top-level node key. A node is identified by alias plus package + node; flow.yaml parsing is strict, so an unknown key is an error rather than a silently ignored field.

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. Any value is clamped to a hard platform ceiling of 10, to bound retry storms. The two "unset" cases differ, and the difference is easy to trip over: omitting the retry: block entirely (no layer opted this node in) falls through to the platform default of 10 attempts, while writing the block and leaving max_attempts unset (or 0) opts into the smaller default of 3.
  • 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), 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.