Skip to main content

The props table

Every Run call receives a props table (Studio context plus the helpers CommandRunner exposes) and a typed arguments table (the values from your Arguments schema). Per-argument Run callbacks receive the same props plus the changed value.

Run = function(props, arguments)
-- Selection at execute-time
local first = props.FirstSelected
local all = props.Selected

-- Argument values, keyed by the argument id from your schema
local color = arguments.Color

-- Helpers
props:SetSelection({ first })
props:FocusView({ first })
props:SendNotification("Done")
end

Methods are called with :, fields are read directly. Both styles work because props is a regular table; the : just passes props as self.

Reference

Selected ({Instance})

Snapshot of Selection:Get() at the moment Execute was clicked. Stable for the duration of Run, even if the user clicks somewhere else mid-execution, Selected doesn't change.

FirstSelected (Instance?)

Convenience for Selected[1]. nil when nothing is selected. Use it when your command really wants exactly one input:

local target = props.FirstSelected
if not target then
return props:SendNotification("Select something first")
end

Arguments ({[string]: any})

Dictionary of current argument values, keyed by the argument id from your Arguments schema. Loosely typed — Luau won't type-check .Spacing as a number here. The same data is also passed to Run as a typed second positional parameter; prefer that for typed access:

-- Given Arguments = { Spacing = { Type = "number", Default = 4 } }
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
local n: number = arguments.Spacing -- typed automatically
-- props.Arguments.Spacing also works, but is `any`
end

See auto-generated types for how ArgumentResult is generated.

Plugin (Plugin)

The actual plugin global, passed through. Useful when you need API only the plugin context has:

local mouse = props.Plugin:GetMouse()
props.Plugin:OpenScript(props.FirstSelected)

Configuration (Configuration)

Per-command in-memory key-value store. Survives across executes within the same widget session (lost on plugin reload).

-- First run
props.Configuration:Set("RunCount", 1)

-- Subsequent runs
local n = props.Configuration:Get("RunCount", 0)
props.Configuration:Set("RunCount", n + 1)

Configuration:Get(key, default) returns default (or nil) when the key is absent. See the configuration store.

RunCommand(self, name, args?) ((...) -> any)

Invoke another command by name. Useful for composition.

props:RunCommand("AnchorAll") -- run AnchorAll with its current args
props:RunCommand("Recolor", { Color = "Red" }) -- override args inline

The inner command's AutoRecordChanges is bypassed. The outer recording covers everything. Returns whatever the inner Run returned.

ViewCommand(self, name) ((...) -> nil)

Switch the active sidebar selection to name (clears the search filter, scrolls into view). No-op with a toast if name doesn't exist.

-- A button that takes the user to a related command
NextStep = {
Type = "button",
Default = "Continue →",
Run = function(props)
props:ViewCommand("ApplyPalette")
end,
}

SetLabelText(self, labelName, text) ((...) -> nil)

Set the displayed text of a label (or output-mode paragraph) argument. Works from inside Run or per-arg callbacks.

Run = function(props)
local before = #props.Selected
-- ...do work...
props:SetLabelText("Status", string.format("Touched %d instances", before))
end

GetLabelText(self, labelName) (string)

Read the current text of a label argument.

AppendOutput(self, outputName, text) ((...) -> nil)

Append a single line of text to an output-type argument's panel. Each line gets a [HH:MM:SS] prefix unless the argument's schema sets Timestamp = false.

props:AppendOutput("Log", "Started job")
props:AppendOutput("Log", string.format("Processed %d items", n))

ClearOutput(self, outputName) ((...) -> nil)

Wipe the contents of an output-type panel.

props:ClearOutput("Log")

Output(self, outputName?) (OutputHandle)

Returns a chainable handle for writing to an output panel. Call with no argument to bind to the first output argument the command declares.

local out = props:Output() -- binds to the first output arg
local log = props:Output("Log") -- explicit name

log:print("info line")
:warn("a warning, rendered yellow")
:error("an error, rendered red")
:clear() -- wipes the panel

OutputHandle shape:

MethodEffect
:print(text)Append a plain line.
:warn(text)Append a [WARN] line wrapped in a yellow <font> tag.
:error(text)Append an [ERROR] line wrapped in a red <font> tag.
:clear()Wipe the panel.

Every method returns the handle, so calls chain. The Name field on the handle holds the resolved output name (or nil if you called Output() with no argument and the command has no output args).

SetSelection(self, list) ((...) -> nil)

Wrapper for Selection:Set(list). Pass an array of Instances.

local clones = {}
for _, src in props.Selected do
table.insert(clones, src:Clone())
end
props:SetSelection(clones)

FocusView(self, objects, extraOffset?) ((...) -> nil)

Frame the camera on the given Models or BaseParts. Computes a bounding box of the inputs and positions the camera to fit. Optional extraOffset is a number, extra studs of padding.

local target = props.FirstSelected
if target then
props:FocusView({ target }, 8)
end

SendNotification(self, params) ((...) -> nil)

Show a toast inside the widget. Two argument forms:

-- String shorthand
props:SendNotification("Done, 12 parts updated")

-- Full table
props:SendNotification({
Text = "Replace existing item?",
Timeout = 0, -- 0 = sticky until dismissed
Button1Text = "Replace",
Button1Callback = function() ... end,
Button2Text = "Cancel",
Button2Callback = function() ... end,
})

See notifications for the full param list.

Typing your Run signature

Every new command ships with the typed signature already wired into the starter template:

local Types = require(script.Types)

return {
Arguments = {
Spacing = { Type = "number", Default = 4 },
} :: Types.ArgumentType,
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
-- arguments.Spacing is typed as `number` automatically. Full IntelliSense.
local s: number = arguments.Spacing
end,
} :: Types.ScriptModule

Types.ArgumentResult is regenerated from your schema on every save, so arguments.<key> always matches the current shape. If you drop the arguments: Types.ArgumentResult annotation, arguments falls back to any. See auto-generated types for how the regeneration works and what the sentinel block looks like.