Skip to main content

!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. Use ctx.metadata.get("component") for the base implementation the instance uses.
ctx.component_type
The component type, such as terraform or helmfile, 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 ... --columns
  • atmos describe stacks --sections and --query
  • atmos 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 as ctx.vars["network"]["cidr"]
  • ctx.<section>.get("key") and ctx.<section>.get("key", default) with a string literal as the key
  • ctx.stack, ctx.component, and ctx.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"))