Command Runner
TL;DR: Turn your Command Bar scripts into reusable Studio tools with editable inputs. Write the script once, and Command Runner builds the UI for you.
What it is
Command Runner is a plugin that allows you to easily create, execute and save commands using a UI form.
You declare values you want to tweak as Arguments by either manually scripting them using a a provided scheme, or create it automatically with a right click context menu. The plugin will automatically build the form, including: text fields, switches, color swatches, sliders, instance pickers, dropdowns, and more. Pick a command from the sidebar, fill in the form, hit Execute, and your Run function fires with those values wrapped in a single undo step.
For instance, here is a simple recolor and anchor command in action:
local Types = require(script.Types)
return {
Arguments = {
Color = {
Type = "color",
Default = Color3.new(1, 0, 0),
Description = "Color to apply",
},
Anchor = {
Type = "switch",
Default = true,
Description = "Anchor each part after recoloring",
},
},
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
for _, object in props.Selected do
if object:IsA("BasePart") then
object.Color = arguments.Color
object.Anchored = arguments.Anchor
end
end
end,
}
The plugin reads the schema and gives you a color swatch, an animated toggle, and an Execute button. Inside Run, arguments.Color is typed as Color3 and arguments.Anchor as boolean, with IntelliSense from the generated Types module sitting next to the command.
Let’s create a size argument using the right click context menu:
The argument is automatically created in our script, then after we modify the command:
Updated script with the size slider
local Types = require(script.Types)
return {
Arguments = {
Color = {
Type = "color",
Default = Color3.new(1, 0, 0),
Description = "Color to apply",
},
Anchor = {
Type = "switch",
Default = true,
Description = "Anchor each part after recoloring",
},
size = {
Type = "slider",
Default = 1,
Description = "Resize the parts with a mutliplier",
LayoutOrder = 1,
Min = 0,
Max = 10,
Step = 1,
},
},
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
for _, object in props.Selected do
if object:IsA("BasePart") then
object.Color = arguments.Color
object.Anchored = arguments.Anchor
object.Size *= arguments.size
end
end
end,
}
Why I built it
I used Studio’s Command Bar a lot for quick Lua scripts, but the slow part was always changing values between runs. I’d have to scroll back through the script, find the variable I wanted to tweak, edit it, and run it again.
Undo was another pain point. If I wanted one clean Ctrl+Z, I had to manually wrap the script in ChangeHistoryService.
The scripts worked, but they were usually too small to justify building a full plugin for each one. Command Runner is the middle ground I wanted: a place to save those scripts, give them named inputs, and automatically wrap each run in ChangeHistoryService.
Why should I use this?
Studio’s built-in Command Bar is great for quick scripts, but once you start rerunning the same script with different values, it gets annoying fast.
Maybe you have a script to recolor a selection, snap props to a grid, resize parts, or mass-set an attribute across a folder. The first time, the Command Bar works fine. But once you keep coming back to that script, editing values by hand every time starts to slow you down.
Command Runner is built for those scripts.
Each command becomes a saved entry in the sidebar, and the values you normally edit in code become proper inputs in a form. Instead of scrolling through code to change a color, size, folder path, number, or selected instance, you adjust it in the UI and hit Execute.
You also don’t have to write the argument table by hand. Right-click inside the arguments panel and choose Create Argument to open a popup that builds it for you. Pick the input type, fill out fields like default value, min / max, dropdown options, or picker settings, and Command Runner writes the entry into your command’s Arguments table.
Once you have more than a few commands, you can tag them so they group in the sidebar, save them to your cross-place library, and even chain commands together with props:RunCommand(name, args).
What about inCommand?
inCommand is a great plugin and well worth a look. It tackles a different slice of the same problem: instead of layering UI on top of commands, it replaces the Command Bar itself with a proper multi-line editor, with syntax highlighting, scripts that persist across places and sessions, and one-click client or server execution during play test.
Command Runner sits one layer above that. Each command is a regular ModuleScript, so authoring stays in Studio’s built-in script editor (and round-trips through Rojo or any external editor synced to your place). The plugin doesn’t replace the editor; it promotes the values inside each command to typed inputs on a generated form, so swapping parameters becomes a click instead of a code edit.
What you get
- Auto-generated UI. Declare your inputs as a table of
{ Type, Default, Description }entries and the plugin builds the form. - One undo per command. Every
Runis wrapped in a singleChangeHistoryServicerecording. One Ctrl+Z reverses the entire operation cleanly. - Cross-place library. Save a command to your personal library and load it in any other place you open. A small bundled set of Global commands ships with the plugin so there’s something useful on day one.
- Auto-generated Luau types. Each command’s sibling
Typesmodule is rewritten on save with anArgumentResulttype matching your schema, soarguments.<key>has IntelliSense. - Runtime delivery. Tag a command
@clientand the plugin relocates it toReplicatedStorage.CommandRunnerRuntime, ready to for a client runtime (in studio testing). - Composable. Call other commands from inside
Runviaprops:RunCommand(name, args), letting you chain commands together into larger macros.
Argument types
Fifteen types ship with the plugin. Each one is a line in your schema and a card in the panel.
| Type | What it renders | What Run receives |
|---|---|---|
string |
Single-line text box | string |
paragraph |
Multi-line text box | string |
number |
Text box with up/down stepper arrows; optional Min / Max / Step |
number |
boolean |
X / empty toggle button | boolean |
switch |
Animated pill toggle | boolean |
button |
Clickable action with a per-arg Run callback |
click count, passed to the callback |
label |
Display-only text, writable by Run via SetLabelText |
(no read value) |
output |
Read-only multi-line area for streaming results from Run |
(no read value) |
dropdown |
Picker with a list of options; custom input optional | the chosen option’s Value (any type) |
instance |
Read-only text box plus a “use selection” picker button | Instance? |
color |
Hex / RGB / BrickColor smart input plus a clickable swatch with an HSV picker popup | Color3 |
slider |
Draggable thumb on a horizontal track | number |
propertyPicker |
Captures a named property off the selected Instance | typed by the property |
attributePicker |
Same idea, but reads :GetAttribute(name) |
any |
colorPicker |
Specialized picker for Color3 properties on a selected Instance |
Color3 |
Each command can also declare Validate, OnExecuted, OnError, OnSelected, and OnDeselected hooks if you want pre-flight checks, post-run side effects, or per-command setup and teardown.
Props table
Command Runner provides a few utilities and data passed through the props table that can come in handy. Data fields are accessed with . (e.g. props.Selected); methods are called with : (e.g. props:RunCommand(...)).
| Field | What it does |
|---|---|
Arguments |
Dictionary of current arg values keyed by arg name. Same data the second arguments parameter has, but loosely typed. |
Selected |
Snapshot of Selection:Get() at execute time. |
FirstSelected |
First entry of Selected. Convenience for single-target commands. |
Configuration |
Per-command key/value store. :Get(key, default) / :Set(key, value). |
Plugin |
The plugin global. |
RunCommand(name, args?) |
Invoke another saved command from inside Run. Bypasses auto-record so the outer run is still one undo step. |
ViewCommand(name) |
Switch the plugin’s active sidebar selection to name. |
SetLabelText(name, text) |
Sets the display text of a label argument. |
GetLabelText(name) |
Reads the current display text of a label argument. |
Output(name?) |
Returns a handle to an output argument with :print, :warn, :error, :clear. Pass nil to grab the first output on the command. |
AppendOutput(name, text) |
Append a line to an output argument. |
ClearOutput(name) |
Empties an output argument. |
SetSelection(list) |
Calls Selection:Set(list) and you don’t need to import the Selection game service. |
FocusView(objects, extraOffset?) |
Frames the camera on the given Models / BaseParts. Pairs well with SetSelection. |
SendNotification(params) |
Show a toast inside the widget. params is a table (Text, Timeout?, plus optional buttons) or a bare string. |
Additional Showcase of features:
To see full documentation, and all features visit our documentation.
Custom Output
local Types = require(script.Types)
return {
Description = "Describe what this command does.",
Arguments = {
Log = {
Type = "output",
Default = "",
LayoutOrder = 1,
Timestamp = true,
},
ClearOutput = {
Type = "button",
Default = "",
LayoutOrder = 2,
Run = function(props: Types.Props)
local output = props:Output() -- Empty returns the first output, provide name if you have multiple
output:clear()
end,
},
} :: Types.ArgumentType,
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
local output = props:Output("Log") -- In the future i think i'll allow arguments.Log to return output object
output:print("This is a print")
output:warn("This is a warn")
output:error("This is an error")
end,
} :: Types.ScriptModule
Button Driven Commands
local Types = require(script.Types)
return {
Description = "This is a button driven command, no execute button",
Arguments = {
Counter = {
Type = "label",
Default = "",
LayoutOrder = 1,
},
IncreaseCounter = {
Type = "button",
Default = "Increment",
Description = "Increment the counter",
LayoutOrder = 2,
Run = function(props, clickCount)
local currentCount = tonumber(props:GetLabelText("Counter")) or 0
props:SetLabelText("Counter", tostring(currentCount + 1))
-- Alternatively you can use the configuration utility in props
-- props.Configuration:Set("Counter", currentCount + 1)
end,
},
ResetCounter = {
Type = "button",
Default = "Reset",
Description = "Zero out the counter",
Run = function(props, clickCount)
props:SetLabelText("Counter", "0")
end,
LayoutOrder = 3,
},
} :: Types.ArgumentType,
DisableExecuteButton = true;
} :: Types.ScriptModule
Dropdowns
local Types = require(script.Types)
return {
Description = "Describe what this command does.",
Arguments = {
Material = {
Type = "dropdown",
Default = Enum.Material.Plastic,
Options = {
{ Name = "Plastic", Value = Enum.Material.Plastic },
{ Name = "Wood", Value = Enum.Material.Wood },
{ Name = "Metal", Value = Enum.Material.Metal },
{ Name = "Glass", Value = Enum.Material.Glass },
{ Name = "Concrete",Value = Enum.Material.Concrete },
{ Name = "Brick", Value = Enum.Material.Brick },
{ Name = "Grass", Value = Enum.Material.Grass },
{ Name = "Sand", Value = Enum.Material.Sand },
},
AllowCustom = true,
Description = "Material to apply to the selection",
LayoutOrder = 1,
}
} :: Types.ArgumentType,
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
for _, selected in props.Selected do
if selected:IsA("BasePart") then
selected.Material = arguments.Material
end
end
end,
} :: Types.ScriptModule
Closing
If you pick it up and run into any bugs, or you want a new argument type, or a new feature, reply in this thread. I use this plugin on the daily, so updates for it will be consistent.

















