Skip to content

YAML Configuration

Ironflow can be configured with a declarative YAML file instead of CLI flags and environment variables. One file describes your entire server infrastructure — from a local dev setup to a multi-tenant platform.

Without YAML, configuring Ironflow means combining CLI flags, environment variables, and docker-compose files. The configuration is scattered and hard to reproduce. With ironflow.yaml, your entire infrastructure is a single, version-controlled file.

Without YAMLWith YAML
--port 9123 --nats-url nats://h1:4222 --node-id node-1 + IRONFLOW_DATABASE_URL=... IRONFLOW_MASTER_KEY=...ironflow serve -f ironflow.yaml
Configuration scattered across flags, env vars, compose filesSingle file, checked into git
Easy to forget a flag when deployingReproducible on every boot

Use ironflow config init to generate a starter YAML for your deployment tier:

Terminal window
# Local development (minimal, SQLite, dev mode)
ironflow config init > ironflow.yaml
# Production single-node (PostgreSQL, observability)
ironflow config init --prod > ironflow.yaml
# Multi-node cluster
ironflow config init --kind cluster > ironflow.yaml
# Multi-tenant platform
ironflow config init --kind platform > ironflow.yaml

Every ironflow.yaml has a kind that determines which features and validations apply:

KindUse CaseRequirements
ServerLocal dev, single-node productionNone (SQLite + embedded NATS by default)
ClusterMulti-node horizontal scalingPostgreSQL + external NATS + stable nodeId
PlatformMulti-tenant SaaSEverything in Cluster + organizations

The kind you choose determines what fields are required. Start with Server and graduate to Cluster or Platform as your deployment grows.


The simplest configuration — 5 lines to start:

apiVersion: ironflow/v1
kind: Server
spec:
port: 9123
auth:
devMode: true
Terminal window
ironflow serve -f ironflow.yaml

This boots Ironflow with embedded NATS (memory mode), SQLite, and authentication disabled. Similar to ironflow serve --dev, though the --dev flag also tightens crash-resume timings for faster local iteration.


Add PostgreSQL, secrets encryption, and observability:

apiVersion: ironflow/v1
kind: Server
spec:
port: 9123
storage:
driver: postgres
url: ${IRONFLOW_DATABASE_URL}
pool:
maxConns: 25
minConns: 5
maxIdleTime: 30s
nats:
storeDir: /data/nats
auth:
masterKey: ${IRONFLOW_MASTER_KEY}
jwtSecret: ${IRONFLOW_JWT_SECRET} # optional — auto-generated if empty
observability:
tracing:
endpoint: otel-collector:4317
sampleRate: 1.0
metrics:
enabled: true

Notice the ${IRONFLOW_DATABASE_URL} syntax — secrets are never stored in the file. See Environment Variable References below.


Scale horizontally with shared PostgreSQL and external NATS. Deploy the same file on every node — only nodeId differs (set via env var per node):

apiVersion: ironflow/v1
kind: Cluster
spec:
port: 9123
storage:
driver: postgres
url: ${IRONFLOW_DATABASE_URL}
pool:
maxConns: 25
minConns: 5
nats:
url: ${NATS_URL}
# credentials: /path/to/nats.creds
cluster:
nodeId: ${IRONFLOW_NODE_ID}
staleClaimThreshold: 2m
auth:
masterKey: ${IRONFLOW_MASTER_KEY}
jwtSecret: ${IRONFLOW_JWT_SECRET} # optional — auto-generated if empty
observability:
tracing:
endpoint: otel-collector:4317
sampleRate: 1.0
metrics:
enabled: true

Cluster kind enforces three rules at startup:

  1. PostgreSQL required — SQLite does not support SKIP LOCKED for distributed scheduling
  2. External NATS required — embedded NATS is single-process only
  3. Stable nodeId required — used for claim ownership and log correlation

See Docker Compose Deployment for the full clustering guide.


Declare organizations, projects, and environments. Ironflow creates them on first boot and is idempotent on subsequent boots:

apiVersion: ironflow/v1
kind: Platform
spec:
port: 9123
storage:
driver: postgres
url: ${IRONFLOW_DATABASE_URL}
nats:
url: ${NATS_URL}
cluster:
nodeId: ${IRONFLOW_NODE_ID}
auth:
masterKey: ${IRONFLOW_MASTER_KEY}
jwtSecret: ${IRONFLOW_JWT_SECRET} # optional — auto-generated if empty
platform:
organizations:
- name: acme-corp
projects:
- name: payments
environments:
- name: development
- name: staging
- name: production
- name: orders
environments:
- name: development
- name: production
- name: globex
projects:
- name: logistics
environments:
- name: production

