!starlark
Compute configuration values with the Atmos Automation Language. Use functions, conditions, and loops to derive tags, names, lists, and maps from the component's merged configuration.
Usage
Write a function body inside a !starlark YAML scalar. Use return to supply the
value that replaces it:
vars:
stage: dev
region: us-east-2
resource_tags: !starlark |
return {
"Environment": ctx.vars["stage"],
"Region": ctx.vars["region"],
"Owner": ctx.settings.get("owner", "platform"),
}
Atmos evaluates the code after imports, inheritance, and overrides have been
merged. An inherited expression reads the consuming component's values. For
example, overriding stage to prod produces an Environment tag of prod.
An expression is a scalar during inheritance. A higher-precedence expression or concrete value replaces that field; returned dictionaries are not deep-merged with lower-precedence values.
A single-line body that contains a colon followed by a space, such as !starlark return {"a": 1}, is not valid
YAML. Quote the whole scalar, or write the body as a block scalar with |:
vars:
inline: !starlark 'return {"a": 1}'
block: !starlark |
return {"a": 1}
The returned dictionary becomes a YAML mapping. Strings, booleans, numbers,
lists, and None also retain their types; None becomes YAML null.
Try it
In an Atmos project with a Terraform component named service, add this
component to a stack manifest:
vars:
stage: dev
region: us-east-2
components:
terraform:
service:
settings:
owner: platform
vars:
resource_tags: !starlark |
return {
"Environment": ctx.vars["stage"],
"Region": ctx.vars["region"],
"Owner": ctx.settings.get("owner", "platform"),
}
Inspect it with your project's stack name:
atmos describe component service -s <stack> --format json
The vars.resource_tags field contains:
{
"Environment": "dev",
"Region": "us-east-2",
"Owner": "platform"
}
The example reads the owner from settings because the stack manifest schema closes the metadata
section to known fields. An unknown key such as metadata.owner fails atmos validate stacks.
Context
The read-only ctx object exposes the effective component configuration.
Access sections as attributes, then use dictionary indexing or .get() for
values. Mappings also support iteration, .keys(), .values(), and .items().
ctx.vars- Merged component variables, including inherited values and overrides.
ctx.metadata- Component metadata. Use
.get()for optional fields. ctx.settings- Merged component settings.
ctx.env- The component's configured environment mapping. This is not the process environment.
ctx.locals- Local values available in the component configuration, including stack-level locals and the component's own locals.
ctx.stack- The stack being evaluated.
ctx.component- The component instance name when evaluation has a component context, such as
api. Usectx.metadata.get("component")for the base implementation the instance uses. ctx.component_type- The component type, such as
terraformorhelmfile, when available.
Attribute access works on ctx itself (ctx.vars, ctx.stack). Inside a section, use subscripts and .get():
ctx.vars["network"]["cidr"] works, while ctx.vars.network and ctx.vars["network"].cidr do not.
The mappings convert to ordinary values: dict(ctx.vars), json.encode(ctx.vars), and **ctx.vars in a call all work.
Missing configuration sections are empty mappings. Reading a missing key with
ctx.vars["name"] fails; ctx.vars.get("name", "default") supplies a fallback.
The context and its nested values cannot be modified. Build and return a new
value instead.
Computed dependencies
Values are resolved when accessed, so a computed field can depend on another computed field regardless of their order in YAML:
vars:
replicas: !starlark |
return ctx.vars["minimum_replicas"] * 2
minimum_replicas: !starlark |
return 3 if ctx.vars["stage"] == "prod" else 1
stage: prod
Here, replicas is 6. Each field is resolved once per component evaluation.
A circular dependency fails with the configuration paths involved, rather than
depending on YAML key order.
Go templates run before YAML functions. Starlark can read rendered values from
ctx; a Go template cannot read the result of a !starlark expression in that
same pass. Source inside !starlark is literal: {{ ... }} inside a Starlark
string stays unchanged.
Functions and results
A block is an implicit function body. It supports local variables, helper functions, loops, conditions, and early returns:
vars:
stage: dev
enabled_regions: [us-east-1, us-east-2]
deployment_names: !starlark |
def name(region):
return "%s-%s" % (ctx.vars["stage"], region)
return [name(region) for region in ctx.vars["enabled_regions"]]
The evaluator includes Starlark builtins, sum, round, and the json module.
See the language overview for the language's syntax and
Starlark background.
Return None, a boolean, string, finite number, list, or dictionary with string
keys. Integers must fit a signed or unsigned 64-bit value. An empty list or
dictionary remains empty. Falling through without a return produces null.
Functions, sets, tuples, non-string dictionary keys, and cyclic collections are
rejected as return values.
Evaluation scope
Use !starlark in component configuration values within stack manifests. It
computes values after structural merging; it does not generate imports,
component names, inheritance rules, or stack identities. It is not supported in
atmos.yaml, profiles, imported atmos.yaml configuration files, or scaffold configuration. Atmos rejects the tag
there when it loads the configuration, and the error names the file and the key that holds it.
Fields that Atmos reads to compute the stack name cannot use !starlark. When
stacks.name_template or stacks.name_pattern consumes a field such as vars.stage, a !starlark value there
fails with stack identity cannot be computed with !starlark, even when selecting
the stack by its manifest path. Set identity fields to literal values instead.
Stack identity is the one place where commands differ on purpose. atmos describe stacks rejects a stack identity
computed with !starlark, while atmos terraform generate varfiles and atmos terraform generate backends accept it.
The selector fields metadata.tags and metadata.labels reject !starlark.
They determine component selection before full evaluation, while context reads
can resolve dependencies that require authentication or execute commands.
This configuration evaluator provides ctx, language builtins, and json.
Automation modules such as exec, fs, and steps, and module loading with
load(), are unavailable here. Use automation scripts for
commands, prompts, and other operations. Existing YAML functions referenced
through ctx retain their own behavior, including any external reads they
perform.
Each block has a budget of 100,000 interpreter steps. This catches runaway loops; it is not a hard memory or CPU limit. Evaluation errors include the configuration field and, for source errors, the originating filename and line.
Commands that support --skip=starlark preserve the expression without
evaluating it. What the command outputs is the source as the manifest loader encoded it, not the literal text you
typed in the file.
Selective evaluation
Commands that print only part of the configuration do not evaluate every !starlark value in the component. They
evaluate the configuration fields a !starlark value reads through ctx:
atmos list ... --columnsatmos describe stacks --sectionsand--queryatmos describe component --query
Atmos reads your Starlark source to find those fields. These reads have a known boundary, so the command evaluates only what they name:
ctx.<section>["key"], and chains of subscripts such asctx.vars["network"]["cidr"]ctx.<section>.get("key")andctx.<section>.get("key", default)with a string literal as the keyctx.stack,ctx.component, andctx.component_type
The sections are vars, metadata, settings, env, and locals.
Anything else falls back to evaluating the full component. That covers a whole section used as a value
(dict(ctx.vars), json.encode(ctx.vars), iteration, or an alias such as v = ctx.vars), a computed key
(ctx.vars[name]), get with a key that is not a string literal, items(), keys(), and values(), ctx passed to
a function, a ctx field that does not exist, and source that does not parse. The fallback is always correct; it only
costs the time of the full evaluation.
A full atmos describe component with no --query always evaluates everything.
Write reads in the bounded forms when a value feeds a command you run often, such as a column in atmos list:
vars:
# Evaluates only vars.stage and settings.owner in selective commands.
label: !starlark |
return "%s/%s" % (ctx.vars["stage"], ctx.settings.get("owner", "platform"))