Imports and loops
Configuration grows: split it into files with imports, and generate repetitive parts with loops instead of copying them.
Imports
import "shared/logging.casc"
import "config/{base,dev}/**.casc"
import "env/${APP_ENV:dev}.casc"
import "vault://secret/base"- A path is relative to the importing file.
- Braces
{a,b}and globs*,**expand; the matches load in lexicographic order. - A path may read the environment with
${NAME}or${NAME:default}— nothing else, so an import can never depend on the configuration it is part of. - A
scheme://path goes to a loader the application registers. - A path that matches nothing is an error, and so is an import cycle, reported with the whole chain.
- Every imported file carries its own version header.
An import merges the file’s statements in at its own position, under the usual rules: what comes later wins. Write imports first, so the file’s own statements override what it imports.
Loops
for <bindings> [from <template>] as <destination> { <body> }A loop writes its body once per element, each time to a destination path that usually interpolates the element. Bindings:
@x in @{list}binds each element; at least one such binding is required.- Several element bindings walk their lists side by side (zipped, not every combination). The lists must be equally long.
- A bare
@ibinds the index, from 0. - Iterate over
@{…}variables; a%{…}reference cannot be a loop source, because it only resolves after loops are expanded. - Bindings exist only inside the body and shadow outer variables.
@domains = ["us-east.example.com", "eu-west.example.com"]
@ports = [8443, 8444]
for @idx, @domain in @{domains}, @port in @{ports} as endpoints."domain-@{idx}" {
url = "https://@{domain}:@{port}"
}%{"endpoints" => %{
"domain-0" => %{"url" => "https://us-east.example.com:8443"},
"domain-1" => %{"url" => "https://eu-west.example.com:8444"}}}Templates
from <path> starts each generated block as a copy of another key and applies the body on top. The template itself stays where it is.
defaults.replica {
cpu = 1
memory = 512MiB
}
@instances = ["a", "b"]
for @name in @{instances} from defaults.replica as replicas."@{name}" {
cpu = 2
}%{"defaults" => %{"replica" => %{"cpu" => 1, "memory" => {:bytes, 536870912}}},
"replicas" => %{
"a" => %{"cpu" => 2, "memory" => {:bytes, 536870912}},
"b" => %{"cpu" => 2, "memory" => {:bytes, 536870912}}}}A complete example
Everything together, as a service might configure itself. It loads next to an env/dev.casc that sets server.port = 4000.
#@version = 1.0
# Shared values first, so every imported file can see them.
@region = ${REGION:"eu-west"}
@*suffix = ".internal"
@shards = ["a", "b", "c"]
@weights = [3, 2, 1]
import "env/${APP_ENV:dev}.casc"
server {
host = "0.0.0.0"
port = !int(${PORT:8080})
tags = ["web", "api"]
location = (52.5200, 13.4050)
}
database {
host = "db.@{region}@{suffix}"
*password = ${DB_PASSWORD:?"DB_PASSWORD is required"}
pool_size = 10
timeout = 500ms
idle_ttl = 1h30m
cache = 512MiB
allow = [10.0.0.0/8, ::1/128]
}
logger {
level = info
format = ${LOG_FORMAT:"text" | downcase}
started = 2026-09-29T08:00:00Z
}
motd = """
Welcome to the service.
Maintenance window: Sundays.
"""
${?ENABLE_METRICS}
metrics.url = "http://%{server.host}:%{server.port}/metrics"
defaults.replica { cpu = 1, memory = 256MiB }
for @i, @shard in @{shards}, @w in @{weights} from defaults.replica as replicas."shard-@{shard}" {
weight = @{w}
index = @{i}
}
+server.tags = ["internal"]
#legacy.enabled = trueWith DB_PASSWORD set, ENABLE_METRICS=1 and LOG_FORMAT=JSON, the server keeps port 8080 (its block comes after the import), its tags become ["web", "api", "internal"], the database host is "db.eu-west.internal" with the password redacted, the log format is "json", the metrics URL is "http://0.0.0.0:8080/metrics", three replicas shard-a to shard-c each start from the template, and legacy is absent.