# Steps

Steps are reusable units of work for [workflows](/workflows), [custom commands](/cli/configuration/commands/steps), and [lifecycle hooks](/stacks/hooks#kind-step-run-a-step-type). A step can run a command, call an HTTP endpoint, collect input, or render terminal output.

## Explore Steps

- [**Step types**](/steps/type) — Browse command execution, orchestration, interactive input, output, and terminal controls.
- [**Step configuration**](/steps/configuration) — Configure conditions, retries, environment, inputs, and outputs.
- [**Workflows**](/workflows) — Assemble reusable operational runbooks.
- [**Custom commands**](/cli/configuration/commands/steps) — Add your own commands to the Atmos CLI.
- [**Lifecycle hooks**](/stacks/hooks#kind-step-run-a-step-type) — Run steps around component operations.

## Use Steps in Different Contexts

A workflow contains an ordered `steps` list:

```yaml
workflows:
  deploy:
    steps:
      - name: plan
        type: atmos
        command: terraform plan vpc
      - name: apply
        type: atmos
        command: terraform apply vpc -auto-approve
```

Custom commands also accept structured steps. Set `type` explicitly when moving examples between contexts; plain string steps in custom commands run as shell commands.

```yaml title="atmos.yaml"
commands:
  - name: greet
    description: Print a greeting
    steps:
      - type: shell
        command: echo "Hello from Atmos"
```

Lifecycle hooks wrap a step in a hook envelope. A single `kind: step` hook puts its parameters under `with`; `kind: steps` accepts an ordered list under `with`.

```yaml title="Component configuration"
components:
  terraform:
    vpc:
      hooks:
        notify:
          events: [after.terraform.apply]
          kind: step
          type: log
          with:
            level: info
            content: "VPC applied"
```

The step library is shared, but the calling context controls defaults, available template values, failure handling, and retries. For example, retrying a `kind: steps` hook retries the whole list. Per-step `when`, [`inputs`](/steps/inputs), and [`artifacts`](/steps/artifacts) behave the same in hooks as in workflows and custom commands. Follow the [hook reference](/stacks/hooks#kind-steps-run-ordered-steps) for its envelope and lifecycle behavior; the workflow step conditions below describe workflow execution.

Each array item is a step — the unit of work Atmos runs, whether it executes a command or renders interactive UI. The `type` field selects step behavior. Fields such as `title`, `columns`, `prompt`, `retry`, and `timeout` are parameters on that same step object; they are not separate steps.

```yaml
steps:
  - name: building
    type: spin
    title: Building application
    command: npm run build
    timeout: 60s
```

In this example, `spin` is the value of `steps[].type`. The fields `title`, `command`, and `timeout` configure that `spin` step.

## Common Fields

- **`name`** — Optional step identifier. Atmos generates names like `step1` when omitted. Names are used by `--from-step` and by templates such as `{{ .steps.plan.value }}`.
- **`type`** — Step behavior selector. Type `atmos` is the default for command steps. See [type](/steps/type).
- **`command`** — Command text for command-running step types such as `atmos`, `shell`, `spin`, and `container` run actions.
- **`stack`** — Step-level stack override for `type: atmos` steps.
- **`identity`** — Step-level auth identity. See [identity](/steps/identity).
- **`when`** — Optional condition that decides whether the step runs. Built-in predicate keywords are evaluated first; other non-empty scalar strings are evaluated as CEL.
- **[`continue`](/steps/continue)** — Forgive this step's own failure so later steps still run and the overall workflow status is unaffected — GitHub Actions' `continue-on-error` semantics.
- **[`inputs`](/steps/inputs)** — Declare this step's freshness sources (`sources`) so Atmos can skip the step when nothing has changed since it last ran successfully. Pair with [`artifacts`](/steps/artifacts) (outputs) and [`preconditions`](/steps/preconditions) (required tools) — three separate sibling fields.
- **[`artifacts`](/steps/artifacts)** — Declare this step's expected output files.
- **[`preconditions`](/steps/preconditions)** — Declare tools that must already be on `PATH` for this step to be considered already satisfied.
- **`env`** — Step-level environment variables. See [env](/steps/env).
- **`working_directory`** — Step-level working directory override. See [working\_directory](/steps/working-directory).
- **`retry`** — Retry policy for the step. See [retry](/steps/retry).
- **`output`** — Step-level output mode override. See [output](/steps/output).
- **`show`** — Step-level display settings override. See [show](/steps/show).
- **`outputs`** — Declared outputs derived from the step result. See [outputs](/steps/outputs).
- **`when`** — Condition controlling whether the step runs. Omitted means success-only: the step runs while the workflow status is still `success`. Use `when: always` for cleanup steps that must run after a previous step fails, such as stopping an emulator, restoring a backup file, or removing Terraform state. Inside a script step, [`defer`](/functions/automation/defer) registers cleanup within the script itself.

Type-specific fields are documented with [type](/steps/type).

## Referencing values from other steps

A named step's result is available to later steps as `{{ .steps.<name>.value }}`
(and `{{ .steps.<name>.values }}` for multi-value steps). These templates — along
with `{{ .env.* }}` and `{{ .flags.* }}` and Sprig/Gomplate functions — resolve
in `shell`, `atmos`, and `exec` step **commands** and in a step's **`env:`**
values, the same way they do for [custom command steps](/cli/configuration/commands/steps):

```yaml
workflows:
  deploy:
    steps:
      - name: component
        type: choose
        prompt: "Component"
        options: [vpc, eks]
      - type: atmos
        command: terraform apply {{ .steps.component.value }} -auto-approve
        env:
          COMPONENT: "{{ .steps.component.value }}"
```

## Interactive Steps in CI (non-TTY)

Interactive step types (`choose`, `input`, `confirm`, `filter`, `file`, `write`) prompt for input and need a terminal. When Atmos runs without a TTY — most commonly in CI — a step behaves based on whether it has a `default`:

- **With a `default`**, the step uses the default value without prompting.
- **Without a `default`**, the step fails with `interactive terminal required for step`. This preserves the previous behavior, so an unattended run never proceeds with an unintended value.

This lets the _same_ workflow prompt locally and run unattended in CI. Source the default from the environment with the `!env` function (`!env VAR fallback`):

```yaml
steps:
  - name: account
    type: choose
    prompt: "Account"
    options: [dev, prod]
    default: !env STACK_ACCOUNT dev
  - type: shell
    command: 'echo "Deploying {{ .steps.account.value }}"'
```

Workflow step `default`, `prompt`, `options`, and `placeholder` evaluate the `!env` and `!exec` YAML functions. (Custom commands defined in `atmos.yaml` resolve these during config load.) The captured value — entered interactively or taken from the default — is available to later steps as `{{ .steps.<name>.value }}`.

## Conditional Execution

Use `when` to skip a workflow step unless a condition is true. Bare predicate keywords keep their existing behavior:

```yaml
steps:
  - name: plan
    type: atmos
    command: terraform plan vpc
    when: ci
```

Any other non-empty scalar string is evaluated as a CEL expression:

```yaml
steps:
  - name: prod-ci-check
    type: shell
    command: ./scripts/check-prod.sh
    when: stack == "prod" && ci
```

Use the explicit `!cel` tag when the value is intended to be CEL or could look ambiguous:

```yaml
steps:
  - name: prod-ci-check
    type: shell
    command: ./scripts/check-prod.sh
    when: !cel 'stack == "prod" && ci'
```

Workflow step CEL expressions can use `ci`, `status`, `stack`, `component`, `workflow`, `step`, and `env`. Predicate keywords win over CEL for bare strings, so `when: ci` uses the built-in predicate while `when: !cel 'ci'` evaluates CEL. `status` starts as `success` and becomes `failure` after a previous step fails.

Two more small fact groups are always available:

- `os`, `arch`, `platform` — the equivalent of a dedicated `platforms:` field, letting a step run only on a given OS/architecture: `when: "os == 'darwin'"`, `when: "platform == 'linux/arm64'"`.
- `checksum`, `timestamp`, `preconditions`, `sources`, `artifacts` — freshness facts for a step's [`inputs`](/steps/inputs)/[`artifacts`](/steps/artifacts)/[`preconditions`](/steps/preconditions) (`checksum.changed`, `timestamp.changed`, `preconditions.success`), computed lazily so a step only pays for the specific fact its `when` expression references. `sources`/`artifacts` resolve to structured per-file records (`path`, `mtime`, `checksum`), not bare strings — see [inputs: Advanced](/steps/inputs#advanced-structured-records).

## Cleanup Steps

Atmos continues evaluating structured steps after a failure so failure-aware
cleanup can run:

```yaml
steps:
  - type: shell
    name: start-emulator
    command: atmos emulator up aws -s dev --ephemeral

  - type: shell
    name: record-or-test
    command: atmos terraform apply demo -s dev -auto-approve

  - type: shell
    name: stop-emulator
    when: always
    command: atmos emulator down aws -s dev

  - type: shell
    name: validate
    command: atmos terraform output demo -s dev
```

If `record-or-test` fails, `stop-emulator` still runs because it declares
`when: always`. The `validate` step is skipped because omitted `when` means
success-only. The workflow still exits with the original failure; cleanup does
not make the failed workflow look successful.
