Skip to content

Chapter 3: Variables — Environment-Driven Config

← Back to Table of Contents | ← Previous: Models


kaboo-workflows supports Docker Compose-style ${VAR} interpolation. Define variables in the vars block, reference them anywhere with ${VAR}, and optionally provide defaults with ${VAR:-fallback}.

vars:
  MODEL: ${MODEL:-us.anthropic.claude-sonnet-4-6-v1:0}
  TONE:  ${TONE:-friendly}
  MAX_TOKENS: ${MAX_TOKENS:-1024}

models:
  default:
    provider: bedrock
    model_id: ${MODEL}
    params:
      max_tokens: ${MAX_TOKENS}

agents:
  assistant:
    model: default
    system_prompt: "You are a ${TONE} assistant."

entry: assistant

Lookup Order

When kaboo-workflows sees ${SOMETHING}, it resolves it in this order:

  1. vars block — your YAML-defined variables
  2. Environment variablesos.environ
  3. Default value — the part after :-
  4. Error — if none of the above, loading fails with a clear message

So ${MODEL:-gpt-4o} means: "use the MODEL var if defined, then check the environment, then fall back to gpt-4o."

Override at Runtime

# Linux/macOS
TONE=formal MODEL=gpt-4o python main.py

# Windows PowerShell
$env:TONE="formal"; $env:MODEL="gpt-4o"; python main.py

Variable Chaining

Variables can reference other variables:

vars:
  BASE_MODEL: us.anthropic.claude-sonnet-4-6-v1:0
  MODEL: ${BASE_MODEL}

This works because kaboo-workflows resolves vars in two sequential passes — the first pass resolves against environment variables, the second pass resolves cross-references between vars. Circular references (A: ${B}, B: ${A}) are caught and raise a clear error.

Type Preservation

Here's a subtle but powerful feature: when the entire value is a single ${VAR} reference (not embedded in a larger string), the original type is preserved.

vars:
  MAX_TOKENS: 1024    # This is an integer in YAML

models:
  default:
    provider: bedrock
    model_id: some-model
    params:
      max_tokens: ${MAX_TOKENS}   # Resolves to integer 1024, not string "1024"

But if you embed it in a string, it becomes a string:

system_prompt: "Use max ${MAX_TOKENS} tokens"  # "Use max 1024 tokens" (string)

This is important for parameters that expect a specific type (like max_tokens needing an int).

Variables Without Defaults

If you reference a variable that doesn't exist and has no default, loading fails immediately:

vars:
  MODEL: ${REQUIRED_MODEL}  # No :- default!
ValueError: Variable '${REQUIRED_MODEL}' is not set in 'vars:' or environment,
and no default was provided.
Use ${REQUIRED_MODEL:-fallback} to set a fallback value.

This is intentional — it forces explicit configuration for deployment-critical values.

Set-but-empty is not unset

The check is whether the name exists, not whether it has a useful value. An environment variable set to the empty string exists, so its default is not applied and the empty string is substituted:

GATEWAY_URL= python app.py      # ${GATEWAY_URL:-http://localhost:8090} -> ""

That is why a variable you want to be mandatory is better left out of .env entirely than written with a blank value — blank passes the check and fails later, somewhere less obvious.

Escaping: Passing ${...} Through Untouched

Some downstream consumer may have its own ${...} syntax that must survive interpolation — a prompt template filled in later, for instance. Write $${ to emit a literal ${:

agents:
  assistant:
    system_prompt: "Substitute $${user_name} yourself, later."

That loads as Substitute ${user_name} yourself, later. A $$ not followed by { is left exactly as written, so ordinary shell or currency text needs no special handling.

What Interpolation Applies To

Interpolation runs after the YAML is parsed, walking every string in the tree — keys' values, list items, nested dicts alike. Two consequences are worth knowing:

  • A value that is exactly ${VAR} keeps its native type, so ${MAX_TOKENS} resolving to 1024 stays an integer rather than becoming "1024".
  • A variable embedded in a larger string is coerced to text and concatenated, as you would expect.

Lookup order is vars first, then the environment, then the :- default.

Per-Source Interpolation

When you use multi-file configs, each file's vars block is interpolated independently before merging. File A's vars don't leak into File B's interpolation.

Tips & Tricks

  • Use variables for anything that changes between environments: model IDs, API endpoints, log levels, session directories.
  • The ${VAR:-default} pattern is your best friend for making configs self-contained — they work out of the box but can be customized via environment.
  • vars is removed after interpolation — it never reaches schema validation. So you can put anything in there, even nested dicts and lists (though string/number values are most common).
  • Want to see what resolved? Use load_config() instead of load() — it returns the validated AppConfig without starting anything.

Next: Chapter 4 — YAML Anchors →