Atmos Scaffold
SkillFiles & storageScaffold 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
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Atmos Scaffold skill
What this skill tells your AI
The instructions your AI receives, as published by cloudposse/atmos in agent-skills/skills/atmos-scaffold/SKILL.md and read by ahel’s review.
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
.tmplextension, or - Adding an
atmos:templatemagic 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, value} objects — value is required, label optional
- label: Development
value: dev
- label: Production
value: prod
- name: default_env
type: select
options: answers.envs # dot-path: only the environments actually picked above
- name: csv_owners
type: input
default: "platform-team,security-team"
- name: primary_owner
type: select
options: '{{ splitList "," answers.csv_owners }}' # Go-template expression
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" # only asked if confirmed above
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 # a list-shaped answer
region: [us-east-1, us-west-2] # a literal list
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-generationsed/shell cleanup. - Keep destructive
post_generatehooks (deleting files, force-pushing, etc.) opt-in and visible inscaffold.yaml, mirroringatmos-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:— usewhen:for new templates; the sentinel trick remains for backward compatibility.
Signals
- GitHub stars
- 1k
- Forks
- 175
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
atmos-scaffold- Source
- github.com/cloudposse/atmos