Streaming output
The output argument type plus props:Output() lets a command stream status messages into its own panel, line by line, with timestamps and severity colors. This is the right shape for:
- Audits that walk a large selection and flag problems.
- Bulk operations that report success/failure per item.
- Anything where "what just happened" is more interesting than "did it succeed."
This recipe walks a complete example end-to-end.
The shape
┌─ Audit Selection ────────────────────────┐
│ ▸ output Log │
│ ┌────────────────────────────────────┐ │
│ │ [10:42:15] Auditing 14 parts... │ │
│ │ [10:42:15] [WARN] Part12: not │ │
│ │ anchored, will fall. │ │
│ │ [10:42:15] [ERROR] Part19: missing │ │
│ │ CollisionGroup. │ │
│ │ [10:42:15] Done. 1 warning, 1 err. │ │
│ └────────────────────────────────────┘ │
│ ┌─ Execute ─────────────────────────┐ │
│ └───────────────────────────────────┘ │
└──────────────────────────────────────────┘
A single read-only panel that the command writes to as it runs.
Schema
return {
Description = "Audit the current selection for common authoring issues.",
AutoRecordChanges = false, -- read-only command — don't pollute undo history
Tags = { "selection", "audit" },
Arguments = {
Log = {
Type = "output",
Default = "",
PlaceholderText = "Press Execute to audit the selection.",
LayoutOrder = 1,
},
} :: Types.ArgumentType,
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
-- next section
end,
} :: Types.ScriptModule
Two things to note:
AutoRecordChanges = false. Auditing doesn't change the place; we don't want to open aChangeHistoryServicerecording for it.PlaceholderText. Shows when the panel is empty. Replaced the moment your first append lands.
The body
Run = function(props: Types.Props, arguments: Types.ArgumentResult)
local log = props:Output("Log")
log:clear()
local parts = {}
for _, inst in props.Selected do
if inst:IsA("BasePart") then
table.insert(parts, inst)
end
end
if #parts == 0 then
log:warn("Select at least one BasePart.")
return
end
log:print(string.format("Auditing %d parts...", #parts))
local warnings, errors = 0, 0
for _, part in parts do
if not part.Anchored and part.Massless == false then
log:warn(string.format("%s: not anchored, will fall.", part.Name))
warnings += 1
end
if part.CollisionGroup == "" or part.CollisionGroup == "Default" then
log:print(string.format("%s: default collision group.", part.Name))
end
if part.Transparency == 1 and part.CanCollide then
log:error(string.format("%s: invisible but collidable.", part.Name))
errors += 1
end
end
log:print(string.format(
"Done. %d warnings, %d errors.",
warnings, errors
))
end,
What's happening, in order:
- Bind the handle once.
props:Output("Log")returns a chainable handle. We hold it in a local so we can call methods likelog:print(...)instead of typingprops:Output("Log"):print(...)on every line. - Clear before writing. Without
log:clear(), every Execute press appends to the previous run's output. Clearing first gives you a fresh panel each time. - Print, warn, error.
:printis a plain line.:warnadds a yellow[WARN]prefix and wraps the line in a colored<font>tag.:errordoes the same with red. - All of the methods chain.
log:print(...):warn(...):error(...)is valid — each method returns the handle. Use it when it reads cleaner; here we wrote one log call per loop iteration so it didn't matter.
What the user sees
Run on a selection of three parts where one is unanchored and one is invisible-but-collidable:
[10:42:15] Auditing 3 parts...
[10:42:15] [WARN] Cube: not anchored, will fall.
[10:42:15] [ERROR] Glass: invisible but collidable.
[10:42:15] Done. 1 warnings, 1 errors.
[WARN] lines render yellow, [ERROR] lines render red. [10:42:15] is the timestamp the plugin prepends automatically.
Variations
Drop the timestamps
For commands where each line is self-contained and timestamps add noise, set Timestamp = false on the schema:
Log = {
Type = "output",
Default = "",
Timestamp = false,
LayoutOrder = 1,
},
Now appended lines render bare:
Auditing 3 parts...
[WARN] Cube: not anchored, will fall.
[ERROR] Glass: invisible but collidable.
Done. 1 warnings, 1 errors.
Append without binding a handle
If you only need a single line and don't want the handle ergonomics, props:AppendOutput writes directly:
props:AppendOutput("Log", "Started job")
-- ... later ...
props:AppendOutput("Log", "Finished job")
AppendOutput always uses the plain (non-severity) style. There's no AppendOutput(name, "WARN", text) form — for severity, use the handle.
Preserve the previous run's log
If you'd rather keep a rolling history across runs, just skip log:clear(). New lines append to the bottom; the user can scroll up to see prior runs. Each run's first log:print(...) line acts as a natural section break thanks to its timestamp.
Multiple output panels
A command can declare more than one output argument and stream to each independently:
Arguments = {
Progress = { Type = "output", Default = "", LayoutOrder = 1 },
Errors = { Type = "output", Default = "", Timestamp = false, LayoutOrder = 2 },
} :: Types.ArgumentType,
Run = function(props)
local progress = props:Output("Progress")
local errors = props:Output("Errors")
-- ...
progress:print("Step 1 of 3...")
errors:warn("Skipped one item")
end,
Useful when you want a fast-scrolling progress feed and a separate "things that went wrong" summary the user can read at the end without scrolling.
When to reach for output (and when not to)
Use output when:
- The command produces enough text that a
labelwould feel cramped. - You want the user to scan results, not just see "done."
- Severity matters — some lines are warnings, some are hard errors.
Skip it when:
- The command is one-shot and the result is a single sentence. Use
props:SendNotification(...)and alabelinstead. - The command doesn't emit text at all. An empty output panel just takes up space.