- Deploy & Operate
- YAML Configuration
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.
Why YAML?
Section titled “Why YAML?”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 YAML | With 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 files | Single file, checked into git |
| Easy to forget a flag when deploying | Reproducible on every boot |
Generate a Starter Template
Section titled “Generate a Starter Template”Use ironflow config init to generate a starter YAML for your deployment tier:
# Local development (minimal, SQLite, dev mode)ironflow config init > ironflow.yaml
# Production single-node (PostgreSQL, observability)ironflow config init --prod > ironflow.yaml
# Multi-node clusterironflow config init --kind cluster > ironflow.yaml
# Multi-tenant platformironflow config init --kind platform > ironflow.yamlThree Kinds
Section titled “Three Kinds”Every ironflow.yaml has a kind that determines which features and validations apply:
| Kind | Use Case | Requirements |
|---|---|---|
Server | Local dev, single-node production | None (SQLite + embedded NATS by default) |
Cluster | Multi-node horizontal scaling | PostgreSQL + external NATS + s3:// blob store + stable nodeId |
Platform | Multi-tenant SaaS | Everything 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.
Server: Local Development
Section titled “Server: Local Development”The simplest configuration — 5 lines to start:
apiVersion: ironflow/v1kind: Serverspec: port: 9123 auth: devMode: trueironflow serve -f ironflow.yamlThis boots Ironflow with embedded NATS (file-backed, in ironflow-nats beside the SQLite file), SQLite, and authentication disabled. Similar to ironflow serve --dev, though the --dev flag also tightens crash-resume timings for faster local iteration.
Server: Single-Node Production
Section titled “Server: Single-Node Production”Add PostgreSQL, secrets encryption, and observability:
apiVersion: ironflow/v1kind: Serverspec: port: 9123
storage: driver: postgres url: ${IRONFLOW_DATABASE_URL} pool: maxConns: 25 minConns: 5 maxIdleTime: 30s
nats: storeDir: /data/nats
blobs: url: ${IRONFLOW_BLOB_URL}
auth: bootstrapKeyFile: /run/ironflow/bootstrap-key.json masterKey: ${IRONFLOW_MASTER_KEY} jwtSecret: ${IRONFLOW_JWT_SECRET}
observability: tracing: endpoint: otel-collector:4317 sampleRate: 1.0 metrics: enabled: trueNotice the ${IRONFLOW_DATABASE_URL} syntax — secrets are never stored in the file. See Environment Variable References below.
Cluster: Multi-Node
Section titled “Cluster: Multi-Node”Scale horizontally with shared PostgreSQL, external NATS and a shared s3:// blob store. Without an s3:// blobs.url, serve refuses to start. Deploy the same file on every node — only nodeId differs (set via env var per node):
apiVersion: ironflow/v1kind: Clusterspec: port: 9123
storage: driver: postgres url: ${IRONFLOW_DATABASE_URL} pool: maxConns: 25 minConns: 5
nats: url: ${NATS_URL} # credentials: /path/to/nats.creds
blobs: url: ${IRONFLOW_BLOB_URL} # must be s3:// on a multi-node cluster
cluster: nodeId: ${IRONFLOW_NODE_ID} staleClaimThreshold: 2m
auth: bootstrapKeyFile: /run/ironflow/bootstrap-key.json masterKey: ${IRONFLOW_MASTER_KEY} jwtSecret: ${IRONFLOW_JWT_SECRET}
observability: tracing: endpoint: otel-collector:4317 sampleRate: 1.0 metrics: enabled: trueCluster kind enforces three rules at startup:
- PostgreSQL required — SQLite does not support
SKIP LOCKEDfor distributed scheduling - External NATS required — embedded NATS is single-process only
- Stable
nodeIdrequired — used for claim ownership and log correlation
See Docker Compose Deployment for the full clustering guide.
Platform: Multi-Tenant
Section titled “Platform: Multi-Tenant”Declare organizations, projects, and environments. Ironflow creates them on first boot and is idempotent on subsequent boots:
apiVersion: ironflow/v1kind: Platformspec: port: 9123
storage: driver: postgres url: ${IRONFLOW_DATABASE_URL}
nats: url: ${NATS_URL}
cluster: nodeId: ${IRONFLOW_NODE_ID}
blobs: url: ${IRONFLOW_BLOB_URL}
auth: bootstrapKeyFile: /run/ironflow/bootstrap-key.json masterKey: ${IRONFLOW_MASTER_KEY} jwtSecret: ${IRONFLOW_JWT_SECRET}
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: productionPlatform 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.
Environment Variable References
Section titled “Environment Variable References”The YAML file is safe to commit to git because secrets use ${VAR} references that resolve at boot time:
| Syntax | Behavior |
|---|---|
${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://literal | Literal 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_SYNC_INTERVAL,
NATS_FILE_STORAGE, NATS_STREAM_REPLICAS, IRONFLOW_MASTER_KEY,
IRONFLOW_JWT_SECRET, IRONFLOW_JWT_SECRET_FILE, IRONFLOW_POLICY_ENFORCEMENT, IRONFLOW_NODE_ID, IRONFLOW_STALE_CLAIM_THRESHOLD,
IRONFLOW_STALE_CLAIM_RECOVERY_INTERVAL, IRONFLOW_MCP_REMOTE) 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; 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_ENABLEDvariables are read directly by the server and work either way. A field written inspec.observabilitywins over its environment variable; a field the file leaves out keeps its environment value. IRONFLOW_BLOB_URL,IRONFLOW_ARTIFACT_THRESHOLD,IRONFLOW_FILES_MAX_OBJECT_BYTES,IRONFLOW_FILES_RECONCILEandIRONFLOW_FILES_PUBLIC_BASE_URLare applied on both paths and overridespec.blobs.url/spec.blobs.artifactThreshold/spec.files.maxObjectBytes/spec.files.reconcile/spec.files.publicBaseURL— the opposite precedence to observability. They are not in the ignored-and-warned set either, so a stray value silently changes blob overflow, the file size cap, background cleanup or signed file URLs with no startup notice.LOG_LEVELandLOG_FORMATare 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 setValidate Without Booting
Section titled “Validate Without Booting”Check your YAML for errors without starting the server:
ironflow validate -f ironflow.yamlThis parses the file, resolves ${VAR} references, runs kind-specific validation, and prints a summary:
Validating ironflow.yaml...
Kind: Cluster Storage: postgres (connection string redacted) 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.yamlExit 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
staleClaimThresoldor a mis-nestedmasterKeyused to be dropped silently and never take effect. Both now fail with the offending field named. - Durations are parsed.
pushTimeout: 30secis rejected (30sis the Go duration form), as is any non-positive value.nats.syncIntervalalso takesalways. - Sizes are parsed.
nats.maxMemorymust be a byte size such as256MiBor4GB. - Numbers are range-checked.
minConnscannot exceedmaxConns,storage.pool.maxIdleTimemust be positive, andtracing.sampleRatemust be within 0.0–1.0 (a typo’d50for “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 Flag Overrides
Section titled “CLI Flag Overrides”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:
# YAML says port 9123, but override to 8080 for this nodeironflow serve -f ironflow.yaml --port 8080
# YAML says dev mode is off, but enable it locallyironflow serve -f ironflow.yaml --devOnly flags you explicitly pass override the YAML. Unset flags preserve the YAML values.
Backward Compatibility
Section titled “Backward Compatibility”If you don’t use -f, nothing changes. All existing flags and environment variables work exactly as before:
# These still work — no YAML neededironflow serveironflow serve --port 9000ironflow serve --nats-url nats://h1:4222 --node-id node-1IRONFLOW_DATABASE_URL="postgres://..." IRONFLOW_BLOB_URL="$IRONFLOW_BLOB_URL" IRONFLOW_JWT_SECRET="$IRONFLOW_JWT_SECRET" ironflow serve --bootstrap-key-file /run/ironflow/bootstrap-key.json --nats-store-dir /data/nats # PostgreSQL + embedded NATS needs a store dirThe -f flag is opt-in. You can adopt YAML incrementally.
Retry Defaults
Section titled “Retry Defaults”spec.engine.retry sets the retry policy for every function that does not set its own.
A function’s own retry setting wins, field by field.
spec: engine: retry: maxAttempts: 25 # total attempts, the first included initialDelay: 1s backoff: 2.0 maxDelay: 5m onExhausted: pause # or: fail nonRetryableStatuses: [400, 401, 403, 405, 422]The defaults are 25 attempts, 1s, ×2, capped at 5m, about 88 minutes of retrying in
all, then pause. The values are read when each run is created and stored on the run,
so changing them moves the runs created afterwards, not the ones already in flight. A
function registered before you changed them picks up the new values on its next run;
re-registering it is not needed.
nonRetryableStatuses applies to push endpoints only. See
Error handling for what retrying, pausing
and failing mean for a run.
Full Field Reference
Section titled “Full Field Reference”${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).
| Field | Type | Default | Notes |
|---|---|---|---|
apiVersion | string | — | Must be ironflow/v1 |
kind | string | — | Server, Cluster, or Platform |
spec.port | int | 9123 | HTTP server port |
spec.storage.driver | string | sqlite | sqlite or postgres |
spec.storage.url | string | — | PostgreSQL connection string, URI or keyword/value form |
spec.storage.path | string | .ironflow/ironflow.db | SQLite file path |
spec.storage.pool.maxConns | int | 25 | Max PostgreSQL connections. Set to 0 to leave pool sizing to pool_max_conns in the connection URL |
spec.storage.pool.minConns | int | 5 | Min PostgreSQL connections |
spec.storage.pool.maxIdleTime | duration | 30s | Idle connection timeout |
spec.blobs.url | string | — | Blob overflow backend. Empty uses local filesystem under SQLite; s3://bucket?endpoint=...®ion=... selects S3-compatible storage. Required as s3:// under PostgreSQL, or serve refuses to start. Environment override: IRONFLOW_BLOB_URL. See Artifact overflow to blob storage. |
spec.blobs.artifactThreshold | string | — | Size above which run/step outputs offload (1MB, 512KiB, etc.). Empty or 0 disables new offloads. Environment override: IRONFLOW_ARTIFACT_THRESHOLD. |
spec.files.maxObjectBytes | string | 100MiB | Server-wide size cap for one stored file (100MiB, 104857600, etc.). A bucket cap can be lower, never higher. Environment override: IRONFLOW_FILES_MAX_OBJECT_BYTES. File bytes use the spec.blobs.url backend. See Files. |
spec.files.reconcile | string | on | Background file cleanup mode: on retries recorded retirements, dry-run reports queue and prefix state without mutations, off starts no worker. Explicit API cleanup runs in every mode. Environment override: IRONFLOW_FILES_RECONCILE. See Files. |
spec.files.publicBaseURL | string | Request origin | External absolute HTTP(S) base URL for signed file URLs. Preserves an optional proxy path prefix and removes trailing slashes. Credentials, queries and fragments are rejected. Environment override: IRONFLOW_FILES_PUBLIC_BASE_URL, on both serve config paths. See Files. |
spec.nats.embedded | bool | true | Use embedded NATS (auto-set to false when url is set) |
spec.nats.url | string | — | External NATS URL |
spec.nats.credentials | string | — | Path to .creds file |
spec.nats.storeDir | string | ironflow-nats beside the SQLite file | JetStream storage directory. With PostgreSQL or :memory: SQLite and embedded NATS, you must set it |
spec.nats.maxMemory | string | 256MiB | JetStream memory limit (embedded NATS only). SI and IEC units both parse — MB is 10⁶, MiB is 2²⁰ |
spec.nats.fileStorage | bool | false | Create file-backed JetStream streams. The cluster Compose files set NATS_FILE_STORAGE=true on the flags-only path instead of this field |
spec.nats.streamReplicas | int | 1 | Replica count for the JetStream streams, the system KV buckets, and KV buckets created after startup |
spec.nats.syncInterval | string | always | Embedded JetStream fsync policy. always fsyncs every write before the ack. A positive Go duration (1s, 2m) fsyncs on that interval: faster, but a power loss can lose acknowledged writes. Ignored with external NATS. See Write durability |
spec.auth.devMode | bool | false | Bypass all authentication |
spec.auth.masterKey | string | — | AES-256 key for secrets |
spec.auth.jwtSecret | string | — | Hex JWT signing secret. Required under PostgreSQL unless jwtSecretFile is configured; auto-generated if empty under SQLite |
spec.auth.jwtSecretFile | string | (None) | Existing private raw hex JWT key file owned by the engine user. Mutually exclusive with jwtSecret; supports persistent API rotation. |
spec.auth.policyEnforcement | bool | false | Enforce stored Rego policy sets on top of RBAC. An evaluation error denies the request in enforce mode. Start new sets in monitor mode. Env: IRONFLOW_POLICY_ENFORCEMENT (flags-only path) |
spec.auth.bootstrapKeyFile | string | SQLite: beside the database | First-boot admin key output. Required under PostgreSQL. Non-empty --bootstrap-key-file overrides this field. |
spec.headless | bool | false | Serve no dashboard: every non-API path returns a plain 404. An explicit --headless flag wins over this field. See Headless Mode |
spec.mcp.remote | bool | false | Serve MCP at /api/v1/mcp for hosted agents. Each tool call runs with the caller’s own API key. See Remote MCP |
spec.observability.tracing.endpoint | string | — | OTLP gRPC endpoint. Overrides IRONFLOW_OTEL_ENDPOINT |
spec.observability.tracing.sampleRate | float | 1.0 | Trace sampling rate. Must be 0.0–1.0; 0 is valid and means “exporter wired, sample nothing” |
spec.observability.tracing.serviceName | string | ironflow | OTel service name |
spec.observability.tracing.insecure | bool | true | Plaintext gRPC for OTLP |
spec.observability.metrics.enabled | bool | false | Enable Prometheus /metrics. Overrides IRONFLOW_METRICS_ENABLED |
spec.observability.metrics.port | int | 0 | Dedicated listener port (1–65535, distinct from API). Zero uses the API port; disabled metrics opens no listener |
spec.engine.pushTimeout | string | 10s | Push mode HTTP timeout |
spec.engine.schedulerInterval | string | 1s | Scheduler poll interval |
spec.engine.staleRunningTimeout | string | 5m | Push-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.maxAttempts | int | 25 | Default total attempts, the first included, for a function that sets none. Must be >= 1; 0 is rejected, not “no retries” (use 1) |
spec.engine.retry.initialDelay | string | 1s | Default delay base: the first retry waits initialDelay × backoff |
spec.engine.retry.maxDelay | string | 5m | Maximum retry delay |
spec.engine.retry.backoff | float | 2.0 | Backoff multiplier. Must be >= 1; a value below 1 would make each retry faster than the last |
spec.engine.retry.onExhausted | string | pause | What happens to a run that used up its attempts on retryable failures: pause (wait for ironflow run resume) or fail |
spec.engine.retry.nonRetryableStatuses | list of int | [400, 401, 403, 405, 422] | HTTP statuses (each 400 to 599) from a push endpoint that fail a run at once. Replaces the default list. An empty list ([]) retries every status |
spec.engine.circuitBreaker.failureThreshold | int | 5 | Consecutive failures that open a push function’s circuit breaker. Must be >= 1. A function’s own circuit_breaker setting overrides it. See Circuit breakers |
spec.engine.circuitBreaker.openDuration | string | 60s | First open window before a probe is allowed. At most 24h; raised to 2 × pushTimeout + 1.3s if shorter, so a probe always finishes inside its window |
spec.engine.circuitBreaker.maxOpenDuration | string | 10m | Cap on the open window, which doubles on each failed probe. At most 24h. A value below openDuration is raised to it, which turns escalation off |
spec.cluster.nodeId | string | — | Stable node identifier |
spec.cluster.staleClaimThreshold | string | 2m | Dead node claim recovery |
spec.cluster.staleClaimRecoveryInterval | string | 60s | How often scheduler scans for orphaned claims |
spec.platform.organizations[].name | string | — | Organization name |
spec.platform.organizations[].projects[].name | string | — | Project name |
spec.platform.organizations[].projects[].environments[].name | string | — | Environment name |
What’s Next?
Section titled “What’s Next?”- Docker Compose Deployment — detailed clustering guide with Docker Compose examples
- Observability — tracing and metrics configuration
- API Keys — authentication setup
- Secrets Management — encrypting secrets at rest