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:
varsblock — your YAML-defined variables- Environment variables —
os.environ - Default value — the part after
:- - 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 to1024stays 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.varsis 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 ofload()— it returns the validatedAppConfigwithout starting anything.