atmos scaffold generate
Generate a component, configuration, or project shape from a template. The template's versioned manifest owns its fields, conditions, files, hooks, and update provenance.
Usage
atmos scaffold generate [template] [target] [flags]
Examples
# Prompt for active fields and generate into a new directory.
atmos scaffold generate terraform-component ./components/terraform/vpc
# Supply values for automation; defaults satisfy required fields.
atmos scaffold generate terraform-component ./components/terraform/vpc \
--defaults \
--set component_name=vpc \
--set environments=dev,staging
# Preview a template without writing files or running generation hooks.
atmos scaffold generate terraform-component ./preview --dry-run --skip-hooks
# Bring a recorded project forward after its template changes.
atmos scaffold generate terraform-component ./components/terraform/vpc \
--update --merge-strategy=manual
# Force line-oriented text merging so YAML formatting (e.g. blank lines) survives an update.
atmos scaffold generate terraform-component ./components/terraform/vpc \
--update --merge-driver=text
# Update without relying on the target's own Git history for the merge base.
atmos scaffold generate terraform-component ./components/terraform/vpc \
--update --update-strategy=rendered
Template Sources
Select an embedded template, a template declared under scaffold.templates in atmos.yaml, or a
local/remote source — git, HTTPS, S3, or an OCI registry reference. A git source can be pinned
with --ref to make a release, tag, or commit explicit; --ref has no effect on OCI/S3/local
sources, which address a specific version through the source string itself.
atmos scaffold list
atmos scaffold generate ./scaffolds/terraform-component ./components/terraform/vpc
atmos scaffold generate https://github.com/example/platform-templates.git ./output --ref v1.2.0
atmos scaffold generate oci://ghcr.io/example/templates:v1.0.0 ./components/terraform/vpc
An OCI source is pulled the same way atmos vendor pull fetches OCI-hosted components —
authentication uses the same precedence (Docker keychain, then ATMOS_GITHUB_TOKEN for
ghcr.io, then anonymous); see Vendor URL Syntax for
details.
Template Configuration
Use the versioned manifest below; the former top-level prompts: key is not valid.
apiVersion: atmos/v1
kind: AtmosScaffoldConfig
metadata:
name: terraform-component
spec:
fields:
- name: component_name
label: Component name
type: input
required: true
validation:
pattern: "^[a-z0-9-]+$"
- name: create_monitoring
type: confirm
default: false
- name: alert_email
type: input
when: "answers.create_monitoring == true"
files:
- path: monitoring.tf
when: "answers.create_monitoring == true"
Fields render into template content and paths through {{ .Config.<field> }}. Required, option,
boolean, and regular-expression validation is enforced after all answer sources merge: interactive
answers, defaults, saved spec.values, and --set. select and multiselect require values from
their declared options; false remains a valid answer for a required boolean.
when: accepts predicate words, CEL, or an implicit-all list. Conditions can inspect only
earlier field answers through answers; use CEL (&&, ||, !) for compound logic because the
map-style {all, any, not} form is not accepted by scaffold manifests.
Dynamic and Label/Value Options
select and multiselect fields declare their choices through options:, which accepts four
shapes. A plain list of strings is the common case, where each choice's label and underlying
value are the same:
spec:
fields:
- name: environment
type: select
options: [dev, staging, production]
An option can instead be a {label, value} object, for when the displayed choice should read
differently from the value stored in answers and passed to templates. value is required;
label defaults to value when omitted:
spec:
fields:
- name: environment
type: select
options:
- label: Development
value: dev
- label: Staging
value: staging
- value: production # No label -- displays as "production".
options: can also be sourced dynamically, using exactly the two forms spec.files[].matrix
axes support below: a dot-path into answers.*, or a Go-template expression:
spec:
fields:
- name: environments
type: multiselect
options: [dev, staging, production]
# Sourced from the prior multiselect answer -- only offers environments
# the user actually selected above.
- name: default_environment
type: select
options: "answers.environments"
spec:
fields:
- name: csv_owners
type: input
label: Comma-separated list of component owners (e.g. GitHub teams)
- name: primary_owner
type: select
options: '{{ splitList "," answers.csv_owners }}'
A dot-path source must resolve to an already list-shaped answer -- a multiselect answer, a
spec.values preset, or a --set-supplied value never declared as a field at all -- while a
template expression computes the list the same way a matrix axis expression does. Both dynamic
forms resolve correctly against whatever the earlier field was ultimately answered, whether that
answer came from the interactive prompt (fields are prompted one at a time, so a later field is
only ever shown after the ones before it) or from --set/--defaults.
When a later field's dot-path sources directly from a field using {label, value} options,
labels are recovered for the filtered subset of values present in the referenced answer:
spec:
fields:
- name: environments
type: multiselect
options:
- label: Development
value: dev
- label: Staging
value: staging
- label: Production
value: prod
# environments answer is [dev, staging] -> default_environment offers
# "Development" and "Staging" -- not "Production", and not the raw
# "dev"/"staging" values.
- name: default_environment
type: select
options: "answers.environments"
Only values ever reach answers and templates; labels are presentation-only.
Two limitations to keep in mind: there's no field-declaration-order validation at load time, so a
forward reference, self-reference, or typo'd dot-path loads successfully and degrades to an empty
option list (no constraint, any value accepted) at runtime instead of erroring; and label recovery
looks back only one hop -- it does not chase labels through a chain of dynamic references, nor
through the template-expression form, both of which fall back to label == value.
Dynamic File Generation
matrix: expands a single discovered file into one generated file per resolved combination — the
Cartesian product of one or more axes, using the same axis shape the workflow matrix: step uses.
Axis values share the same answers.-prefix dot-path and template-expression convention as a
dynamic options: source (see "Dynamic and Label/Value Options" above).
spec:
fields:
- name: environments
type: multiselect
options: [dev, staging, production]
files:
- path: environment.yaml
target: "stacks/{{ .matrix.environment }}.yaml"
matrix:
environment: answers.environments
An axis's value is a literal list declared directly in scaffold.yaml (e.g.
region: [us-east-1, us-west-2]), a dot-path into answers.* referencing an already
list-shaped answer, such as a multiselect field, or a Go-template expression (any string
containing {{) that computes the list. --set values for a multiselect field are split on
commas automatically, so --set environments=dev,staging works non-interactively.
Declaring more than one axis expands their full Cartesian product; add when: to prune
combinations that don't apply, using the matrix CEL variable alongside answers:
spec:
files:
- path: deploy.yaml
target: "deploy/{{ .matrix.environment }}/{{ .matrix.region }}.yaml"
matrix:
environment: [dev, staging, production]
region: [us-east-1, us-west-2]
when: "matrix.region in answers.environments[matrix.environment].regions"
target: is required whenever matrix: is set. The resolved combination is available in
target: and the file's own content — not just the output path — as .matrix.<axis>, matching
Go template's leading-dot field access. when: is CEL, not Go template, so it reads the same
value as matrix.<axis> instead, without the leading dot (see the when: example above). Two
files (matrixed or not) rendering to the same output path is a hard error, never a silent
overwrite.
An axis doesn't need to come from a multiselect at all — a plain free-text answer is just a
string, and any Sprig/Gomplate function can split it into a list:
spec:
fields:
- name: environments_csv
type: input
label: Comma-separated list of environments
files:
- path: deploy.yaml
target: "deploy/{{ .matrix.environment }}.yaml"
matrix:
environment: '{{ splitList "," answers.environments_csv }}'
Typing dev,staging,production at the prompt generates the same three files a multiselect
with those three options would — except the values aren't limited to a fixed,
template-author-declared list.
When an axis's values aren't already list-shaped anywhere in answers — e.g. an answer is itself
a map of structured values rather than a flat multiselect — compute the list with
collectKeys, a template function unconditionally available to axis expressions, alongside every
Sprig/Gomplate function. Scaffold templating always has both available and is independent of the
templates.settings.sprig.enabled/templates.settings.gomplate.enabled settings, which only gate
stack manifest templating. collectKeys(m) returns m's top-level keys, sorted; collectKeys(m, "nestedKey") collects nestedKey's own keys from every value in m, flattened and deduplicated:
spec:
files:
- path: deploy.yaml
target: "deploy/{{ .matrix.environment }}/{{ .matrix.region }}.yaml"
matrix:
environment: '{{ collectKeys answers.environments }}'
region: '{{ collectKeys answers.environments "regions" }}'
when: "matrix.region in answers.environments[matrix.environment].regions"
Given an environments answer shaped like this:
environments:
dev:
regions:
us-east-1: {}
production:
regions:
us-east-1: {}
us-west-2: {}
environment resolves to dev and production, and region to every region used by any
environment (us-east-1 and us-west-2) — when: then prunes the Cartesian product down to each
environment's actual regions.
Generation Hooks
Generation hooks run after answers are validated and before or after files are written:
spec:
hooks:
prepare:
events: [before.scaffold.generate]
kind: step
type: shell
with:
command: mkdir -p generated
validate:
events: [after.scaffold.generate]
kind: steps
with:
- type: shell
command: terraform fmt -recursive
- type: shell
command: terraform validate
Scaffold hooks run in stable name order and support only kind: step and kind: steps. A single
step hook uses its envelope type: plus step-specific with: data; a steps hook executes the
ordered with: list. The shared envelope supplies events, when, env, retry, and
on_failure. Use answers in hook CEL and {{ .Answers.<field> }} inside a step template.
Use --skip-hooks to skip all hooks or --skip-hooks=prepare,validate to skip named hooks. The
stack-level hooks reference documents additional stack-only kinds such as scanners,
stores, Git, and CI integrations.
Update and Safety Flags
--defaults- Use defaults and
--setvalues without prompting. --dry-run- Render a preview without generated-file writes.
--force- Permit generation into a non-empty target without update merging.
--update- Apply an optimistic three-way merge using the recorded source/base revision.
--base-refOverride the recorded merge base (used with
--update; defaults toHEAD). Only applies to--update-strategy=tracked—rendered's base comes from the target's own recorded.atmos/scaffold.yaml, not--base-ref. Combining--base-refwith--update-strategy=renderedis rejected; drop--base-refwhen usingrendered.--update-strategy(defaulttracked)Choose where
--update's three-way merge base comes from.trackedreads it from the target's own Git history at--base-ref.renderedinstead re-renders the template at the ref that produced what's currently on disk, using that generation's recorded answers, with no dependency on the target being a Git repository at all. This needs the template itself to define ascaffold.yamlmanifest (so the old ref's fields can be resolved) and the target to already carry a prior generation's.atmos/scaffold.yamlrecord (so the original answers are recoverable) — two separate files, not one.--merge-driver(defaultauto)Choose
auto(YAML-aware for.yaml/.yml, text otherwise) ortextto force every file through the line-oriented text merge driver, preserving formatting (e.g. blank lines) that a YAML-aware re-encode would otherwise collapse.--merge-strategy(defaultmanual)- Choose
manual,ours, ortheirsfor merge conflicts. --skip-hooks- Skip all hooks or a comma-separated set of hook names.
--git/--no-git- Control initial Git setup; generation defaults to no Git initialization.