How-to
Adding a Custom Scripted Button to REAPER's UI
A developer's walkthrough of exactly how a .lua file becomes a clickable toolbar button in REAPER — which config files get written, what the command ID really is, and the one API call you'd add to make the button light up.
I am using Reaper on Ubuntu Studio as a mixdown DAW, accepting a stereo input from my Presonus StudioLive mixing board. I work with other musicians and frequently upload demo mixes of sessions for review. The process of mixing down demos for each song, normalizing the file and converting to mp3 before uploading is tedious and repetitive, making it a great candidate for automation.
Scripting the process with Lua and triggering it from a custom UI button greatly speeds things up, and also produces finished mp3 files with standardized settings, so they play better together.
I did this on REAPER 7.77 on Linux, but the files and formats are identical on every platform — only the folder differs. The fastest way to find them is Options → "Show REAPER resource path in explorer/finder." On Linux that's ~/.config/REAPER/, on Windows %APPDATA%\REAPER\, on macOS ~/Library/Application Support/REAPER/.
The two registrations
The mental model to internalize first: an action is not a button. They are two separate registrations, in two separate files. An action is an addressable thing REAPER can run — it lives in reaper-kb.ini. A toolbar button is a visual reference to an action — it lives in reaper-menu.ini [1]. A script can be an action with no button at all (bind a key instead), and the same action can sit on several toolbars. Everything below is just filling in those two registrations.
Notably, the script file itself needs nothing — no manifest, no header comment, no registration call. Registration is a side effect that REAPER records externally.
Turning a script into an action
Menu path: Actions → Show action list… → New action… → Load ReaScript…, then pick the file.
That single click appends one line to reaper-kb.ini:
SCR 4 0 RSe52e9c07eba6532c01cef8a3a41d42ebfcc3b979 "Custom: mixdown-publish.lua" mixdown-publish.lua
Field by field [1]: SCR is the record type (a scripted action). 4 is a flags/type field — it packs language and option bits; treat it as opaque and REAPER-managed. 0 is the section ID, and 0 is the Main section. The 40-hex-char token is the command ID string. The quoted string is the Actions-list name (auto-generated, editable). The trailing path is relative to the Scripts/ folder.
The section ID is worth dwelling on. Here are two rows from the same file for a different script, registered in two contexts:
SCR 4 0 RS1ee9bb229dabffe151848d7efa3c10f748e1a1cf "Custom: lyrics.lua" Cockos/lyrics.lua
SCR 4 32060 RS7d3c_1ee9bb229dabffe151848d7efa3c10f748e1a1cf "Custom: lyrics.lua" Cockos/lyrics.lua
32060 is REAPER's ID for the MIDI Editor section. The recognized sections are Main = 0, MIDI Editor = 32060, MIDI Event List = 32061, MIDI Inline = 32062, and Media Explorer = 32063 [1][2]. A script registered for one section is invisible in the others. Notice the second row's ID gets the section spliced in as a hex prefix: 7d3c is 32060 in hex. If you wanted mixdown-publish.lua reachable from inside the MIDI editor's own toolbar, you'd need a second SCR row with 32060 and a distinct ID.
What the ID actually is
The command ID string is a named command. Everywhere else in REAPER you reference it with a leading underscore — _RSe52e9c07eba6532c01cef8a3a41d42ebfcc3b979 — which distinguishes it from a built-in action's plain integer ID (like 40023). To call it from other code, look it up:
local cmdID = reaper.NamedCommandLookup("_RSe52e9c07eba6532c01cef8a3a41d42ebfcc3b979")
if cmdID ~= 0 then
reaper.Main_OnCommand(cmdID, 0)
end
NamedCommandLookup is the required indirection for anything that isn't a native action; Main_OnCommand's own docs point you to it [3]. ReverseNamedCommandLookup(cmdID) does the inverse (and returns the name without the underscore) [4]. This is exactly how a recorded custom action calls a script as one of its steps, and how you'd fire this button from a MIDI/OSC binding.
Two empirical notes. First, the ID is tied to the script's location, not its contents. I rewrote this script's filename-sanitizing logic and re-saved in place; the button kept working with no re-registration, running old and new behavior through the same ID across the edit. Second, it is not a simple sha1(path). Some community sources describe it as _RS plus a SHA-1 of the relative path, but I couldn't reproduce that against the obvious path variants — so treat it as opaque and location-stable, nothing more [5]. Crucially, don't hardcode the numeric ID: it's per-install and not portable [2]. Always resolve the string form at runtime.
Putting it on a toolbar
Menu path: right-click any toolbar → Customize toolbar… → Add, search by name, OK, save.
That appends to reaper-menu.ini — a different file:
[Main toolbar]
default=7bb33abf03be6cea
item_0=40023 New project...
...
item_16=42618 Razor editing
item_17=_RSe52e9c07eba6532c01cef8a3a41d42ebfcc3b979 Script: mixdown-publish.lua
tbf_7=1
tbf_11=1
tbf_16=1
Each item is item_N=<command id> <label> [1]. Built-ins use their integer; our script uses the same underscore-prefixed named ID. REAPER doesn't care whether the ID behind a button is native, a script, or an extension command — a toolbar item is just an action reference plus a label. Don't skip index numbers; REAPER ignores everything after a gap [1].
One genuine gotcha: the toolbar label is independent of the Actions-list name. The list calls it "Custom: mixdown-publish.lua"; the button defaulted to "Script: mixdown-publish.lua". Two auto-generated strings for one command ID, each editable separately — don't be confused when they drift.
The tbf_* lines are per-item toolbar flags that were already present for unrelated toggle buttons; item 17 has none. default=7bb33abf03be6cea is REAPER's bookkeeping for which stock layout this diverged from. And no icon was assigned, so no icon_17= line exists and the button shows the default. To fix that: same dialog, select the item, Icon…, pick an image (REAPER ships a large set under Data/toolbar_icons/, or point at your own PNG). That writes an icon_17= line [1][6].
Registering from code
AddRemoveReaScript is the API equivalent of "Load ReaScript" — handy in an installer script:
-- add=true, sectionID=0 (Main), commit=true
local cmdID = reaper.AddRemoveReaScript(true, 0, "/full/path/to/script.lua", true)
-- cmdID is the new integer ID, or 0 on failure.
reaper.AddRemoveReaScript(false, 0, "/full/path/to/script.lua", true) -- remove
When bulk-registering, pass commit=false for all but the last call so REAPER rewrites reaper-kb.ini only once [4].
Making it a toggle
mixdown-publish.lua runs and exits, so REAPER draws a flat, fire-once button. A button that lights up while a mode is on has to manage its own state:
local _, _, sectionID, cmdID = reaper.get_action_context()
local state = reaper.GetToggleCommandStateEx(sectionID, cmdID)
state = (state == 1) and 0 or 1
reaper.SetToggleCommandState(sectionID, cmdID, state)
reaper.RefreshToolbar2(sectionID, cmdID)
get_action_context() is how a running script learns its own section and command ID (it also reports MIDI/OSC trigger values) [7]. The docs are explicit that only ReaScripts can have their toggle state set programmatically — a native or extension action can't be lit up this way from outside [3]. A real toggle button also wants an _on icon variant, which REAPER auto-pairs [6]. This script doesn't need any of it, but it's the natural payoff once the ID plumbing above makes sense.
For the rest of the customization ladder, see the broader survey.
Sources
- REAPER Filetype-descriptions (Ultraschall) — reaper-kb.ini and reaper-menu.ini formats, section IDs
- REAPER API functions (Ultraschall) — section IDs, toggle-state values, ID portability warning
- REAPER ReaScript API reference (official) — Main_OnCommand, SetToggleCommandState, RefreshToolbar2
- ReaScript API Documentation (extremraym) — AddRemoveReaScript, ReverseNamedCommandLookup
- Advanced Actions Management (X-Raym) — command IDs, path-determinism
- How to make Custom Toolbar Icons for REAPER (The REAPER Blog)
- REAPER | ReaScript (official) — get_action_context