Atmos Scaffold

SkillFiles & storage

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

Available today. Use it from your connected AI after setup.

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 .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.

TypePrompt widget
input / text / stringFree-form text (huh Input)
selectSingle choice from options:
multiselectMultiple choices from options: (filterable)
confirm / bool / booleanYes/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

NeedSkill
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 contentatmos-templates
Project bootstrap from the built-in template catalogatmos-init
OCI registry URL syntax, auth precedence, full source-type referenceatmos-vendoring
Generated JSON Schema for IDE validationatmos-schemas
when:/CEL syntax referenceatmos-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.

Signals

GitHub stars
1k
Forks
175
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
atmos-scaffold
Source
github.com/cloudposse/atmos