# !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:

```yaml
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 `|`:

```yaml
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:

```yaml
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:

```shell
atmos describe component service -s <stack> --format json
```

The `vars.resource_tags` field contains:

```json
{
  "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:

```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:

```yaml
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](/scripting/language) 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](/scripting) 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`:

```yaml
vars:
  # Evaluates only vars.stage and settings.owner in selective commands.
  label: !starlark |
    return "%s/%s" % (ctx.vars["stage"], ctx.settings.get("owner", "platform"))
```
