Instant Connection for Pixel Streaming

— New Feature Automated Setup

Guide

How to Automate REAPER with Claude Code and ReaScript

Automate REAPER with Claude Code and Lua ReaScript on Vagon Cloud Computer. Rename tracks, create regions, and verify audio exports in one workspace.

On this page

An interview project may be ready to hear but not ready to deliver. Tracks still need consistent names, approved excerpts need region boundaries, and each export needs the right content and settings. These repeatable steps are a practical starting point for REAPER automation.

With Claude Code, you can turn explicit instructions into Lua ReaScripts, review the proposed changes, and test them inside REAPER. The aim is not to hand over editing decisions. It is to automate preparation while keeping control of the session and its sound.

REAPER multitrack arrangement and mixer showing audio waveforms, track controls, and level meters.

Source: REAPER

This guide follows one workflow: inspect a saved project, rename specified tracks, create regions from approved time ranges, and prepare checked audio exports. You will also test repeat runs and keep a record of the changes.

Vagon Cloud Computer brings Claude Code, REAPER, plugins, media, and outputs into one Windows workspace, providing a complete environment for developing and reusing the workflow.

Define the Job Before Writing the Script

Choose a task with explicit inputs and an outcome you can verify. “Clean up this session” leaves the assistant to interpret what matters. “Rename these tracks and create three regions at approved boundaries” establishes a clear scope.

For this example, start with a saved interview project containing host, guest, and music tracks. The workflow will:

  • Identify the intended tracks and apply approved names.

  • Create three named regions from supplied start and end times.

  • Record proposed and completed changes.

  • Prepare those regions for export using reviewed render settings.

Media positions, fades, gain, pan, FX, and routing stay unchanged. Track folders also remain untouched: in REAPER, their parent-child structure can affect signal flow, so they are not simply visual containers.

Establish the Time Reference

The example will use seconds from the project’s internal start, with the project start-time offset set to zero. It will check that assumption before creating regions. This avoids confusing displayed timecode with the positions supplied to the script.

Choose the excerpt boundaries yourself and check them against playback. The automation does not assume that Claude Code has listened to the interview or identified the best passages.

Define success in practical terms: the correct tracks have the intended names, regions match the approved ranges, unrelated session settings remain unchanged, and running the script again creates no duplicates. Render success is a separate check, covering both the exported audio and its delivery specifications.

Set Up the REAPER Workspace on Vagon

Keep Claude Code, REAPER, and the project’s media on the same Vagon Cloud Computer. You can develop the scripts alongside the session, inspect their effects in REAPER, and render the approved outputs without transferring each script revision between machines.

Check the available preinstalled apps, then install any additional applications required. Confirm that Claude Code opens and REAPER launches. Record the REAPER version so the scripts can target its supported API.

Transfer a Complete, Working Session

Use Vagon Files to bring across the project and its referenced media. An .rpp file stores project information; it does not automatically contain the recordings.

Organize the workspace like this:




Open the transferred session, resolve missing media, and confirm that required plugins are installed and authorized. Play a short section to check the project loads and produces the expected output.

Project folder containing an Audio subfolder alongside a REAPER RPP project file.

Source: Reapertips

Save a separate working copy in project, leaving the original intact. Keep script files in scripts, inspection and change reports in reports, and exports in revision-specific folders under renders.

Give Claude Code a Clear Working Boundary

Start Claude Code in the reaper-automation folder and provide this instruction:

Create and edit Lua scripts in scripts. Write inspection and change reports to reports. Do not modify source media, rewrite the .rpp file directly, install extensions, or start renders without approval. Apply session changes through scripts loaded in REAPER.

This keeps the development work separate from project execution. With the working copy saved and playback checked, the next step is to establish the API version and inspect the session before making changes.

Claude Code terminal showing the welcome screen, recent activity, and prompt input.

Source: Anthropic

Give Claude Code the API Reference for Your REAPER Version

Use Lua for this workflow. It is embedded in REAPER, so the examples do not require Python, SWS, or another scripting extension.

The important distinction is where the code runs: Claude Code prepares the .lua files, while REAPER supplies the reaper.* functions when you load and execute them inside the application. A standalone Lua interpreter cannot verify their interaction with the session.

Establish a Version Target

The examples in this guide target REAPER 7.83. Check your installed version before using them. If you work with another release, have Claude Code verify the required functions against that installation’s documentation rather than assuming compatibility.

