| name | atmos-scaffold |
| description | Scaffold templates: authoring scaffold.yaml, form fields (types, validation, conditional when:), conditional file generation, step-backed hooks (pre/post-generate), update-safe 3-way merge, and atmos scaffold generate/list/validate |
| metadata | {"copyright":"Copyright Cloud Posse, LLC 2026","version":"1.0.0","category":"scaffolding"} |
| references | ["references/scaffold-yaml-schema.md","references/merge-strategy.md"] |
Atmos Scaffold
Use this skill for generating boilerplate (components, configs, directory structures)
from templates via atmos scaffold generate, for authoring new templates
(scaffold.yaml), and for updating previously-generated output from a changed
template via --update.
For bootstrapping a brand-new Atmos project from the built-in template catalog, load
atmos-init instead — it shares this exact engine but has its own command surface and
built-in template list.
Quick Shape
apiVersion: atmos/v1
kind: AtmosScaffoldConfig
metadata:
name: terraform-component
description: Standard Terraform component structure
spec:
fields:
- name: component_name
label: Name of the component
type: input
required: true
atmos scaffold generate terraform-component ./components/terraform/vpc
atmos scaffold list
atmos scaffold validate ./components/terraform/vpc/scaffold.yaml
atmos scaffold ships experimental — behavior may change between releases.
Creating a Template
A template is a directory containing scaffold.yaml (the questionnaire and
optional conditional-generation/hooks config) plus the files to generate.
Files are auto-discovered by walking the template directory — there is no
files: manifest listing every file (spec.files: exists only for the optional
conditional-generation overlay, see below).
Mark a file as a Go template (rendered with the collected answers) either by:
- Naming it with a
.tmpl extension, or
- Adding an
atmos:template magic comment in the first 10 lines, in the
comment style matching the file type: # atmos:template (shell/YAML/Python),
// atmos:template (Go/JS/C++), /* atmos:template */ (C-style block),
<!-- atmos:template --> (HTML/XML/Markdown)
Template sources: embedded (built into the Atmos binary), custom (declared under
scaffold.templates in atmos.yaml), or catalog/remote (git/https/s3/oci — advertised
as stubs, fetched on selection). An OCI source (oci://ghcr.io/org/template:v1) is pulled
via the same pkg/oci client atmos vendor pull reuses (load atmos-vendoring for the
URL syntax and auth precedence). --ref only applies to git sources; OCI/S3/local sources
address a version through the source string itself.
Form Fields
spec.fields is an ordered questionnaire; fields prompt in the order declared.
| Type | Prompt widget |
|---|
input / text / string | Free-form text (huh Input) |
select | Single choice from options: |
multiselect | Multiple choices from options: (filterable) |
confirm / bool / boolean | Yes/no |
Common field keys: name (required, used as the template variable — access via
{{ .Config.<name> }}), label, description, required, default,
options (select/multiselect), placeholder (input), validation.pattern/message
(regex, input fields only).
Dynamic and label/value options: (select/multiselect)
options: accepts a plain string list, a list of {label, value} objects, a dot-path into an
earlier answer, or a Go-template expression:
spec:
fields:
- name: envs
type: multiselect
options:
- label: Development
value: dev
- label: Production
value: prod
- name: default_env
type: select
options: answers.envs
- name: csv_owners
type: input
default: "platform-team,security-team"
- name: primary_owner
type: select
options: '{{ splitList "," answers.csv_owners }}'
The dot-path and template-expression forms 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 — the same
answers.-prefix convention spec.files[].matrix axes use. A dot-path may also point at a spec.values preset or
a --set-supplied value never declared as a field at all; there's no field-declaration-order
check at load time, so a forward/self/typo'd reference degrades gracefully at runtime instead of
failing to load. When a dot-path (not a template expression) sources from a field using
{label, value} pairs, those labels are recovered for the filtered subset of values present in
the answer — only values ever flow into answers/templates, never labels. Full details:
references/scaffold-yaml-schema.md.
Conditional prompts (when:)
A field can declare when: to be shown only if a condition on earlier-declared
fields' answers holds true:
spec:
fields:
- name: enable_monitoring
type: confirm
default: false
- name: alert_email
type: input
when: "answers.enable_monitoring == true"
when: accepts a predicate keyword (always, never, ci, local), a CEL string, or
a list (implicit all). Reference collected answers via the answers map — e.g.
"'dev' in answers.environments" for a multiselect, "answers.x == true" for a
confirm (a bare answers.x is not valid CEL here — it's typed dyn, not bool;
compare explicitly). Use CEL's &&/||/! for compound conditions — the
{all:/any:/not:} map form is not accepted for scaffold when: (see
references/scaffold-yaml-schema.md for why).
A when: can only see fields declared before it in the list.
Full field/validation reference: references/scaffold-yaml-schema.md.
Conditional File Generation
spec.files: is an optional overlay gating specific auto-discovered files, keyed by
their path in the template tree. Files not listed always generate.
spec:
files:
- path: stacks/deploy/dev.yaml
when: "'dev' in answers.environments"
- path: stacks/deploy/staging.yaml
when: "'staging' in answers.environments"
This is static gating over a fixed, enumerable set of files the template author
already created — one file stays one file. For generating a variable number of files
(one per selected value, or one per resolved combination of several axes), see
spec.files[].matrix below.
This is distinct from the older path-templating trick: if a file's path itself is a
Go template that renders to "", "false", "null", or "<no value>", the engine
skips it too (ShouldSkipFile). Prefer declarative when: for new templates — it's
evaluated before any rendering and doesn't require crafting a path template.
Dynamic File Generation (matrix)
spec.files[].matrix expands one discovered file into one generated file per resolved
combination of one or more axes — the same map[axis][]values shape workflow matrix:
steps use. Requires target: (a Go-template string overriding the discovered path:),
since a single path: can't serve as the output for more than one file.
spec:
files:
- path: templates/deploy.yaml
target: "deploy/{{ .matrix.environment }}/{{ .matrix.region }}.yaml"
matrix:
environment: answers.environments
region: [us-east-1, us-west-2]
when: "matrix.region in answers.environments[matrix.environment].regions"
Each axis's value is a literal list, a dot-path into answers.* referencing an
already list-shaped answer, or a Go-template expression computing the list from
nested/structured or free-text answer data (e.g. '{{ collectKeys answers.environments "regions" }}' for a computed axis, or '{{ splitList "," answers.environments_csv }}' for a free-text one — see atmos-templates for collectKeys). The resolved
combination is available as .matrix.<axis> in target:, in when: (pruning
combinations that don't apply), and in the file's own rendered content.
Full schema: references/scaffold-yaml-schema.md.
Hooks
spec.hooks: runs step-backed actions before/after generation, keyed by hook name,
reusing the exact vocabulary stack-level lifecycle hooks use — load atmos-hooks
for the full events/kind/when/type/with reference and atmos-steps for the
step types available in with:. Events are before.scaffold.generate and
after.scaffold.generate; a hook with no events: matches both.
spec:
hooks:
git-add:
events:
- after.scaffold.generate
kind: step
type: shell
when: "size(answers.environments) > 0"
with:
command: "git add ."
Only kind: step/kind: steps are supported today. Stack-level command, scanner,
store, git, and CI kinds require stack/component context that scaffold generation
does not have. kind: step takes one registered step type in type: and its payload
in with:; kind: steps takes an ordered with: list. Answers reach when: through
the answers CEL variable and reach step bodies through {{ .Answers.<field> }}
Go-template syntax.
Security: use --skip-hooks (skip all) or --skip-hooks=name1,name2 (skip
specific hooks) to bypass hooks for a diagnostic or untrusted-template run — the same
flag semantics terraform already has. ATMOS_SCAFFOLD_SKIP_HOOKS is the matching
env var.
Updating Existing Projects (3-Way Merge)
atmos scaffold generate my-template ./target --update
atmos scaffold generate my-template ./target --update --base-ref=v1.2.0
atmos scaffold generate my-template ./target --update --merge-strategy=theirs
atmos scaffold generate my-template ./target --update --dry-run
--update performs a real 3-way merge (base = the git ref the target was generated
from, defaulting to HEAD) instead of failing on a non-empty target directory.
--merge-strategy controls conflict resolution: manual (surface conflicts, default),
ours (keep your version), theirs (use the template's version). Full mechanics
(base storage, conflict markers, the "offer to update instead of failing" interactive
prompt): references/merge-strategy.md.
Commands and Flags
atmos scaffold generate [template] [target]: --force, --update, --base-ref,
--dry-run, --interactive/-i (default true), --defaults (use defaults/--set
without prompting), --set key=value (repeatable), --scaffold-source-override,
--ref (git ref for a template source), --git/--no-git (default false — see
atmos-init for the opposite default), --merge-strategy, --skip-hooks.
atmos scaffold list: templates from scaffold.templates in atmos.yaml (plus
embedded/catalog). atmos scaffold validate [path]: validates scaffold.yaml against
the JSON Schema.
Routing
| Need | Skill |
|---|
Stack hook kinds, lifecycle events, envelope (events/when/retry/on_failure) | atmos-hooks |
Every registered step type and aliases usable in a hook's with: | atmos-steps |
| Go-template/Gomplate/Sprig functions available in file content | atmos-templates |
| Project bootstrap from the built-in template catalog | atmos-init |
| OCI registry URL syntax, auth precedence, full source-type reference | atmos-vendoring |
| Generated JSON Schema for IDE validation | atmos-schemas |
when:/CEL syntax reference | atmos-workflows |
Guardrails
- Prefer declarative
spec.fields[].when:/spec.files[].when: over hand-rolled path
templates or post-generation sed/shell cleanup.
- Keep destructive
post_generate hooks (deleting files, force-pushing, etc.) opt-in
and visible in scaffold.yaml, mirroring atmos-hooks' guidance for stack hooks.
- A
when: can only reference fields/files declared earlier — referencing a
not-yet-declared field silently sees its zero value, not an error; order fields
deliberately.
- Don't confuse the path-sentinel skip trick with declarative
when: — use when:
for new templates; the sentinel trick remains for backward compatibility.