String interpolation
A double-quoted string may embed ${<ref>} placeholders. Each placeholder is replaced at config-load time with the resolved value of the reference (see References & scope), coerced to its string form.
env = {
DATABASE_URL = "postgresql://app:${secret.pg.value}@${host.primary.addr}:5432/app"
}
string ::= '"' ( char | escape | interp )* '"'
interp ::= "${" ref "}"
ref ::= ident ( "." ident )+
escape ::= "\\\"" | "\\\\" | "\\n" | "\\r" | "\\t" | "\\${"
A string that contains no ${...} lexes as a plain string literal. A string with at least one ${...} lexes as a template (alternating literal and interpolation segments) and the evaluator concatenates the resolved parts.
Allowed refs inside ${...}
Any reference the References & scope rules accept:
host.<name>.<field>— including nested fields likehost.prod.ssh.addr.secret.<name>.value— the resolved plaintext flows in. State stores a substring marker (see Secrets).
A bare identifier inside ${...} is rejected:
${foo} ← error: "bare identifier `foo` not allowed in `${...}`"
${secret.foo} ← error: too short (secret refs need exactly 3 segments)
${a.${b}} ← error: "nested `${` is not supported"
${} ← error: "empty `${}` is not allowed"
Scalar coercion
The reference must resolve to a scalar — string, number, or bool. Numbers render via their JSON form (4000, 1.5), bools as true / false. Lists, maps, or null error at evaluation time:
cannot interpolate non-scalar value `${host.h.tags}` into a string
(refs inside `${...}` must resolve to a string, number, or bool)
Escaping
\${ produces a literal ${ in the output and is not a placeholder. There is no other \$ escape — a bare \$ followed by anything else is a lex error.
shell_var = "literal \${HOME} not stratum" # -> "literal ${HOME} not stratum"
The honesty guard still applies
Embedding secret.X.value inside a string interpolation lands in the same forbidden-attr check as a bare secret reference. The check fires by attr name, not by ref form:
# Both are rejected — system_file.content is in SECRET_FORBIDDEN_ATTRS.
content = secret.s.value
content = "prefix ${secret.s.value} suffix"
See Secrets: the honesty guard for the list of forbidden (kind, attr) pairs and the reasoning.
Where interpolation is most useful
Connection strings and other glued-together values where a bare ref doesn't fit:
resource "docker_container" "api" {
host = host.primary.addr
image = "api:dev"
env = {
# Embed a secret inside a URI — bare `secret.pg.value` would only work
# if the whole env value were the password.
DATABASE_URL = "postgresql://app:${secret.pg.value}@db:5432/app"
# Combine multiple host fields.
INTERNAL_API = "http://${host.primary.addr}:4000"
}
}
When a secret is interpolated into a larger string, state stores the value with an inline substring marker ("--requirepass <secret:pg:sha256:HEX>") instead of replacing the whole leaf with an object marker. See Secrets: substring redaction.