Open Help → ReaScript documentation in REAPER to generate the local API reference. The online ReaScript API is useful too, but explicitly warns that it may not match the latest application version.

For region handling, use the newer region/marker functions documented for the target release rather than building a new example around functions marked “discouraged.”

Give Claude Code a focused instruction:

Write Lua ReaScripts for REAPER 7.83 using its documented native API. Start with a read-only inventory of the active saved project. Record track GUIDs, names, item counts, project time offset, and existing regions. Write a report without changing the session. Check that required functions are available and stop with an explanatory message if they are missing.

That inventory will supply the identifiers and timing information needed for the modification script. It also gives you an early compatibility check before allowing any project edits.

Inspect the Session Before Making Changes

Start with a read-only inventory of the saved working project. It should identify the tracks, list existing regions, and record the project’s start-time offset. You can then choose targets from actual session data rather than guessing from track order.

Save this example as scripts/inspect_session.lua. It uses the newer region/marker functions listed in the REAPER API reference.

local required = {
  "GetNumRegionsOrMarkers",
  "GetRegionOrMarker",
  "GetRegionOrMarkerInfo_Value",
  "GetSetRegionOrMarkerInfo_String",
  "GetProjectTimeOffset"
}
for _, name in ipairs(required) do
  assert(reaper.APIExists(name), "Required API unavailable: " .. name)
end

local project, path = reaper.EnumProjects(-1)
assert(project and path ~= "", "Save the working project first.")

local function clean(value)
  local text = tostring(value):gsub("[\t\r\n]", " ")
  return text
end

local lines = {
  "REAPER\t" .. reaper.GetAppVersion(),
  "PROJECT\t" .. clean(path),
  "TIME_OFFSET_SECONDS\t"
    .. tostring(reaper.GetProjectTimeOffset(project, false)),
  "",
  "TRACK\tGUID\tNAME\tITEMS"
}

for i = 0, reaper.CountTracks(project) - 1 do
  local track = reaper.GetTrack(project, i)
  local _, name = reaper.GetTrackName(track)
  lines[#lines + 1] = string.format(
    "%d\t%s\t%s\t%d",
    i + 1, reaper.GetTrackGUID(track), clean(name),
    reaper.CountTrackMediaItems(track)
  )
end

