Command Runner | Create Studio Commands with Ease and Efficiency

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.

:open_book: Documentation | :shopping_cart: Roblox Marketplace | :package: itch.io

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 Run is wrapped in a single ChangeHistoryService recording. 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 Types module is rewritten on save with an ArgumentResult type matching your schema, so arguments.<key> has IntelliSense.
  • Runtime delivery. Tag a command @client and the plugin relocates it to ReplicatedStorage.CommandRunnerRuntime, ready to for a client runtime (in studio testing).
  • Composable. Call other commands from inside Run via props: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.

Tag Management

Save Commands via Library

Global Library (More global commands to come)

Color Wheel

Settings

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
Instance Picker

Paragraph Input Box

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.