# 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

```shell
atmos scaffold generate [template] [target] [flags]
```

## Examples

```shell
# 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.

```shell
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](/vendor/url-syntax#oci-syntax) for
details.

## Template Configuration

Use the versioned manifest below; the former top-level `prompts:` key is not valid.

```yaml title="scaffold.yaml"
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.

## Computed Fields

A `type: computed` field is never prompted for and can't be set with `--set` — its `value:` is
either a Go-template expression deriving it from other fields' answers (using the same
`answers.*` binding `options:`'s dynamic form uses below), or a literal of any type (string,
number, boolean, list, or map), used as-is with no rendering at all. A string is only treated as
an expression when it actually contains a template action (`{{ ... }}`, or your configured
delimiters); a plain string with none, like `value: hello`, is a literal too:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: regions
      type: multiselect
      options: [us-east-1, us-west-2, eu-west-1]
    - name: primary_region_select
      type: select
      options: answers.regions
      when: "size(answers.regions) > 1"
    - name: primary_region
      type: computed
      value: "{{ ternary answers.primary_region_select (index answers.regions 0) (gt (len answers.regions) 1) }}"
    - name: provider_version_pins
      type: computed
      value:
        aws: "~> 5.0"
        azurerm: "~> 3.0"
        google: "~> 5.0"
```

Without a computed field, every file that needs the "primary region, defaulting to the only
region when there's just one" value has to re-derive it with the same `{{ if .Config.primary_region_select }}...{{ end }}`
snippet. `primary_region` derives it once, and `.Config.primary_region` is then usable anywhere
`.Config` is: file content, `target:`, and `matrix:` axes. `provider_version_pins` is a plain
literal — no template rendering happens for it at all, it's just stored and exposed at
`.Config.provider_version_pins` as-is, useful for a small hand-authored reference table.
`options:` is the one exception —
it's resolved before computed fields have a value, so a `select`/`multiselect` field's
`options:` referencing a computed field is a load-time error rather than a silently
non-functional constraint.

`value:` is required on a `computed` field (and rejected on every other field type); `required:`
and `default:` are both rejected on a `computed` field, since it's always self-supplied. Computed
fields evaluate once, in `spec.fields[]` declaration order, after every regular field's answer is
already final — a computed field can reference any regular field regardless of order, but only an
**earlier-declared** computed field's own result. Referencing itself or a later-declared computed
field fails at scaffold-load time rather than silently rendering as `<no value>`. Because that
evaluation happens after the interactive form completes, a regular field's own `when:` can never
depend on a computed field — only the reverse works.

A computed field's `value:` must be **pure** — deterministic given the same
answers, with no reliance on outside state that can change between calls (the current time, an
environment variable, a remote fetch that isn't guaranteed to return the same content twice).
Interactive generation with no target directory given evaluates every computed field twice: once
to suggest a target directory name, again against the final answers to actually generate files.
An impure expression can compute a different value each time, so the suggested directory and the
generated files can end up reflecting two different results for the same field.

## 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:

```yaml title="scaffold.yaml"
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:

```yaml title="scaffold.yaml"
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:

```yaml title="scaffold.yaml"
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"
```

```yaml title="scaffold.yaml"
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:

```yaml title="scaffold.yaml"
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`.

## Loading External Data with `!include` and Other YAML Functions

The [`!include`](/functions/yaml/include) YAML function, already available in stack manifests, also
works in `scaffold.yaml` -- resolving before schema validation, so it can be used anywhere a literal
YAML value is otherwise accepted: `options:`, a computed field's `value:`, and a `matrix:` axis.

A YQ-filtered `!include` shapes external data into `{label, value}` options directly, so a list
doesn't have to be duplicated across fields that need the same choices:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: license
      type: select
      options: !include "./lib/licenses.yaml '. | to_entries | map({\"label\": .value.full_name, \"value\": .key})'"
```

An unfiltered `!include` on a `type: computed` field's `value:` lands the raw included structure at
`.Config.<name>` instead -- see [Computed Fields](#computed-fields) above -- useful for a small
reference table read as data rather than a list of choices:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: license_lookup
      type: computed
      value: !include ./lib/licenses.yaml
```

`lib/licenses.yaml` here is a normal template file, resolved relative to `scaffold.yaml`'s own
directory. A local file that exists solely to be included this way is automatically excluded from
generation output -- it's never copied into the generated project, the same way `scaffold.yaml`
itself never is. `!include` also accepts everything it does in a stack manifest: a remote `git::`,
`oci://`, or `https://` source works the same way, with no such exclusion needed since a remote
source is never part of the local template directory to begin with.

Included content is not processed transitively: a literal `!include` tag written inside an
included file's own content is never resolved, and stays as inert, unprocessed text. This matches
stack manifests, which share the same underlying `!include` implementation and the same
limitation.

Besides `!include`/[`!include.raw`](/functions/yaml/include.raw), `scaffold.yaml` resolves a fixed
set of other YAML functions that need no stack, component, or backend context --
[`!env`](/functions/yaml/env), [`!random`](/functions/yaml/random), [`!cwd`](/functions/yaml/cwd),
the `!git.*`/[`!repo-root`](/functions/yaml/repo-root) family
([`!git.root`](/functions/yaml/git.root), [`!git.sha`](/functions/yaml/git.sha),
[`!git.ref`](/functions/yaml/git.ref), [`!git.branch`](/functions/yaml/git.branch),
[`!git.repository`](/functions/yaml/git.repository), [`!git.owner`](/functions/yaml/git.owner),
[`!git.name`](/functions/yaml/git.name), [`!git.host`](/functions/yaml/git.host),
[`!git.url`](/functions/yaml/git.url)), and [`!literal`](/functions/yaml/literal) (useful for a
field value that looks like a Go template expression but should be taken as-is). Any other YAML
function -- one that needs real stack, component, or backend context Atmos doesn't have while
loading `scaffold.yaml`, such as `!terraform.state`, `!store`, or `!secret` -- is rejected with a
clear error naming the tag, rather than silently left unresolved.

[`!exec`](/functions/yaml/exec) is deliberately **not** supported, even though it needs no stack
context: a template's `scaffold.yaml` is resolved for every template configured in `atmos.yaml`
just to populate `atmos scaffold list` and the interactive template picker, not only the one a
user actually selects or generates. Allowing shell execution here would let any configured
template -- including a shared or vendored one -- run arbitrary code merely by being listed.

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

```yaml title="scaffold.yaml"
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`:

```yaml title="scaffold.yaml"
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:

```yaml title="scaffold.yaml"
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:

```yaml title="scaffold.yaml"
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:

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

## Glob Paths and Directory-Level Matrix

`spec.files[].path` may be a glob pattern instead of a literal path — `*`, `?`, `[...]`, `**` (any
depth, including zero), and `{a,b}` (brace expansion), matched against every discovered file's
path. This lets one entry gate or multiply an entire directory at once, instead of listing every
file individually. A backslash in the pattern is always treated as a forward-slash directory
separator, regardless of which OS authored or evaluates it — discovered paths are always
forward-slash-normalized. A malformed pattern (an unclosed `[` or `{`) fails `atmos scaffold
validate` and scaffold generation immediately, rather than silently and permanently matching
nothing.

Skip (or gate) an entire directory, recursively, with one `when:`-gated entry:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: "docs/legacy/**"
      when: "answers.include_legacy_docs"
```

When more than one entry's `path:` matches the same discovered file, the **last** matching entry
in declaration order wins — the same precedence convention `.gitignore`/`CODEOWNERS` use. Order
entries broad-to-specific, and place a more specific override _after_ a broad glob it should
shadow:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: "docs/legacy/**"
      when: "answers.include_legacy_docs"
    - path: "docs/legacy/keep-this.md"
      when: "always"
```

Combine a glob `path:` with `matrix:` and `target:` to duplicate an entire directory's files once
per matrix combination, exactly like a single-file matrix entry — every matched file gets the same
`.matrix.<axis>` values a single-file entry would (resolved once per `path:`, not once per matched
file, so every matched file sees the identical combination even if an axis expression uses a
non-deterministic function). Since a glob can match many files, `target:` must differentiate
_which_ matched file an output came from, using two new template variables available everywhere
`.matrix.<axis>` is (`target:` and the file's own content), regardless of whether `path:` is a
glob or `matrix:` is even set:

- **`.file.Path`** — the currently matched file's own discovered path.
- **`.file.RelPath`** — `.file.Path` with the matching entry's glob literal prefix stripped.
  Equal to `.file.Path` when the entry's `path:` has no glob metacharacter.

```yaml title="scaffold.yaml"
spec:
  files:
    - path: "components/**"
      target: "environments/{{ .matrix.env }}/{{ .file.RelPath }}"
      matrix:
        env: [dev, staging, production]
```

A `components/` directory containing `vpc/main.tf` and `eks/main.tf` produces six generated files:
`environments/dev/vpc/main.tf`, `environments/dev/eks/main.tf`, and the same pair under `staging/`
and `production/`. A `target:` that doesn't differentiate matched files (e.g. omitting
`.file.RelPath`) fails immediately, before any file in the run is written — this is checked
deterministically, not just caught after a collision happens mid-run.

`.file.Path`/`.file.RelPath` are available in Go templates only — they aren't exposed to CEL
`when:` alongside `answers`/`matrix`. To exclude one specific file from a glob+matrix entry's own
`when:`, declare a second, more specific entry after the broad one instead, per the precedence
rule above.

## Generation Hooks

Generation hooks run after answers are validated and before or after files are written:

```yaml title="scaffold.yaml"
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.

An unset or bare-relative `working_directory:` on a step defaults to (or resolves under) the
scaffold's target directory -- also available as `{{ .TargetPath }}` -- so `terraform fmt
-recursive` above formats the generated project, not wherever `atmos` happened to be launched
from. Use `working_directory: "."` to opt back into the directory `atmos` was launched from. A
`type: atmos` step is exempt from this default: it keeps running in the directory `atmos` was
launched from when `working_directory:` is unset, since the nested `atmos` invocation must resolve
its own config there, but an explicit `working_directory:` on that step is still honored.

Use `--skip-hooks` to skip all hooks or `--skip-hooks=prepare,validate` to skip named hooks. The
stack-level [hooks reference](/stacks/hooks) documents additional stack-only kinds such as scanners,
stores, Git, and CI integrations.

## Update and Safety Flags

- **`--defaults`**
  Use defaults and 
  `--set`
   values 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-ref`**

  Override the recorded merge base (used with `--update`; defaults to `HEAD`). Only applies to
  `--update-strategy=tracked` — `rendered`'s base comes from the target's own recorded
  `.atmos/scaffold.yaml`, not `--base-ref`. Combining `--base-ref` with
  `--update-strategy=rendered` is rejected; drop `--base-ref` when using `rendered`.
- **`--update-strategy` (default `tracked`)**

  Choose where `--update`'s three-way merge base comes from. `tracked` reads it from the
  target's own Git history at `--base-ref`. `rendered` instead 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 a `scaffold.yaml` manifest (so the old ref's fields can be resolved) and the target to
  already carry a prior generation's `.atmos/scaffold.yaml` record (so the original answers are
  recoverable) — two separate files, not one. Under `rendered`, `--update` also deletes a file
  the template stopped generating between refs, unless you've edited it locally — in that case
  the update fails with an unresolved conflict instead of silently deleting or keeping it.
  `tracked` doesn't support this: there's no safe way to know which files a historical commit
  actually belonged to the template.
- **`--merge-driver` (default `auto`)**

  Choose `auto` (YAML-aware for `.yaml`/`.yml`, text otherwise) or `text` to 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` (default `manual`)**
  Choose 
  `manual`
  , 
  `ours`
  , or 
  `theirs`
   for merge conflicts.
- **`--max-changes` (default `50`)**

  Maximum percentage of changed lines allowed in a `--update` three-way merge before it fails
  instead of applying. `0` disables this check entirely — the merge is never rejected for having
  too many changes (conflicts still write markers for `--merge-strategy=manual` to resolve). Any
  other value is compared against a computed change percentage that has no upper bound, so no
  positive value is a guaranteed bypass the way `0` is — raising it only makes a hard failure
  less likely, not impossible. Configurable via `ATMOS_SCAFFOLD_MAX_CHANGES`.
- **`--recreate-deleted` (default `false`)**

  By default, `--update` leaves in place a file you deleted that the template still generates,
  instead of silently recreating it. Pass `--recreate-deleted` to always recreate it with the
  template's current content. This is independent of `--force`/`--merge-strategy`: `--force`
  already means "on conflict, the template's version wins," so tying recreation to it would make
  manual conflict resolution and recreating a deleted file mutually exclusive.
- **`--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.

## Related Commands

- [`atmos scaffold validate`](/cli/commands/scaffold/validate)
- [`atmos scaffold list`](/cli/commands/scaffold/list)
- [`atmos init`](/cli/commands/init)
- [Lifecycle hooks](/stacks/hooks)