lines[#lines + 1] = "\nREGION_ID\tNAME\tSTART_SECONDS\tEND_SECONDS"
for i = 0, reaper.GetNumRegionsOrMarkers(project) - 1 do
  local region = reaper.GetRegionOrMarker(project, i, "")
  local function value(key)
    return reaper.GetRegionOrMarkerInfo_Value(project, region, key)
  end
  if value("B_ISREGION") ~= 0 then
    local ok, name = reaper.GetSetRegionOrMarkerInfo_String(
      project, region, "P_NAME", "", false
    )
    assert(ok, "Could not read region name.")
    lines[#lines + 1] = string.format(
      "%d\t%s\t%.9f\t%.9f",
      value("I_NUMBER"), clean(name),
      value("D_STARTPOS"), value("D_ENDPOS")
    )
  end
end

local _, script_path = reaper.get_action_context()
local directory = assert(script_path:match("^(.*[\\/])"))
local report_dir = directory .. "../reports/"
reaper.RecursiveCreateDirectory(report_dir, 0)
local report_path = report_dir
  .. "inspection_

local required = {
  "GetNumRegionsOrMarkers",
  "GetRegionOrMarker",
  "GetRegionOrMarkerInfo_Value",
  "GetSetRegionOrMarkerInfo_String",
  "GetProjectTimeOffset"
}
for _, name in ipairs(required) do
  assert(reaper.APIExists(name), "Required API unavailable: " .. name)
end

local project, path = reaper.EnumProjects(-1)
assert(project and path ~= "", "Save the working project first.")

local function clean(value)
  local text = tostring(value):gsub("[\t\r\n]", " ")
  return text
end

local lines = {
  "REAPER\t" .. reaper.GetAppVersion(),
  "PROJECT\t" .. clean(path),
  "TIME_OFFSET_SECONDS\t"
    .. tostring(reaper.GetProjectTimeOffset(project, false)),
  "",
  "TRACK\tGUID\tNAME\tITEMS"
}

for i = 0, reaper.CountTracks(project) - 1 do
  local track = reaper.GetTrack(project, i)
  local _, name = reaper.GetTrackName(track)
  lines[#lines + 1] = string.format(
    "%d\t%s\t%s\t%d",
    i + 1, reaper.GetTrackGUID(track), clean(name),
    reaper.CountTrackMediaItems(track)
  )
end

lines[#lines + 1] = "\nREGION_ID\tNAME\tSTART_SECONDS\tEND_SECONDS"
for i = 0, reaper.GetNumRegionsOrMarkers(project) - 1 do
  local region = reaper.GetRegionOrMarker(project, i, "")
  local function value(key)
    return reaper.GetRegionOrMarkerInfo_Value(project, region, key)
  end
  if value("B_ISREGION") ~= 0 then
    local ok, name = reaper.GetSetRegionOrMarkerInfo_String(
      project, region, "P_NAME", "", false
    )
    assert(ok, "Could not read region name.")
    lines[#lines + 1] = string.format(
      "%d\t%s\t%.9f\t%.9f",
      value("I_NUMBER"), clean(name),
      value("D_STARTPOS"), value("D_ENDPOS")
    )
  end
end

local _, script_path = reaper.get_action_context()
local directory = assert(script_path:match("^(.*[\\/])"))
local report_dir = directory .. "../reports/"
reaper.RecursiveCreateDirectory(report_dir, 0)
local report_path = report_dir
  .. "inspection_

local required = {
  "GetNumRegionsOrMarkers",
  "GetRegionOrMarker",
  "GetRegionOrMarkerInfo_Value",
  "GetSetRegionOrMarkerInfo_String",
  "GetProjectTimeOffset"
}
for _, name in ipairs(required) do
  assert(reaper.APIExists(name), "Required API unavailable: " .. name)
end

local project, path = reaper.EnumProjects(-1)
assert(project and path ~= "", "Save the working project first.")

local function clean(value)
  local text = tostring(value):gsub("[\t\r\n]", " ")
  return text
end

local lines = {
  "REAPER\t" .. reaper.GetAppVersion(),
  "PROJECT\t" .. clean(path),
  "TIME_OFFSET_SECONDS\t"
    .. tostring(reaper.GetProjectTimeOffset(project, false)),
  "",
  "TRACK\tGUID\tNAME\tITEMS"
}

for i = 0, reaper.CountTracks(project) - 1 do
  local track = reaper.GetTrack(project, i)
  local _, name = reaper.GetTrackName(track)
  lines[#lines + 1] = string.format(
    "%d\t%s\t%s\t%d",
    i + 1, reaper.GetTrackGUID(track), clean(name),
    reaper.CountTrackMediaItems(track)
  )
end

lines[#lines + 1] = "\nREGION_ID\tNAME\tSTART_SECONDS\tEND_SECONDS"
for i = 0, reaper.GetNumRegionsOrMarkers(project) - 1 do
  local region = reaper.GetRegionOrMarker(project, i, "")
  local function value(key)
    return reaper.GetRegionOrMarkerInfo_Value(project, region, key)
  end
  if value("B_ISREGION") ~= 0 then
    local ok, name = reaper.GetSetRegionOrMarkerInfo_String(
      project, region, "P_NAME", "", false
    )
    assert(ok, "Could not read region name.")
    lines[#lines + 1] = string.format(
      "%d\t%s\t%.9f\t%.9f",
      value("I_NUMBER"), clean(name),
      value("D_STARTPOS"), value("D_ENDPOS")
    )
  end
end

local _, script_path = reaper.get_action_context()
local directory = assert(script_path:match("^(.*[\\/])"))
local report_dir = directory .. "../reports/"
reaper.RecursiveCreateDirectory(report_dir, 0)
local report_path = report_dir
  .. "inspection_

Run and Review the Inventory

In REAPER, open Actions → Show action list, choose ReaScript: Load, and select the file. Run it to create a timestamped report in reports.

REAPER Actions window with the New action menu open and Load ReaScript selected.

Source: TwelveTake Studios

Check three things before continuing:

  • The project path identifies the saved working copy.

  • The host, guest, and music tracks have been identified correctly.

  • Existing region names will not conflict with the planned excerpts.

Copy the approved track GUIDs into the modification script’s settings. If the reported time offset is not zero, resolve the timing convention before proceeding. Do not change an established project’s offset merely to satisfy the example.

The inspection does not alter the session. It provides identifiers and timing information, not an assessment of plugin availability or audio quality. Test it in your installed REAPER version before building on its results.

Build a Preview from the Approved Targets

Use the inspection report to select the track GUIDs and confirm the timing convention. Then build a change plan before allowing the script to edit the project.

Save the following as scripts/prepare_delivery.lua. Replace the project path, GUID placeholders, and illustrative region times with your approved values.

local settings = {
  project_path = [[C:\your-project\project\interview_working.rpp]],
  tracks = {
    {guid = "{HOST-GUID}", name = "Host"},
    {guid = "{GUEST-GUID}", name = "Guest"},
    {guid = "{MUSIC-GUID}", name = "Music"}
  },
  regions = {
    {name = "Excerpt_01", start = 10, finish = 40},
    {name = "Excerpt_02", start = 60, finish = 95},
    {name = "Excerpt_03", start = 120, finish = 155}
  }
}

for _, name in ipairs({
  "GetProjectTimeOffset", "GetNumRegionsOrMarkers",
  "GetRegionOrMarker", "GetRegionOrMarkerInfo_Value",
  "GetSetRegionOrMarkerInfo_String"
}) do
  assert(reaper.APIExists(name), "Required API unavailable: " .. name)
end

local project, path = reaper.EnumProjects(-1)
assert(path == settings.project_path, "Open the specified working project.")
assert(
  math.abs(reaper.GetProjectTimeOffset(project, false)) < 0.000000001,
  "This example requires a zero project start-time offset."
)

local tracks, regions, operations, messages = {}, {}, {}, {}
for i = 0, reaper.CountTracks(project) - 1 do
  local track = reaper.GetTrack(project, i)
  tracks[reaper.GetTrackGUID(track)] = track
end

for i = 0, reaper.GetNumRegionsOrMarkers(project) - 1 do
  local region = reaper.GetRegionOrMarker(project, i, "")
  local function value(key)
    return reaper.GetRegionOrMarkerInfo_Value(project, region, key)
  end
  if value("B_ISREGION") ~= 0 then
    local ok, name = reaper.GetSetRegionOrMarkerInfo_String(
      project, region, "P_NAME", "", false
    )
    assert(ok, "Could not read region name.")
    regions[name] = regions[name] or {}
    table.insert(regions[name], {
      start = value("D_STARTPOS"), finish = value("D_ENDPOS")
    })
  end
end

local requested = {}
for _, entry in ipairs(settings.tracks) do
  assert(not requested[entry.guid], "Duplicate target GUID.")
  requested[entry.guid] = true
  local track = assert(tracks[entry.guid], "Track GUID not found.")
  assert(type(entry.name) == "string" and entry.name ~= "", "Invalid name.")
  local _, current = reaper.GetTrackName(track)
  if current ~= entry.name then
    operations[#operations + 1] = {
      kind = "rename", track = track, name = entry.name
    }
    messages[#messages + 1] = "PLAN Rename: " .. current .. " -> " .. entry.name
  else
    messages[#messages + 1] = "SKIP Track already named: " .. entry.name
  end
end

requested = {}
for _, entry in ipairs(settings.regions) do
  assert(type(entry.name) == "string" and entry.name ~= "", "Invalid name.")
  assert(not requested[entry.name], "Duplicate requested region name.")
  requested[entry.name] = true
  assert(
    type(entry.start) == "number" and type(entry.finish) == "number"
    and entry.start >= 0 and entry.finish > entry.start
    and entry.finish < math.huge,
    "Invalid bounds: " .. entry.name
  )

  local matches = regions[entry.name] or {}
  assert(#matches <= 1, "Existing duplicate region: " .. entry.name)
  if #matches == 1 then
    assert(
      math.abs(matches[1].start - entry.start) < 0.000001
      and math.abs(matches[1].finish - entry.finish) < 0.000001,
      "Existing region has conflicting bounds: " .. entry.name
    )
    messages[#messages + 1] = "SKIP Region already matches: " .. entry.name
  else
    operations[#operations + 1] = {
      kind = "region", name = entry.name,
      start = entry.start, finish = entry.finish
    }
    messages[#messages + 1] = string.format(
      "PLAN Create region: %s [%.3f, %.3f]

local settings = {
  project_path = [[C:\your-project\project\interview_working.rpp]],
  tracks = {
    {guid = "{HOST-GUID}", name = "Host"},
    {guid = "{GUEST-GUID}", name = "Guest"},
    {guid = "{MUSIC-GUID}", name = "Music"}
  },
  regions = {
    {name = "Excerpt_01", start = 10, finish = 40},
    {name = "Excerpt_02", start = 60, finish = 95},
    {name = "Excerpt_03", start = 120, finish = 155}
  }
}

for _, name in ipairs({
  "GetProjectTimeOffset", "GetNumRegionsOrMarkers",
  "GetRegionOrMarker", "GetRegionOrMarkerInfo_Value",
  "GetSetRegionOrMarkerInfo_String"
}) do
  assert(reaper.APIExists(name), "Required API unavailable: " .. name)
end

local project, path = reaper.EnumProjects(-1)
assert(path == settings.project_path, "Open the specified working project.")
assert(
  math.abs(reaper.GetProjectTimeOffset(project, false)) < 0.000000001,
  "This example requires a zero project start-time offset."
)

local tracks, regions, operations, messages = {}, {}, {}, {}
for i = 0, reaper.CountTracks(project) - 1 do
  local track = reaper.GetTrack(project, i)
  tracks[reaper.GetTrackGUID(track)] = track
end

for i = 0, reaper.GetNumRegionsOrMarkers(project) - 1 do
  local region = reaper.GetRegionOrMarker(project, i, "")
  local function value(key)
    return reaper.GetRegionOrMarkerInfo_Value(project, region, key)
  end
  if value("B_ISREGION") ~= 0 then
    local ok, name = reaper.GetSetRegionOrMarkerInfo_String(
      project, region, "P_NAME", "", false
    )
    assert(ok, "Could not read region name.")
    regions[name] = regions[name] or {}
    table.insert(regions[name], {
      start = value("D_STARTPOS"), finish = value("D_ENDPOS")
    })
  end
end

local requested = {}
for _, entry in ipairs(settings.tracks) do
  assert(not requested[entry.guid], "Duplicate target GUID.")
  requested[entry.guid] = true
  local track = assert(tracks[entry.guid], "Track GUID not found.")
  assert(type(entry.name) == "string" and entry.name ~= "", "Invalid name.")
  local _, current = reaper.GetTrackName(track)
  if current ~= entry.name then
    operations[#operations + 1] = {
      kind = "rename", track = track, name = entry.name
    }
    messages[#messages + 1] = "PLAN Rename: " .. current .. " -> " .. entry.name
  else
    messages[#messages + 1] = "SKIP Track already named: " .. entry.name
  end
end

requested = {}
for _, entry in ipairs(settings.regions) do
  assert(type(entry.name) == "string" and entry.name ~= "", "Invalid name.")
  assert(not requested[entry.name], "Duplicate requested region name.")
  requested[entry.name] = true
  assert(
    type(entry.start) == "number" and type(entry.finish) == "number"
    and entry.start >= 0 and entry.finish > entry.start
    and entry.finish < math.huge,
    "Invalid bounds: " .. entry.name
  )

  local matches = regions[entry.name] or {}
  assert(#matches <= 1, "Existing duplicate region: " .. entry.name)
  if #matches == 1 then
    assert(
      math.abs(matches[1].start - entry.start) < 0.000001
      and math.abs(matches[1].finish - entry.finish) < 0.000001,
      "Existing region has conflicting bounds: " .. entry.name
    )
    messages[#messages + 1] = "SKIP Region already matches: " .. entry.name
  else
    operations[#operations + 1] = {
      kind = "region", name = entry.name,
      start = entry.start, finish = entry.finish
    }
    messages[#messages + 1] = string.format(
      "PLAN Create region: %s [%.3f, %.3f]

local settings = {
  project_path = [[C:\your-project\project\interview_working.rpp]],
  tracks = {
    {guid = "{HOST-GUID}", name = "Host"},
    {guid = "{GUEST-GUID}", name = "Guest"},
    {guid = "{MUSIC-GUID}", name = "Music"}
  },
  regions = {
    {name = "Excerpt_01", start = 10, finish = 40},
    {name = "Excerpt_02", start = 60, finish = 95},
    {name = "Excerpt_03", start = 120, finish = 155}
  }
}

for _, name in ipairs({
  "GetProjectTimeOffset", "GetNumRegionsOrMarkers",
  "GetRegionOrMarker", "GetRegionOrMarkerInfo_Value",
  "GetSetRegionOrMarkerInfo_String"
}) do
  assert(reaper.APIExists(name), "Required API unavailable: " .. name)
end

local project, path = reaper.EnumProjects(-1)
assert(path == settings.project_path, "Open the specified working project.")
assert(
  math.abs(reaper.GetProjectTimeOffset(project, false)) < 0.000000001,
  "This example requires a zero project start-time offset."
)

local tracks, regions, operations, messages = {}, {}, {}, {}
for i = 0, reaper.CountTracks(project) - 1 do
  local track = reaper.GetTrack(project, i)
  tracks[reaper.GetTrackGUID(track)] = track
end

for i = 0, reaper.GetNumRegionsOrMarkers(project) - 1 do
  local region = reaper.GetRegionOrMarker(project, i, "")
  local function value(key)
    return reaper.GetRegionOrMarkerInfo_Value(project, region, key)
  end
  if value("B_ISREGION") ~= 0 then
    local ok, name = reaper.GetSetRegionOrMarkerInfo_String(
      project, region, "P_NAME", "", false
    )
    assert(ok, "Could not read region name.")
    regions[name] = regions[name] or {}
    table.insert(regions[name], {
      start = value("D_STARTPOS"), finish = value("D_ENDPOS")
    })
  end
end

local requested = {}
for _, entry in ipairs(settings.tracks) do
  assert(not requested[entry.guid], "Duplicate target GUID.")
  requested[entry.guid] = true
  local track = assert(tracks[entry.guid], "Track GUID not found.")
  assert(type(entry.name) == "string" and entry.name ~= "", "Invalid name.")
  local _, current = reaper.GetTrackName(track)
  if current ~= entry.name then
    operations[#operations + 1] = {
      kind = "rename", track = track, name = entry.name
    }
    messages[#messages + 1] = "PLAN Rename: " .. current .. " -> " .. entry.name
  else
    messages[#messages + 1] = "SKIP Track already named: " .. entry.name
  end
end

requested = {}
for _, entry in ipairs(settings.regions) do
  assert(type(entry.name) == "string" and entry.name ~= "", "Invalid name.")
  assert(not requested[entry.name], "Duplicate requested region name.")
  requested[entry.name] = true
  assert(
    type(entry.start) == "number" and type(entry.finish) == "number"
    and entry.start >= 0 and entry.finish > entry.start
    and entry.finish < math.huge,
    "Invalid bounds: " .. entry.name
  )

  local matches = regions[entry.name] or {}
  assert(#matches <= 1, "Existing duplicate region: " .. entry.name)
  if #matches == 1 then
    assert(
      math.abs(matches[1].start - entry.start) < 0.000001
      and math.abs(matches[1].finish - entry.finish) < 0.000001,
      "Existing region has conflicting bounds: " .. entry.name
    )
    messages[#messages + 1] = "SKIP Region already matches: " .. entry.name
  else
    operations[#operations + 1] = {
      kind = "region", name = entry.name,
      start = entry.start, finish = entry.finish
    }
    messages[#messages + 1] = string.format(
      "PLAN Create region: %s [%.3f, %.3f]

Review the Plan Before Adding Execution

Load and run this version through REAPER’s Actions window. It only inspects the session and prints a plan. It does not rename tracks, create regions, save the project, or render audio.

An illustrative result might look like this:

PLAN Rename: Audio 01 -> Host
SKIP Track already named: Guest
PLAN Create region: Excerpt_01 [10.000, 40.000]
PLAN Rename: Audio 01 -> Host
SKIP Track already named: Guest
PLAN Create region: Excerpt_01 [10.000, 40.000]
PLAN Rename: Audio 01 -> Host
SKIP Track already named: Guest
PLAN Create region: Excerpt_01 [10.000, 40.000]

The script stops if a target is missing, the project path differs, or an existing region has conflicting boundaries. It also rejects duplicate requested targets.

Review every planned operation against the session. Matching region names are deliberately treated as controlled identifiers, not permission to replace existing edits. Once the plan is correct, the next step is to add a confirmed execution block and a persistent change report to this same script.

Apply the Plan and Record the Results

Add the following block after the final line of prepare_delivery.lua. It uses the project, operations, and messages variables already created by the preview code.

The completed script will write the plan to reports, ask for confirmation, and apply only the listed changes. Each operation is checked by reading its result back from REAPER.

assert(
  reaper.APIExists("AddRegionOrMarker"),
  "Required API unavailable: AddRegionOrMarker"
)

local _, script_path = reaper.get_action_context()
local directory = assert(script_path:match("^(.*[\\/])"))
local report_dir = directory .. "../reports/"
reaper.RecursiveCreateDirectory(report_dir, 0)
local report_path = report_dir
  .. "delivery_

assert(
  reaper.APIExists("AddRegionOrMarker"),
  "Required API unavailable: AddRegionOrMarker"
)

local _, script_path = reaper.get_action_context()
local directory = assert(script_path:match("^(.*[\\/])"))
local report_dir = directory .. "../reports/"
reaper.RecursiveCreateDirectory(report_dir, 0)
local report_path = report_dir
  .. "delivery_

assert(
  reaper.APIExists("AddRegionOrMarker"),
  "Required API unavailable: AddRegionOrMarker"
)

local _, script_path = reaper.get_action_context()
local directory = assert(script_path:match("^(.*[\\/])"))
local report_dir = directory .. "../reports/"
reaper.RecursiveCreateDirectory(report_dir, 0)
local report_path = report_dir
  .. "delivery_

Test Confirmation, Execution, and Repeat Runs

First, run the completed script and decline the confirmation. The report should contain the plan and cancellation status, while the session remains unchanged.

Run it again, approve the changes, and inspect the track names and region boundaries. The report distinguishes planned operations, skipped targets, and operations verified after application.

REAPER arrangement showing a highlighted time range across audio and MIDI tracks.

Source: Envato Tuts+

Finally, run it once more without changing the settings. It should record “No changes needed.” This checks that the automation does not create duplicate regions during ordinary repeat use.

The undo block groups project edits; it does not make execution transactional. If an error occurs, inspect the session and use Undo as appropriate before retrying. Reports remain on disk, and the script does not automatically save the project.

Test both script sections together in your installed REAPER version before using them on production work. The API checks establish function availability, not complete runtime correctness or audio quality.

Prepare the Approved Regions for Export

The script has prepared the session, but render settings still determine what reaches the delivery folder. Start with one excerpt and verify the result before exporting the batch.

REAPER Render to File dialog showing the master mix source, output destination, audio settings, and rendering controls.

Source: Reapertips

Open View → Region/Marker Manager and select the three approved excerpt regions. Then open File → Render, choose Master mix as the source, and set the bounds to Selected project regions. Confirm that the intended region selection is reflected in the render setup.

REAPER Region/Marker Manager listing named regions with start, end, and length columns.

Source: Envato Tuts+

This keeps unrelated regions out of the batch. If every excerpt needs the same complete mix, there is no need to introduce separate track assignments through the Region Render Matrix.

Set the Delivery Rules Once

Check the following before rendering:

  • Output format: the requested file type, sample rate, bit depth where applicable, and channel count.

  • Destination: a revision-specific folder under renders, such as renders/review_01.

  • Filename: a pattern such as $project_$region, with unique region names.

  • Tail: whether effects should continue beyond each region boundary.

  • Processing: any explicitly required normalization, limiting, fades, or trimming.

REAPER’s rendering tools support filename wildcards, queued renders, and region-based output. Those features remove repetitive setup, but the settings must still match the delivery specification.

REAPER output format controls showing MP3 encoding, quality settings, and primary and secondary format tabs.

Source: Reapertips

Select only the first excerpt for a test render. Listen to its opening and ending, confirm the expected tracks are present, and inspect its technical properties. Once it passes, select the remaining approved regions and render them with the same reviewed settings.

Save a render preset for reusable settings, but recheck the destination and region selection for each session. A preset is a starting configuration, not proof that the current batch will produce the correct files.

REAPER render dialog showing region-based filename wildcards and a preview of the files to be exported.

Source: Envato Tuts+

Verify the Files Before Approving the Batch

Check the exported files against the delivery requirements, not just REAPER’s completion message. A successful render can still contain the wrong tracks, an awkward cut, or an unintended effects tail.

Use one final checklist:

  • Files: the expected count, unique names, and correct destination.

  • Content: the intended speakers, music, and processing.

  • Boundaries: complete words, appropriate fades, and no unwanted material.

  • Technical properties: the requested format, sample rate, channel count, and bit depth where applicable.

  • Levels: the specified loudness and peak limits, if required.

Compare each file’s duration with its region length, allowing for any intentional render tail, padding, or trimming. A difference deserves investigation, but is not automatically an error.

REAPER render status showing waveform clipping indicators, true peak, and LUFS measurements.

Source: Reapertips

Distinguish Playback Problems from Export Problems

Review the audio in your Vagon session using the monitoring setup available to you. If playback stutters or sounds interrupted, check whether the issue also exists in the rendered file before changing the project.

Playing the downloaded original on another device is a useful cross-check when diagnosing a monitoring problem or completing final delivery review. It is not an additional requirement for every intermediate render.

Claude Code can help prepare file-inspection commands or summarize a generated render report. Technical checks can confirm properties such as duration and channel count, but listening remains necessary to assess intelligibility, natural edits, and the intended mix.

If an output fails, correct the relevant cause: region boundary, source selection, plugin state, or export setting. Render a new revision into a separate folder so the approved and rejected outputs remain distinguishable.

Reuse the Workflow Without Reusing the Wrong Assumptions

Keep the tested scripts, inspection reports, working project, and render settings together. Record the REAPER version and any plugin dependencies needed to reproduce the session.

For another project, reuse the script logic but replace its inputs. Run the inspection again, select the correct track GUIDs, supply approved region boundaries, and verify the project path and time offset. Do not carry identifiers from one session into another without checking them.

Save exports in revision-specific folders, and retain a known-good project copy before further changes. Back up important media and deliverables separately from the working environment.

Vagon Cloud Computer keeps the complete setup together: Claude Code for developing the automation, REAPER for execution and review, and the applications, plugins, and files required to finish the work. The next session can begin with a prepared environment and a tested process.

The benefit is repeatability, not unattended decision-making. You define the edit and delivery requirements; the automation handles consistent preparation, records its actions, and leaves you with results you can inspect before approval.

FAQs

Can Claude Code automate REAPER without MCP?

Yes. Claude Code can prepare Lua ReaScripts that you load and run inside REAPER. MCP is optional. This guide uses an explicit script-loading workflow rather than assuming the assistant can control the open session.

Why use Lua instead of Python?

Lua is embedded in REAPER, so it does not require a separate language installation. Python is also supported for ReaScript, but requires additional setup. Lua keeps this example’s dependencies small.

Do I need SWS or ReaPack?

No. These examples use native REAPER functions. Other scripts may require extensions or packages, so check their dependencies before installing or running them.

Which REAPER version do these scripts target?

They target REAPER 7.83 and use its newer region/marker API. Check function availability in your installed version and test the complete scripts on a working copy before production use.

Why does the script reject a nonzero project time offset?

The example uses seconds from the project’s internal start and assumes a zero start-time offset to avoid confusion with displayed timecode. For an established project with another offset, adapt the timing convention rather than changing project settings solely to satisfy the script.

Can the script choose the best interview excerpts?

No. You supply approved boundaries. Selecting excerpts through transcription or audio analysis would require a separate workflow and editorial review.

Does Undo reverse every script action?

No. The undo block groups project edits, but does not remove reports or rendered files. If execution fails, changes may be partial. Inspect the session before deciding how to recover.

Can I reuse the automation in another project?

Yes, after replacing the project path, track GUIDs, and region settings. Inspect the new session first, review the change plan, and confirm that repeat runs do not create duplicates.

Does this workflow need a GPU?

The Lua examples do not use GPU acceleration. Choose resources around the session’s actual demands, including plugin processing, virtual instruments, memory use, and rendering workload.

Are REAPER and plugin licenses included with Vagon?

Application availability does not automatically include license entitlement. Check the requirements for REAPER, Claude Code access, and any third-party plugins used in the session.

Vagon Computer

Run it on a cloud computer

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.

Vagon Computer

Run it on a cloud computer

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.

Written by

Mert Boz

Mert is a philosophy student with a strong interest in economics, motorsports, and basketball. He enjoys thinking about how the world works, both in theory and in real life. In his free time, you can often find him watching a race, following a basketball game, or reading about economic ideas.

More posts by this writer →

Vagon Computer

Run this guide on a cloud computer.

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.

Vagon Computer

Run this guide on a cloud computer.

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.