joetjen.net
EN DE
Configuration language

Imports and loops

Configuration grows: split it into files with imports, and generate repetitive parts with loops instead of copying them.

Format 1.0

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 @i binds 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.

config.casc
#@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 = true

With 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.