Platform inherits all Cluster requirements and adds:

  • At least one organization — the platform must have tenants
  • Unique names — duplicate org, project, or environment names within a parent are rejected

The default organization (org_default) and its project/environment are always created first, regardless of what’s in the YAML. Your Platform organizations are created on top.


The YAML file is safe to commit to git because secrets use ${VAR} references that resolve at boot time:

SyntaxBehavior
${VAR}Required — startup fails with a clear error if not set
${VAR:-default}Optional — uses the default value if the env var is not set
postgres://literalLiteral string — used as-is (no ${} means no resolution)

Example:

storage:
url: ${IRONFLOW_DATABASE_URL} # required — error if unset
nats:
maxMemory: ${NATS_MAX_MEM:-256MiB} # optional — defaults to 256MiB
auth:
masterKey: ${IRONFLOW_MASTER_KEY} # required — error if unset

${VAR} is how the environment reaches a YAML-configured server for every field the config parser owns. With -f, the file is the source of truth: only CLI flags override it, and the env vars the flag-only path reads (NATS_URL, NATS_CREDS_FILE, NATS_STORE_DIR, NATS_FILE_STORAGE, NATS_STREAM_REPLICAS, IRONFLOW_MASTER_KEY, IRONFLOW_JWT_SECRET, IRONFLOW_NODE_ID, IRONFLOW_STALE_CLAIM_THRESHOLD, IRONFLOW_STALE_CLAIM_RECOVERY_INTERVAL) are not consulted. ironflow serve warns and ironflow validate lists any that are set but ignored — NATS_URL dropped this way boots every node on its own embedded NATS. IRONFLOW_DATABASE_URL is read under driver: postgres as the URL source (the Helm shape); under driver: sqlite it is ignored and listed like the others — the declared driver is the one that opens.

Three groups escape the rule, and they do not agree on precedence:

  • The IRONFLOW_OTEL_* / IRONFLOW_METRICS_ENABLED variables are read directly by the server and work either way. A field written in spec.observability wins over its environment variable; a field the file leaves out keeps its environment value.
  • IRONFLOW_BLOB_URL and IRONFLOW_ARTIFACT_THRESHOLD are applied on both paths and override spec.blobs.url / spec.blobs.artifactThreshold — the opposite precedence to observability. They are not in the ignored-and-warned set either, so a stray value silently redirects blob overflow with no startup notice.
  • LOG_LEVEL and LOG_FORMAT are read by the CLI before any config is parsed.

The resolved value is used as the field’s value verbatim — it is never re-read as YAML, so a password containing &, *, #, : or a newline lands intact instead of being parsed as an anchor, alias, comment or extra key. References are resolved in values only; ${VAR} in a key position is not substituted.

If an env var is not set and has no default, the error message tells you exactly which field and variable:

config file error: environment variable ${IRONFLOW_DATABASE_URL} is not set

Check your YAML for errors without starting the server:

Terminal window
ironflow validate -f ironflow.yaml

This parses the file, resolves ${VAR} references, runs kind-specific validation, and prints a summary:

