# cast

The `cast` step type records deterministic terminal demos as asciicast files. It can run nested workflow steps, simulate typed commands, and render additional outputs from the same recording.

```yaml
steps:
  - name: list-stacks
    type: cast
    mode: steps
    command: atmos list stacks --format=tree
    defaults:
      cast:
        rate: 12ms
        width: 120
        height: 36
      simulate:
        mode: typed
        cursor: true
        rate: 35ms
        prompt:
          text: "> "
          style: command
    output:
      cast: website/static/casts/list-stacks.cast
    steps:
      - type: simulate
        text: atmos list stacks --format=tree
      - type: shell
        output: raw
        command: atmos list stacks --format=tree
      - type: toast
        level: success
        content: Cast recorded.
```

## Fields

- **`mode`** — Recording mode. Use `steps` for deterministic nested workflow steps, or `session` for interactive terminal sessions.
- **`command`** — Optional command string stored in the asciicast header.
- **`shell`** — Shell that `mode: session` starts, such as `bash`, `zsh`, or `fish`. The value is a command line and may include arguments (`shell: bash -o vi`); they are passed after the arguments `shell_init` adds, so a clean startup is kept. A command line that manages startup files itself, such as `shell: bash --rcfile demo.rc`, pairs with `shell_init: user` to run as written. If the shell exits before the first action, the cast fails with the shell's own message (for example an unknown option). When unset, a tape's `Set Shell` applies, and then `$SHELL`.
- **`shell_init`** — Controls which startup files the `mode: session` shell loads, so recordings are reproducible and do not pick up personal prompts, aliases, or plugins.
  - `clean` (default) starts the shell without the user's startup files: bash with `--noprofile --norc`, zsh with `-f` (plus `NO_PROMPT_SP`), fish with `--no-config`, and other shells with `ENV=` cleared. `BASH_SILENCE_DEPRECATION_WARNING=1` is always set, which silences the macOS bash banner.
  - `user` loads the shell's normal startup files.
  - Any other value is a path to an rc file that is sourced instead of the user's startup files (bash `--rcfile`, zsh through a temporary `ZDOTDIR`, fish `-C source`). Other shells reject rc-file paths.
  Session blocks inherit the parent cast's value. Precedence, highest first: the step's `shell_init`, the `--shell-init` flag of [`atmos cast record`](/cli/commands/cast/record), `cast.session.shell_init` in [`atmos.yaml`](/cli/configuration/cast) (environment variable `ATMOS_CAST_SESSION_SHELL_INIT`), then `clean`.

  Before the first action, session mode waits for the shell to go quiet (its first prompt), so the first keystroke never lands ahead of the prompt.
- **`output.cast`** — Path for the generated `.cast` file.
- **`output.gif`, `output.mp4`** — Optional animated outputs rendered after the cast recording succeeds. Atmos automatically installs the managed `agg` renderer for GIF and `agg` plus FFmpeg for MP4 on first use. MP4 is produced from a GIF and can retain GIF palette constraints.
- **`output.html`, `output.ascii`, `output.png`, `output.jpg`** — Optional static outputs rendered natively from the recording's final terminal content — an inline-styled HTML fragment, plain text with no ANSI codes, or a terminal screenshot image. No external tools required. `.ascii` artifacts are cheap to commit and diff, and can be consumed by other tooling without an asciicast player.
- **`defaults.cast`** — Recording defaults for the cast step. Supports `rate`, `width`, and `height`. Explicit top-level `rate`, `width`, or `height` fields on the cast step override these defaults.
- **`defaults.simulate`** — Defaults applied to direct child `type: simulate` steps. Supports `mode`, `cursor`, `prompt`, `rate`, `jitter`, `duration`, and `interval`. Explicit fields on the child simulate step override these defaults, including `cursor: false`.
- **`env`** — Environment variables available to nested cast steps.
- **`working_directory`** — Directory inherited by nested steps that do not define their own working directory.
- **`steps`** — Nested workflow steps to execute while the recorder is active. In `steps` mode, `shell`, `simulate`, `toast`, and other normal workflow steps can be recorded.

Use shared YAML includes for repeated recording settings when multiple casts should have the same terminal dimensions, playback rate, simulated prompt, or recording environment.

## Tape interpretation

