scenarios/
JSON-defined workflows that drive Songbird into a starting state for repeatable manual testing.-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-injectsproject.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-projectexplicitly. - 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
include:— list of other scenario names (bare or path-qualified). Resolved depth-first, deduplicated by file path. Steps from includes run before the localsteps.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 onechannel.actionkey per step.noteis 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. |
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 samestepstwice, extract them to a primitive. - Param names are camelCase on the wire even though Rust uses
snake_case (
startBeat, notstart_beat;trackId, nottrack_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 MCPlist_sync_commands tool, or
by reading rust/crates/data/songbird-sync/src/channels/*/defs.rs. A few
useful starting points:
project.new— fresh projectproject.open { path }— load a.birdfixturesong.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,songbirdSamplerclip.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: 2barsstep type. Playback starts whentransport.playruns. - Recording. No “record my manual session to a scenario file” toggle.
handoffs/future/2026-05-13-scenario-system.md for the planned
follow-on work.