The plan-as-artifact loop

Do not expose a DAW as hundreds of low-level commands and hope the agent figures it out. Expose a small, typed, reversible, state-aware command surface that lets the agent inspect → plan → validate → preview → commit → undo.

S&S takes that pattern literally. Every mutation an agent triggers begins life as a typed JSON Plan, a self-describing artifact the agent can read, edit, validate against current state, hand to the executor, and undo cleanly if the result misses the mark.

This page is the agent-facing reference. It covers when to use the loop, the six verbs, the Plan and PlanValidationResult shapes, error recovery via suggestedFix, and the relationship to checkpoints.

When to use the loop

SituationRecommended path
Multi-step musical change ("make a beat", "add bass + drums + keys")Plan loop
One-shot read ("what scenes exist?")sas inspect … directly
One-shot mutation already covered by a composite (compose_scene)Either. The creative shortcuts (sas make beat, revise_track, revise_scene) run the loop for you, checkpoint included; compose_scene and the other composites don't checkpoint
Pure transport ("play", "stop")Direct tool call (sas dsl_play). While tracks are being frozen, Play returns FREEZE_IN_PROGRESS (retryable): wait for the freeze, then call it again
Anything you might want to undoPlan loop
State-dependent change ("revise the bass darker")Plan loop; the validator catches missing preconditions
Arranging scenes into a song ("the chorus twice after the verse"), matching its scene levels ("the chorus is louder than the verse"), or choosing which take a section sings ("verse 3 should sing the first verse's words again")The arrangement tools directly (arrangement_normalize_kick_levels for the levels, arrangement_assign_takes for a take; takes otherwise follow their section's position by themselves); they keep their own undo history (arrangement_undo) and never touch scenes or tracks

The non-loop tools are still there. The loop is a higher-leverage path for changes the agent expects to think about: it forces a state check, saves a recovery point automatically, and gives the agent a structured preview of what would happen before the engine touches anything.

The six verbs

                 ┌─────────────────────────────┐
                 │       sas inspect …          │  Read current state
                 └──────────────┬──────────────┘
                                ▼
                 ┌─────────────────────────────┐
                 │       sas plan <intent>      │  Intent → typed Plan
                 └──────────────┬──────────────┘
                                ▼
                 ┌─────────────────────────────┐
                 │       sas validate <plan>    │  Schema + preconditions
                 └──────────────┬──────────────┘
                          valid │
                                ▼
                 ┌─────────────────────────────┐
                 │       sas apply <plan>       │  Auto-checkpoint + execute
                 └──────────────┬──────────────┘
                                ▼
                 ┌─────────────────────────────┐
                 │       sas preview            │  Hear the result
                 └──────────────┬──────────────┘
                       miss     │     hit
                       ┌────────┴───────┐
                       ▼                ▼
       ┌────────────────────────┐   ┌─────────────────┐
       │  sas history undo      │   │  next iteration │
       │  → back to checkpoint  │   │  (revise / add) │
       └────────────────────────┘   └─────────────────┘

Every verb is read-only or reversible. The only step that writes is apply, and it always saves a checkpoint first.

A worked example

# 1. See what we're working with
sas inspect project --json | jq .

# 2. Ask for a plan
sas plan "make a chill 4-bar lo-fi beat in A minor at 85 BPM" \
  --plan-out /tmp/lofi.plan.json

# 3. Sanity-check the plan against current state
sas validate /tmp/lofi.plan.json
# → { "valid": true, "preview": { "wouldCreate": { "scenes": 1, "tracks": 4 }, ... } }

# 4. Apply with auto-checkpoint
sas apply /tmp/lofi.plan.json --checkpoint pre-lofi
# → "Applied 5/5 steps · Checkpoint: pre-lofi"

# 5. Listen
sas preview --json | jq '.data.changes.audio.url'
# → "file:///.../render-cache/<hash>.wav"

# 6a. Happy with it? Iterate.
sas plan "make the keys jazzier" --plan-out /tmp/jazzier.plan.json
sas apply /tmp/jazzier.plan.json

# 6b. Or roll back to the pre-lofi state
sas history undo pre-lofi

Every line is one sas invocation. No engine state-machine to track, no manual cleanup if something goes wrong.

The Plan shape

Plans are versioned JSON. The current schema is plan_schema_version: 1.

{
  // Stable id; agents key revisions by this.
  "id": "plan-scene_create-1714850000-abc123",

  // Free-text user-facing goal, mirrored from the user's input.
  "intent": "make a chill 4-bar lo-fi beat",

  // Top-level type tag. Determines validator branch + builder template.
  // One of: scene_create | scene_revise | track_revise | transition_create
  //       | mix_balance | render_preview | composite
  "type": "scene_create",

  // What the plan assumes at apply time. Validator checks before execution.
  "preconditions": {
    "project_bound": true,
    "scene_active": false,    // we're creating, not modifying
    "bpm": 85,
    "key": "A minor"
  },

  // Sequential steps. Each step's `outputs` may be referenced by later
  // steps via `${steps.<id>.outputs.<key>}` placeholders.
  "steps": [
    {
      "id": "plan-…0.scene_create",
      "type": "scene_create",
      "label": "Create scene 'lo-fi'",
      "inputs": { "name": "lo-fi" }
    },
    {
      "id": "plan-…1.dsl_track_create",
      "type": "dsl_track_create",
      "label": "Add bass track",
      "inputs": {
        "name": "Bass",
        "role": "bass",
        "sceneId": "${steps.plan-…0.scene_create.outputs.sceneId}"
      }
    }
    // … more steps
  ],

  // How to undo on partial failure. Default is checkpoint_undo.
  // compensate_per_step uses LIFO inverse hooks instead.
  // no_rollback is for pure-read plans.
  "rollback": { "strategy": "checkpoint_undo" },

  "metadata": {
    "created_at": "2026-05-04T15:00:00.000Z",
    "created_by": "cli",        // 'cli' | 'mcp' | 'workflow'
    "tool_origin": "make_beat", // composite that emitted the plan, if any
    "plan_schema_version": 1,
    "notes": "Free-form annotations from the agent"
  }
}

Plan steps reference each other via ${steps.…}

When dsl_track_create needs the sceneId produced by scene_create earlier in the plan, it refers to it by step id:

{
  "id": "plan-…1.dsl_track_create",
  "type": "dsl_track_create",
  "inputs": {
    "name": "Bass",
    "role": "bass",
    "sceneId": "${steps.plan-…0.scene_create.outputs.sceneId}"
  }
}

The executor resolves these at run time, so plans stay deterministic even before the engine has assigned IDs.

The PlanValidationResult shape

sas validate returns:

{
  "valid": false,
  "errors": [
    {
      // JSONPath into the offending Plan.
      "path": "$.preconditions.project_bound",
      // Stable error code for programmatic dispatch.
      "code": "missing_precondition",
      "message": "No project is bound — open or create one first.",
      // Concrete recovery: exactly what to call to unblock.
      "suggestedFix": { "tool": "list_projects", "args": {} }
    }
  ],
  "warnings": [
    {
      "path": "$.preconditions.bpm",
      "code": "tempo_drift",
      "message": "Plan asks for 90 BPM; scene is 85 BPM. Will use scene tempo."
    }
  ],
  "preview": {
    // Counts of net-new objects.
    "wouldCreate": { "scenes": 1, "tracks": 4, "clips": 4 },
    // Names/IDs modified in place.
    "wouldModify": { "tracks": [] },
    // Names/IDs deleted (high-risk).
    "wouldDelete": { "tracks": [] },
    // Aggregate risk: low ≤2 mods, no deletes; medium ≤5 mods OR creates;
    // high any delete OR ≥6 mods.
    "riskLevel": "medium",
    // Convenience flag: agents check before auto-applying.
    "requiresConfirmation": false
  }
}

Errors block apply. Warnings don't, but agents should surface them to the user.

Error and warning codes

CodeMeaningTypical suggestedFix
missing_fieldA required Plan field is absentNone; fix the Plan
unsupported_schema_versionmetadata.plan_schema_version isn't one this app readsRe-create the plan with sas plan
empty_stepsThe Plan has no stepsNone; re-plan
duplicate_step_idTwo steps share the same idRegenerate ids; the convention is ${plan.id}.${idx}.${type}
unknown_step_typeStep type not registered in ToolRegistryNone; the agent should pick a different action
unresolved_reference${steps.…} placeholder references a non-existent stepReorder steps; verify ids
forward_referenceA step refers to the output of a later stepReorder steps
unknown_output_keyA ${steps.<id>.outputs.<key>} names an output that step doesn't produceUse an output the step returns
missing_preconditionA required preconditions.* flag isn't satisfiedTool that creates the missing state (list_projects, scene_activate, …)
scene_not_found / track_not_foundThe plan names a scene or track that doesn't existInspect first, then use a real name or id

Warnings (they don't block apply): tempo_drift (the plan's tempo differs from the scene's) and high_track_count (the plan creates more than 12 tracks).

Error recovery via suggestedFix

The validator's suggestedFix is structured exactly like the Phase 3 Remediation.mcp shape, so an agent can pattern-match instead of re-parsing the error message:

# Extract suggested fixes the agent would call next
sas validate /tmp/plan.json --json | jq '.data.changes.validation.errors[].suggestedFix'

# Apply the first suggested fix automatically:
FIX=$(sas validate /tmp/plan.json --json \
  | jq -c '.data.changes.validation.errors[0].suggestedFix // empty')
if [ -n "$FIX" ]; then
  TOOL=$(echo "$FIX" | jq -r .tool)
  sas run "$TOOL" --json-body "$(echo "$FIX" | jq -c .args)"
  sas validate /tmp/plan.json    # re-check
fi

The pattern is: read errors → apply suggested fixes → re-validate → repeat → apply when valid. The agent never has to guess what state the engine needs.

Checkpoints: the safety net

Every apply call auto-creates a checkpoint named pre-apply-<plan.id> (plan ids already carry a timestamp). Override the name with --checkpoint <name>. Disable with --skip-checkpoint (rare; use when the caller is itself a higher-level reversible flow).

What's captured:

LayerWhat's snapshottedWhy
scenes rows (scoped to project_id)Full rowScene structure + key + BPM
tracks / audio_tracks / sample_tracks rowsFull rowsTrack inventory + role + ordering
MIDI clips per trackNotes payload + serialized clipRestoring a track means restoring its clip
plugin_data rows for the projectFullPlugin settings, samples, audio textures
Engine surface state per trackmute, solo, volume_db, pan, plugin_state base64Live-mix state

What's not captured:

  • Full audio bounces: they're content-addressable and survive in the render cache independently. To preserve a specific render before a checkpoint, run sas preview (or render_to_performance) first.
  • Raw plugin DLL state beyond serialized form. If a plugin doesn't support state serialization, expect drift on restore.

Restoration is a single SQLite transaction + sequence of engine RPCs. Total time target: <2s for typical projects (≤20 tracks). In-flight renders are cancelled.

Checkpoints from the CLI and from apply expire after 24 h; checkpoints made through MCP expire after 1 h, and workflow checkpoints after 6 h. sas history prune evicts expired ones; eviction also runs at app startup.

Where each verb fits

Read with inspect

Before planning, the agent typically inspects state to ground its plan:

sas inspect project --include scenes,tracks,musical_context

Returns scenes, tracks, key/BPM/chord progression, and recent checkpoints. Use this to fill in precondition fields when authoring a plan by hand, or just to ground the agent's reasoning.

Plan with plan

sas plan "make a beat" --plan-out plan.json

The router maps the free-text intent to a PlanType and dispatches to a builder. To override the router's guess:

sas plan "add a sub bass" --type track_revise --plan-out plan.json

Validate with validate

Always validate before apply. Validation is fast (no engine RPCs); it checks the schema, preconditions, step references, and that the scenes and tracks a plan names exist.

Apply with apply

sas apply plan.json                        # auto-checkpoint
sas apply plan.json --checkpoint mine      # named checkpoint
sas apply plan.json --dry-run              # validate-only
sas apply plan.json --skip-checkpoint      # caller handles undo

Idempotent: re-running a partially-failed plan replays from the last non-completed step. Step ids are deterministic so the executor can resume safely.

Hear with preview

sas preview                                # active scene, cache-friendly
sas preview <sceneId> --refresh            # force re-render
sas preview --track-id <trackId>           # bounce one track

Returns audio.url, durationSeconds, cacheHit, and staleness. Per-track preview uses the C++ trackIds filter in SceneRenderer.cpp.

Undo with history undo

sas history list                           # find a checkpoint
sas history undo <name>                    # restore

After undo, the project is byte-identical to the state at checkpoint creation. Render cache entries from after the checkpoint are preserved (content-addressable + immutable), so re-applying the same plan would hit the cache.

Composite tools: same loop, less typing

The creative shortcuts make_beat (sas make beat), revise_track and revise_scene follow the same pattern under the hood: they emit a plan, validate it, auto-checkpoint, apply, and report. The other composites (compose_scene, compose_contract, add_instrument, play_scene, render_to_performance) call the tools directly and don't create a checkpoint; save one first with sas history checkpoint <name> if you may want to go back.

Use composites when the workflow is well-trodden. Drop down to the plan loop when:

  • The agent needs to show the user what it's about to do before doing it.
  • The agent needs to edit a plan mid-flight (e.g., swap one step's inputs).
  • The agent is iterating ("preview, then revise, then preview again").
  • The composite doesn't fit and you'd otherwise be writing 5+ shell lines.

See also

Last Updated:
Contributors: shiehn