A cast step can interpret [VHS](https://github.com/charmbracelet/vhs)-dialect tape script text directly, in memory, instead of a hand-authored `steps:` list — useful when you already have a `.tape` file (for example, one written for the real `vhs` binary) and want to point Atmos at it directly.

- **`tape`** — Inline, multi-line VHS-dialect script. Mutually exclusive with `source` and with an explicit `steps:` list.
- **`source`** — Path to an external `.tape` file, interpreted the same way as `tape`. It must be a string; a source map is rejected. Mutually exclusive with `tape` and with an explicit `steps:` list. Any `Source` directive in it (including nested ones) resolves relative to the step's `working_directory` (or the process CWD if unset) — the same base `source` itself resolves against, matching real `vhs`, which resolves every `Source` in a script relative to its own invocation directory, not relative to whichever file a `Source` happens to be read from.

```yaml
steps:
  - name: hero-demo
    type: cast
    mode: session
    source: demo/hero.tape
    output:
      mp4: hero.mp4
```

```yaml
steps:
  - name: quick-check
    type: cast
    mode: steps
    tape: |
      Type "atmos list stacks" Enter
      Type "atmos validate stacks" Enter
    output:
      cast: quick-check.cast
```

### `mode: steps` vs `mode: session`

The existing `mode` field governs how a tape is translated, and it determines whether a failing typed command actually fails the step:

- **`mode: steps`** (exit-code fidelity): each `Type "cmd" Enter` becomes a `type: simulate` child (so it looks typed in the recording) paired with a real `type: shell` child that actually executes `cmd`, with a real, tracked exit code. The shell children do not show step-name labels unless the cast step sets `show.labels` itself. `Sleep`/`Wait` (including `Wait@<duration>`) are dropped with a warning because step execution is already synchronous. `Screenshot` is supported, and becomes a `type: screenshot` child. Any directive with no discrete-step equivalent (a bare keypress, `Hide`/`Show`, or a `Type` with no trailing `Enter`) is a **parse-time error** naming the offending line and telling you to switch to `mode: session`.
- **`mode: session`** (raw keypress/PTY fidelity): `Type`/`Sleep`/`Wait`/keypresses map onto the existing `write`/`key`/`pause`/`wait` session actions. A `Wait` must carry a pattern, such as `Wait /prompt$/`: VHS accepts a bare `Wait` (wait for the terminal to go idle), but session mode has no idle-detection equivalent and rejects it with a parse error that names the line. This keeps `mode: session`'s existing limitation: no per-command exit code, since the whole script drives one continuous PTY session.

A `wait` action never matches the echo of a line the script typed and submitted with Enter; it waits for output from the command.

### Tape text is literal

Tape-derived steps never go through Go templating, so `{{ ... }}` inside a `Type` is typed and run exactly as written. For example, `Type "docker ps --format '{{.Names}}'" Enter` runs that command unchanged. Hand-written steps still render templates as usual.

Double-quoted strings are read verbatim, as VHS does: backslash escapes are not interpreted, so `Type "printf 'a\nb'"` types exactly that text. To embed double quotes, wrap the string in backticks or single quotes.

### Timing modifiers and keys

A `@<duration>` suffix tunes timing for a single directive:

- `Type@50ms "..."` sets the per-character typing speed for that `Type`.
- A keypress such as `Backspace@100ms 5` sets the delay between repeats.
- `Wait@10s /re/`, `Wait+Screen@10s /re/`, and `Wait+Line@10s /re/` set a timeout for that one `Wait`, overriding `Set WaitTimeout`.

Supported keys, each with an optional repeat count: `Enter`, `Space`, `Tab`, `Up`, `Down`, `Left`, `Right`, `Backspace`, `Escape`, `PageUp`, `PageDown`, `Delete`, `Insert`, `Home`, `End`, `Ctrl+<letter>`, `Ctrl+Alt+<letter>`, `Alt+<char>`, `Shift+Tab`, and `Shift+<letter>`. Any other combination is a parse error that lists the supported forms.

### Setup commands

In `mode: steps`, lines that only set up the environment update tracked state instead of running as a subprocess whose changes would be lost. A line qualifies when it is exactly one simple `cd`, `export`, `unset`, or `clear` command, parsed with a real shell parser. In `mode: steps`, a tracked `unset` is applied before each subsequent command so inherited variables are absent, not merely empty. Values expand the way a shell expands them: `export PATH=$PATH:/x`, quoted values with spaces, `cd ~`, and a bare `cd` all work.

Compound lines (`cd sub && make`), command substitution (`cd "$(git rev-parse --show-toplevel)"`), and `cd -` run as ordinary commands. In `mode: steps`, Atmos logs a warning that their working-directory and environment changes do not carry to later commands; split the line or switch to `mode: session`.

In `mode: session`, hidden setup commands are lifted only from a leading `Hide` block, meaning one that appears before anything visible. `unset` always replays in the live shell so it can remove variables inherited from the process or enclosing workflow; subsequent setup in that block also replays in order. Later `Hide` blocks replay literally and in order, so the real shell applies them at the right time.

### `Set`, `Output`, `Require`, `Source`

| VHS directive | Atmos target | Notes |
|---|---|---|
| `Set Shell`, `Set Width`, `Set Height`, `Set TypingSpeed`, `Set WaitTimeout` | `shell`, `width`/`height`, `write_rate`, default `Wait` timeout | An explicit field on the step always wins over a tape-implied value. `Set Width`/`Set Height` are VHS pixels, converted to terminal columns and rows as described below. |
| `Set FontSize`, `Set Padding`, `Set LineHeight`, `Set LetterSpacing` | feed the `Width`/`Height` conversion only | They do not change rendering fonts. They apply regardless of where they appear relative to `Set Width`/`Set Height`. |
| `Set FontFamily`/`Theme`/`Margin`/`BorderRadius`/`Framerate`/`CursorBlink`/`WaitPattern`/... | _(ignored)_ | Cosmetic VHS options with no Atmos rendering analog. |
| `Output <path>` | `output.*` | One per supported extension (`.cast`, `.gif`, `.mp4`, `.html`, `.ascii`, `.png`, `.jpg`/`.jpeg`). An unsupported extension (such as `.webm` or `.txt`) is an error that names the extension and lists the supported ones. Tape-selected rendered outputs overwrite existing files, like VHS; hand-written rendered output paths retain their no-overwrite protection, including when mixed with tape-selected outputs. |
| `Require <program>` | a synthesized `type: require` child | Runs the same tool-presence check as a hand-authored `type: require` step, before the cast body executes. |
| `Source <file>` | inlined directives | Resolved relative to the step's `working_directory` (or the process CWD) — the same fixed base used for `source` itself and for every `Source`, no matter how deeply nested. |
| `Hide` / `Show` | lifted into `env:`/`working_directory:`, or literal replay | In a leading `Hide` block, commands that are single simple `export`/`cd`/`clear` lines are lifted into the step's own config (no PTY replay needed); `unset`, anything after a replayed command, and every later `Hide` block, replays literally while muted. `mode: steps` has no PTY at all, so `Hide`/`Show` are always an error there. |
| `Copy`, `Paste`, `Env` | _(unsupported)_ | Always a parse error in both modes — rewrite the tape (inline `Env` as a shell `export`, replace `Copy`/`Paste` with a direct `Type`) before using `tape:`/`source:`. |

### `Set Width` and `Set Height`

In a tape, `Set Width` and `Set Height` are VHS pixels. Atmos converts them to terminal columns and rows using the tape's font metrics:

```text
columns = floor((Width - 2 * Padding) / (FontSize * 0.6 + LetterSpacing))
rows    = floor((Height - 2 * Padding) / (FontSize * LineHeight))
```

The VHS defaults apply when the tape does not set them: `FontSize` 22, `Padding` 60, `LineHeight` 1.0, `LetterSpacing` 0. VHS's default 1200x600 therefore becomes 81 columns by 21 rows. A result below 20 columns or 5 rows is clamped to that minimum with a warning, which usually means the value was written as cells rather than pixels. To size a recording in cells, set the step's `width` and `height` fields, which always win over the tape.

### `atmos cast record`

For a one-off tape with no workflow YAML at all, use the `record` subcommand directly:

```shell
atmos cast record demo/hero.tape --output=hero.mp4
```

`record` builds an ephemeral `type: cast` step from the tape and runs it through the same interpreter — see [`atmos cast record`](/cli/commands/cast/record) for the full flag reference. For the complete supported/ignored/error directive matrix and worked migration examples, see the `atmos-vhs` skill.

## Wait semantics in session mode

A `wait` action in `mode: session` matches only output produced after the most recent input action, which is a `write` or a `key`. The echo of the command just typed and output from earlier commands cannot satisfy it, so `wait` with a prompt pattern reliably means the command just sent has finished. A wait that times out fails the cast with an error such as `timed out waiting for cast output: /re/ not seen within 10s`. This applies to hand-written `wait` actions and to waits translated from a tape.

## Set a segment's playback duration

Use a `type: playback` group inside a cast to fit its nested steps into a fixed
recorded duration. Grouping the Enter key with the completion wait includes output
from the moment the command starts:

```yaml
- type: write
  text: atmos terraform deploy --stack dev --all
- type: playback
  duration: 4s
  steps:
    - type: key
      key: enter
    - type: wait
      regex: '(?s)Apply .*completed.*> '
      timeout: 90s
- type: pause
  duration: 3s
```

Place these actions inside `mode: session` or a nested `type: session` block.
The group's clock starts before its first child and stops after its last child
finishes. A deployment that takes 24 seconds therefore plays in 4 seconds, while
the preceding typing and following 3-second reading pause keep their normal timing.
All captured events, including terminal redraws, retain their content and order;
the recorder scales their timestamps proportionally. Elapsed times printed by the
command still show its actual execution time.

The duration must be positive. It is an exact playback duration, so a shorter
recording is stretched to fit too. Execution, wait timeouts, and cancellation still
use real time. Output remains live while recording; only the active group's events
are buffered for timing adjustment. Silent groups retain their requested duration.
Failed recordings are discarded as usual.

Playback groups also work in `mode: steps`, wrapping ordinary steps such as `shell`
or `atmos`. They require an enclosing cast and cannot nest or overlap. Keep typing
and reading pauses outside a group when they should retain their original timing.
