scaffold.yaml Schema Reference
The full AtmosScaffoldConfig manifest schema (pkg/project/config.ScaffoldSpec,
JSON Schema generated at pkg/datafetcher/schema/scaffold/scaffold-config/1.0.json).
Envelope
apiVersion: atmos/v1kind: AtmosScaffoldConfigmetadata:name: my-template # requireddescription: ...author: ...version: ...spec:source: ... # provenance; written to project records, ignored in templatesbaseRef: ... # 3-way-merge base ref; written to project recordsdelimiters: ["[[", "]]"] # optional: override the default {{ }} Go template delimitersfields: [...] # the questionnairevalues: {...} # preset/default answer values keyed by field namefiles: [...] # optional conditional-generation overlayhooks: {...} # optional pre/post-generate hooks
spec.fields[] — questionnaire
- name: component_name # required; used as the template variable (.Config.component_name)type: input # input|text|string|select|multiselect|confirm|bool|booleanlabel: Component name # short prompt label (falls back to name if omitted)description: ... # longer help text shown with the promptrequired: truedefault: my-defaultoptions: [a, b, c] # select/multiselect only; see below for {label,value}/dynamic formsplaceholder: e.g. vpc # input fields onlyvalidation:pattern: '^[a-z][a-z0-9-]*$'message: "Must be lowercase alphanumeric with hyphens"when: "answers.some_earlier_field == true" # optional; gates whether this field is shown
Field name uniqueness is enforced — a duplicate name fails to load
(ErrDuplicateScaffoldFieldName) rather than silently dropping an answer.
options: — static, label/value, or dynamic
options: (select/multiselect only) accepts four shapes:
- A plain string list — label and value are the same:
options: [dev, staging, prod]
- A list of
{label, value}objects —valueis required (a non-empty string);labelis optional and defaults tovaluewhen omitted. Onlyvalueever reachesanswers/templates/when:—labelis presentation-only:options:- label: Developmentvalue: dev- label: Productionvalue: prod - A dot-path string,
answers.<name>— resolves against an earlier field's answer, aspec.valuespreset, or a--set-supplied value never declared as a field at all. Must resolve to a list-shaped value (amultiselectanswer, or a structured[]string/[]anyof strings). Mirrorsspec.files[].matrixaxis dot-path resolution exactly — sameanswers.prefix convention, same underlying resolver:options: answers.envs - A Go-template expression (recognized by containing the scaffold's configured left delimiter,
{{by default) — computes the list from nested/structured answer data:options: '{{ splitList "," answers.csv_field }}'
Both dynamic forms (dot-path and template expression) resolve correctly once the referenced
earlier field has been answered — interactively (fields prompt one at a time, so a later field is
only ever shown after the ones before it) or headlessly against --set/--defaults-supplied
values. Unlike
an earlier, now-removed load-time check, a dot-path's root name is not validated against field
declaration order at load time — load time can't distinguish a genuine forward/self-reference
mistake from a legitimate spec.values preset or --set-supplied value that was never declared
as a field at all. A forward reference simply resolves to no options at that point (disabling the
membership check for that field, not erroring); a self-reference is tautologically valid (the
field's own final answer is what gets checked against itself). See
validateFieldOptionsSource/resolveFieldOptionsFromAnswers in
pkg/project/config/validation.go.
Label recovery: when a direct single-segment dot-path (answers.<name>, not a deeper path
like answers.nested.envs, and not a template expression) sources from a field whose own
options: used {label, value} pairs, those labels are recovered for the filtered subset of
values present in the referenced answer — a value present in the answer but absent from the
source field's own options list falls back to label == value rather than erroring or being
dropped. Label recovery does not propagate through a chained dynamic reference (a dot-path field
sourcing from another dot-path field) or through the template-expression form — both always yield
label == value.
when: is evaluated by building one huh.Group per field (huh's WithHideFunc)
against a snapshot of every other field's current answer at render time — so a
when: can only meaningfully reference fields declared earlier in the fields:
list. In non-interactive mode (--defaults/no TTY), the same when: check gates
whether a hidden required field is treated as "missing" (MissingRequiredValues), so
--set doesn't need a value for a conditionally-hidden field.
spec.files[] — conditional generation overlay
- path: stacks/deploy/dev.yaml # required; matched against the file's discovered pathwhen: "'dev' in answers.environments"
Files not listed in spec.files: always generate (subject to the pre-existing
path-templating sentinel-skip behavior). This overlay does not declare which files
exist — the template's file tree does that; it only gates whether an already-discovered
file gets written.
spec.files[].matrix — dynamic file generation
- path: templates/deploy.yamltarget: "deploy/{{ .matrix.environment }}/{{ .matrix.region }}.yaml" # required when matrix is setmatrix:environment: answers.environments # dot-path into an already list-shaped answerregion: [us-east-1, us-west-2] # literal listwhen: "matrix.region in answers.environments[matrix.environment].regions"
Expands this one file entry into one generated file per resolved combination of every
axis's values (their full Cartesian product, sorted per axis for deterministic output)
— the same shape the workflow matrix: step uses. target: is required (a single
path: can't serve as more than one output) and rendered once per combination.
Each axis's value is one of:
- a literal list of strings
- a string starting with
answers., a dot-path into an already list-shaped answer - a Go-template expression (any string containing
{{) computing the list from nested/structured or free-text answer data (viacollectKeys,splitList, or any other Sprig/Gomplate function — seeatmos-templates)
when: gets a matrix CEL variable alongside answers, evaluated once per resolved
combination to prune ones that don't apply. The resolved combination is also available
as .matrix.<axis> in target: and the file's own rendered content.
spec.hooks — step-backed hooks
hooks:<hook-name>:events: [before.scaffold.generate, after.scaffold.generate] # default: bothkind: step # or: stepswhen: "..." # default (empty): success-only, mirroring stack-level hookstype: shell # kind: step only — a registered step typewith: # kind: step: one step's params; kind: steps: an ordered listcommand: "git add ."
This reuses pkg/hooks.Hook field-for-field (same struct as stack-level lifecycle
hooks) — see atmos-hooks for the full vocabulary. Only kind: step/kind: steps are
implemented for scaffold hooks; command/store/git kinds are stack/component-only
today (their execution engines assume ExecContext.Info, which scaffold generation
doesn't have).
before.scaffold.generate hooks run once, right after the form is filled in and before
any file is written — a failure aborts before any write happens (no rollback needed).
after.scaffold.generate hooks run once after the file loop completes and the project
record (.atmos/scaffold.yaml) is saved; they still get a chance to run on a failed
generation if their when: explicitly opts in (when: always/when: failure) — the
implicit-success default skips them on failure, matching stack-level hooks.
Why when: can't use the {all:/any:/not:} map form here
pkg/condition.Condition's Go type has no exported fields (by design — it's a small
AST wrapper), so when it's reflected into JSON Schema, invopop emits a schema with
additionalProperties: false alongside the oneOf branches. If object were included
as an allowed branch, a value like when: {all: [ci, success]} would satisfy the
oneOf's object branch but then immediately fail the sibling additionalProperties: false (since all isn't a declared JSON Schema property) — a real, confirmed
contradiction, not a hypothetical one. Scaffold's when: schema therefore only allows
string (predicate keyword or CEL expression) and array (implicit all) — use CEL's
&&/||/! operators for compound logic instead of the map form.
spec.values — preset/default values
values:cloud_provider: aws # overrides a field's own `default:`, still overridden by --set
Precedence (lowest to highest): field default: → spec.values → --set/interactive
answer.