Skip to main content

Derive a scaffold field once with type: computed

· 3 min read
Jorrit Elfferich
Mission Critical Engineer @ Schuberg Philis

A scaffold template's spec.fields[] questionnaire is great at collecting answers, but not every value a template needs is really an answer. Some values are just a function of other answers — "the primary region, defaulting to the only region when there's just one" — and until now, every file that needed that value had to re-derive it itself, with the same {{ if .Config.primary_region_select }}...{{ end }} snippet copied into each one.

The Problem​

Templating and scaffolding tools all hit the same shape of problem eventually: a value the generated output needs isn't something the user should be prompted for at all — it's derived from answers they already gave. Ask for a list of regions, then only ask for a primary region when there's more than one; when there's exactly one, it's the primary by definition, no prompt needed. That derivation logic is simple once, but a scaffold template has no single place to put it. It gets pasted into every file that references the value, and every copy has to independently stay in sync with the same conditional. Miss one, or get the fallback logic subtly wrong in one file, and that file quietly disagrees with the rest of the generated project.

The Fix​

A new type: computed field declares a value once — either derived from other answers via a value: Go-template expression, or a plain literal (string, number, boolean, list, or map) used as-is — and is never itself prompted for or settable with --set. A string is only treated as an expression when it actually contains a template action; a plain string like hello is a literal too, same as any other type:

spec:
fields:
- name: regions
type: multiselect
options: [us-east-1, us-west-2, eu-west-1]
- name: primary_region_select
type: select
options: answers.regions
when: "size(answers.regions) > 1"
- name: primary_region
type: computed
value: "{{ ternary answers.primary_region_select (index answers.regions 0) (gt (len answers.regions) 1) }}"

primary_region derives its value exactly once, and .Config.primary_region is then usable everywhere .Config is — file content, target: path templates, and matrix: axes — with no per-file fallback logic to keep in sync. Computed fields evaluate in declaration order, after every regular field's answer is already final, so a computed field can reference any regular field regardless of where it's declared, and any earlier-declared computed field's own result.

A computed field's value: doesn't have to be an expression at all — a plain literal works too, useful for a small hand-authored reference table shared across every file in the template:

- name: provider_version_pins
type: computed
value: {aws: "~> 5.0", azurerm: "~> 3.0", google: "~> 5.0"}

provider_version_pins is stored exactly as written, with no template rendering, and is reachable the same way as any other computed field: .Config.provider_version_pins.

How to Use It​

Add a type: computed field to any existing scaffold.yaml, following the shape above, and reference its name from .Config in any file the template generates:

atmos scaffold generate <template> <target> --set regions=us-east-1,us-west-2

See the Computed Fields section of the atmos scaffold generate docs for the full set of validation rules (value: is required on a computed field and rejected on every other type; required:/default: are both rejected on a computed field) and the ordering constraints that keep a computed field's dependencies resolvable.

Get Involved​

See the atmos scaffold generate docs for the full reference, or open an issue with feedback.