Validating ironflow.yaml...
Kind: Cluster
Storage: postgres (postgres://localhost/iron...)
NATS: external (nats://h1:4222,h2:4222)
Auth: enabled (master key set)
Tracing: otel-collector:4317 (sample: 100%)
Metrics: enabled (/metrics)
Node ID: node-1
✓ Valid. Ready to boot with: ironflow serve -f ironflow.yaml

Exit code 0 means valid, exit code 1 means invalid.

The same checks run at ironflow serve -f time, so anything validate accepts boots, and anything it rejects fails fast instead of quietly falling back to a default:

  • Unknown keys are errors. A typo like staleClaimThresold or a mis-nested masterKey used to be dropped silently and never take effect. Both now fail with the offending field named.
  • Durations are parsed. pushTimeout: 30sec is rejected (30s is the Go duration form), as is any non-positive value.
  • Sizes are parsed. nats.maxMemory must be a byte size such as 256MiB or 4GB.
  • Numbers are range-checked. minConns cannot exceed maxConns, storage.pool.maxIdleTime must be positive, and tracing.sampleRate must be within 0.0–1.0 (a typo’d 50 for “50%” is rejected rather than silently clamped to full sampling).

The Tracing and Metrics lines report the effective configuration — the IRONFLOW_OTEL_* / IRONFLOW_METRICS_ENABLED environment of the shell you run validate in, with the file’s spec.observability values layered on top. That is exactly what serve boots with.


CLI flags always take precedence over YAML values. This lets you use a shared YAML file but override specific settings per-node or per-environment:

Terminal window
# YAML says port 9123, but override to 8080 for this node
ironflow serve -f ironflow.yaml --port 8080
# YAML says dev mode is off, but enable it locally
ironflow serve -f ironflow.yaml --dev

Only flags you explicitly pass override the YAML. Unset flags preserve the YAML values.


If you don’t use -f, nothing changes. All existing flags and environment variables work exactly as before:

Terminal window
# These still work — no YAML needed
ironflow serve
ironflow serve --port 9000
ironflow serve --nats-url nats://h1:4222 --node-id node-1
IRONFLOW_DATABASE_URL="postgres://..." ironflow serve

The -f flag is opt-in. You can adopt YAML incrementally.


${VAR} is expanded in every value position, so any field below can be written as field: ${VAR} — including numeric and boolean ones, which keep their type after expansion (port: ${PORT} decodes as an int).

FieldTypeDefaultNotes
apiVersionstringMust be ironflow/v1
kindstringServer, Cluster, or Platform
spec.portint9123HTTP server port
spec.storage.driverstringsqlitesqlite or postgres
spec.storage.urlstringPostgreSQL connection string
spec.storage.pathstring.ironflow/ironflow.dbSQLite file path
spec.storage.pool.maxConnsint25Max PostgreSQL connections. Set to 0 to leave pool sizing to pool_max_conns in the connection URL
spec.storage.pool.minConnsint5Min PostgreSQL connections
spec.storage.pool.maxIdleTimeduration30sIdle connection timeout
spec.blobs.urlstringBlob overflow backend. Empty uses local filesystem; s3://bucket?endpoint=...&region=... selects S3-compatible storage. Environment override: IRONFLOW_BLOB_URL. See Artifact overflow to blob storage.
spec.blobs.artifactThresholdstringSize above which run/step outputs offload (1MB, 512KiB, etc.). Empty or 0 disables new offloads. Environment override: IRONFLOW_ARTIFACT_THRESHOLD.
spec.nats.embeddedbooltrueUse embedded NATS (auto-set to false when url is set)
spec.nats.urlstringExternal NATS URL
spec.nats.credentialsstringPath to .creds file
spec.nats.storeDirstringJetStream storage directory
spec.nats.maxMemorystring256MiBJetStream memory limit (embedded NATS only). SI and IEC units both parse — MB is 10⁶, MiB is 2²⁰
spec.nats.fileStorageboolfalseCreate file-backed JetStream streams
spec.nats.streamReplicasint1JetStream stream replica count
spec.auth.devModeboolfalseBypass all authentication
spec.auth.masterKeystringAES-256 key for secrets
spec.auth.jwtSecretstringJWT signing secret — auto-generated if empty
spec.observability.tracing.endpointstringOTLP gRPC endpoint. Overrides IRONFLOW_OTEL_ENDPOINT
spec.observability.tracing.sampleRatefloat1.0Trace sampling rate. Must be 0.0–1.0; 0 is valid and means “exporter wired, sample nothing”
spec.observability.tracing.serviceNamestringironflowOTel service name
spec.observability.tracing.insecurebooltruePlaintext gRPC for OTLP
spec.observability.metrics.enabledboolfalseEnable Prometheus /metrics. Overrides IRONFLOW_METRICS_ENABLED
spec.engine.pushTimeoutstring10sPush mode HTTP timeout
spec.engine.schedulerIntervalstring1sScheduler poll interval
spec.engine.staleRunningTimeoutstring5mPush-mode stale running run timeout. Does not move the SQL recovery sweep, which stays at 5m so it can never drop below the SDK heartbeat
spec.engine.retry.maxAttemptsint3Default retry attempts. Must be >= 1; 0 is rejected, not “no retries”
spec.engine.retry.initialDelaystring1sFirst retry delay
spec.engine.retry.maxDelaystring5mMaximum retry delay
spec.engine.retry.backofffloat2.0Backoff multiplier. Must be >= 1; a value below 1 would make each retry faster than the last
spec.cluster.nodeIdstringStable node identifier
spec.cluster.staleClaimThresholdstring2mDead node claim recovery
spec.cluster.staleClaimRecoveryIntervalstring60sHow often scheduler scans for orphaned claims
spec.platform.organizations[].namestringOrganization name
spec.platform.organizations[].projects[].namestringProject name
spec.platform.organizations[].projects[].environments[].namestringEnvironment name