> ## Documentation Index
> Fetch the complete documentation index at: https://songbird.studiocollective.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# scenarios/

# scenarios/

JSON-defined workflows that drive Songbird into a starting state for
repeatable manual testing.

```bash theme={null}
./utils/build-rs -s lead-synth                 # bare name, recursive search
./utils/build-rs --scenario workflows/lead-synth   # path-qualified
```

When `-s NAME` is passed, the Tauri app boots normally, waits for the
React UI to signal `app.ui_ready`, then runs the scenario by calling sync
engine commands one at a time — the same code path the UI uses. There's no
parallel "scenario API". If a sync engine command exists, you can call it
from a scenario.

After `project.new` and `project.open` steps the runner *also* waits for
the audio engine graph rebuild (spawned on the orchestrator's
`session-rebuild` worker thread) to complete before dispatching the next
step. Without this, a follow-up `plugin.add_to_track` or `transport.play`
could race the rebuilt graph and silently no-op.

For all other steps the inter-step delay defaults to **zero** — the
in-place track-add path now goes through the same `apply_track_install`
helper as the full rebuild, so there's no quietly-incomplete async work
between steps to wait out. If you hit a new async path the architecture
hasn't accounted for, bump `RunOptions::delay_between_steps` (default
`Duration::ZERO`) to a small non-zero value as a backstop.

## Default project

Before the scenario's first step, the runner auto-injects `project.new`
unless the scenario already contains `project.new` or `project.open`.
Net effect:

* A scenario that just says "add a track" gets a clean slate. No need to
  include `primitives/empty-project` explicitly.
* A fixture scenario that calls `project.open: { path: "x.bird" }` opts
  out automatically — the load won't get clobbered.

`primitives/empty-project.json` is still around for the rare case where
you want to be explicit, or want to `project.new` mid-scenario.

## File format

```json theme={null}
{
  "description": "Lead synth + 1-bar loop rolling.",
  "include": ["primitives/empty-project", "primitives/add-midi-track"],
  "steps": [
    { "plugin.add_to_track": { "trackId": "${lead.trackId}", "pluginId": "songbird4OSC", "slotType": "instrument" } },
    { "transport.set_looping": { "enabled": true } },
    { "transport.play": {} }
  ]
}
```

Three pieces of syntax — that's the entire DSL:

* **`include:`** — list of other scenario names (bare or path-qualified).
  Resolved depth-first, deduplicated by file path. Steps from includes run
  *before* the local `steps`.
* **`as:`** + **`${name.field}`** — save a step's return value under a name,
  then reference fields of that value in later steps. Only whole-string refs
  are supported (`"${lead.trackId}"`, not `"track-${lead.trackId}"`).
* **`steps:`** — ordered list of `{ "channel.action": { …params }, "as"?: …, "note"?: … }`.
  Exactly one `channel.action` key per step. `note` is for human/LLM
  comments — ignored at runtime.

## Folder layout

| Folder        | Contents                                                                               |
| ------------- | -------------------------------------------------------------------------------------- |
| `primitives/` | Smallest reusable bricks (1–3 steps). Add a track, add a plugin, set a loop.           |
| `workflows/`  | Composed scenarios — usually a stack of `include:`s, sometimes with a few extra steps. |
| `fixtures/`   | "Load this `.bird` file" scenarios (project state is the fixture, not commands).       |
| `tests/`      | Throw-away or test-specific scenarios kept around for repro / e2e.                     |

Names must be globally unique across folders (the recursive lookup
errors on ambiguity). Folders are organizational only.

## Authoring tips

* **Reuse > inline.** A new scenario is usually `include:` two or three
  existing ones plus one or two local steps. If you write the same `steps`
  twice, extract them to a primitive.
* **Param names are camelCase** on the wire even though Rust uses
  snake\_case (`startBeat`, not `start_beat`; `trackId`, not `track_id`).
* **Save names are scenario-global.** Once a step saves `as: "lead"`, every
  later step in the run can use `${lead.…}` — including steps from included
  scenarios that run afterward. Pick distinct names if you compose two
  scenarios that both want to save "track".
* **No conditionals, no loops.** Compose with `include:` instead. If a
  scenario needs branching, it's actually two scenarios.

## Discovering commands

Every sync engine command is callable from a scenario. The full list with
param schemas is introspectable via the MCP `list_sync_commands` tool, or
by reading `rust/crates/data/songbird-sync/src/channels/*/defs.rs`. A few
useful starting points:

* `project.new` — fresh project
* `project.open { path }` — load a `.bird` fixture
* `song.add_midi_track { name? }` → returns `{ trackId, trackIndex, name, type }`
* `song.add_audio_track { name? }`
* `plugin.add_to_track { trackId, pluginId, slotType? }` — built-ins:
  `songbird4OSC`, `songbirdSTK`, `songbird303`, `songbirdWavetable`,
  `songbirdSampler`
* `clip.add_midi_note { trackId, note, velocity, startBeat, durationBeats }`
* `transport.set_loop_range { startBar, endBar }` *(bars, not beats)*
* `transport.set_looping { enabled }`
* `transport.play`, `transport.stop`, `transport.pause`

## What scenarios can NOT do (yet)

* **Assertions.** Today the runner just dispatches steps and logs the
  result. "Did the workflow actually work?" is human-driven for now.
* **Timing / waits.** No `wait: 2bars` step type. Playback starts when
  `transport.play` runs.
* **Recording.** No "record my manual session to a scenario file" toggle.

See `handoffs/backlog/engine/2026-05-13-scenario-system.md` for the planned
follow-on work.
