Blog/Roblox/🧠Advanced Strategy

Roblox Studio Plugins: Build, Debug, and Publish

A Roblox Studio plugin is not a game script with extra steps — the plugin global is not passed to ModuleScripts automatically, ChangeHistoryService does nothing at runtime, and Roblox's own tutorial still teaches a method its own reference page marks deprecated. Here is how to build one that actually works: toolbar button, undo support, a dockable widget, saved settings, and what publishing it to the Creator Store pays.

Published September 16, 2026·14 min read·By Mythras
A custom "Empty Script" plugin button inside a new Custom section of the Roblox Studio toolbar ribbon.

Open Roblox's own reference page for Plugin and it says, in the middle of a code sample, that the plugin global "is not passed to ModuleScripts within the plugin. In order to use it in a ModuleScript, you must explicitly pass it." That is not a beginner mistake being corrected — it is a documented behaviour of the API itself, and it is the first thing that trips up anyone who structures a plugin the way they'd structure a game (main Script requiring a ModuleScript and expecting the same globals to carry over). A second thing worth knowing before you write a line of code: Roblox's own Studio widgets tutorial still walks through Plugin:CreateDockWidgetPluginGui() as of this writing — a method its own class reference marks "Deprecated: This method has been superseded by CreateDockWidgetPluginGuiAsync()." Two official pages, one live contradiction. This guide uses the current method and flags where the docs disagree with themselves.

This vertical has covered building games in Roblox Studio — Studio basics, Luau fundamentals, ModuleScripts — but never building a tool for Studio itself. A plugin is not a game script that happens to run in the editor. It has its own security context (PluginSecurity), its own undo/redo system that does nothing at runtime, and its own distribution and monetization path through the Creator Store that pays differently than the DevEx pipeline covered in the DevEx and monetization guide.

What actually makes a script a plugin

A regular Script becomes a plugin by where it runs, not by anything written in it. Roblox's documentation describes a plugin as "an extension that adds additional features or functionality to Studio," and the plugin global — the object every plugin API hangs off of — only exists when that script executes as a plugin: saved as a local plugin, running under PluginDebugService, or installed from the Toolbox. Run the exact same source as a normal Script in ServerScriptService and plugin is nil.

That single global is also where the ModuleScript gap above comes from. Plugin reference documentation gives the fix directly: pass plugin into an Initialize function on the module rather than expecting the module to read a global.

-- Script (the plugin entry point)
local pluginModule = require(script.Parent.PluginModule)
pluginModule:Initialize(plugin)
-- PluginModule (a ModuleScript)
local pluginModule = {}
local plugin

function pluginModule:Initialize(pluginReference)
	plugin = pluginReference
end

return pluginModule

Every plugin method call carries Security: PluginSecurity on its reference page — a different security class than the Script/LocalScript code this vertical's other guides cover, which is also why a plugin can do things an in-game script cannot: reach the Selection service, insert instances directly into ServerStorage from the Explorer, and drive Studio's own undo stack.

Turn on Plugin Debugging before you write anything

Before saving your first plugin, Roblox's tutorial has one setup step: enable Plugin Debugging Enabled in the Studio section of Studio's settings. Doing so "will expose the PluginDebugService in Studio, which provides real-time debugging for your plugin's code and makes it easier to reload and save your plugin." Skip this and every edit means re-saving the plugin file and hoping Studio picks it up; with it on, you get an explicit debugger tree and one-click reload, covered below.

Build a toolbar button: your first plugin

Roblox's own worked example is a plugin called AddEmptyScript that inserts an empty Script as the child of whatever the user has selected, or into ServerScriptService if nothing is selected. It is small enough to read in full and it demonstrates the three pieces every button-driven plugin needs — a toolbar, a button, and a click handler:

local ServerScriptService = game:GetService("ServerScriptService")
local Selection = game:GetService("Selection")

-- Create a new toolbar section and Plugins menu folder titled "Custom"
local toolbar = plugin:CreateToolbar("Custom")

-- Add a toolbar button labeled "Empty Script"
local newScriptButton = toolbar:CreateButton("Empty Script", "Create an empty script", "rbxassetid://14978048121")

-- Make button clickable even if 3D viewport is hidden
newScriptButton.ClickableWhenViewportHidden = true

local function onPluginButtonClicked()
	local selectedObjects = Selection:Get()
	local parent = selectedObjects[1] or ServerScriptService
	local newScript = Instance.new("Script")
	newScript.Source = ""
	newScript.Parent = parent
end

newScriptButton.Click:Connect(onPluginButtonClicked)

Plugin:CreateToolbar(name) both creates a labeled section in the Plugins ribbon tab and a matching folder in the Plugins menu — the code above's "Custom" argument is what produces the section you see below. PluginToolbar:CreateButton() takes a label, a tooltip ("Create an empty script"), and an icon asset id, and returns a PluginToolbarButton whose Click event is your entry point. ClickableWhenViewportHidden is worth setting explicitly — its default lets a plugin button go dead the moment the 3D viewport isn't visible, which is exactly the state a docked plugin widget puts Studio into.

New plugin button added to a Custom toolbar section in Studio's Plugins tab.

Save it locally, then reload without restarting Studio

A plugin script has to exist somewhere Studio treats as a plugin, not as game content. The path:

  1. Insert a Script inside ServerStorage and rename it AddEmptyScript.
  2. Paste the code above into it.
  3. With the script selected in the Explorer, choose Save as Local Plugin from Studio's Plugins menu, then Save in the popup — this "insert[s] the plugin script into your local Plugins folder of the Studio installation."

The AddEmptyScript script selected in ServerStorage before being saved as a local plugin.

  1. The plugin appears under PluginDebugService and starts running immediately.

The AddEmptyScript plugin now running inside PluginDebugService after being saved.

  1. Delete the original script in ServerStorage and keep editing the copy inside PluginDebugService — Roblox's documentation calls this out as important, "otherwise you may end up applying changes to the wrong script." The ServerStorage copy is a one-time source for the save step, not a live-edited original.

Click the button and a new empty Script lands wherever you had selected, or in ServerScriptService if nothing was:

An empty Script inserted into ServerScriptService by clicking the custom plugin toolbar button.

From then on, edits go through PluginDebugService itself: right-click the plugin there and choose Save and Reload Plugin to push a change, or Reload Plugin to re-run without re-saving — useful for stepping through a breakpoint without persisting a half-finished edit. Right-clicking PluginDebugService itself and choosing Save and Reload All Plugins in Debugger (Ctrl+Shift+L / ⌘+Shift+L) reloads everything you're debugging at once.

Add undo and redo with ChangeHistoryService

Any plugin that changes the place — moving parts, setting properties, inserting instances — has to register those changes with ChangeHistoryService, or a player's <kbd>Ctrl</kbd>+<kbd>Z</kbd> simply won't touch what the plugin did. Roblox's class reference states plugin developers "must use ChangeHistoryService to tell Studio how to undo and redo changes," and separately notes that the service "is not enabled at runtime, so calling its methods in a running experience has no effect" — it is an editor-only system, not something to reach for inside a published game.

The pattern is begin, act, finish:

local ChangeHistoryService = game:GetService("ChangeHistoryService")
local Selection = game:GetService("Selection")

local toolbar = plugin:CreateToolbar("Example Plugin")
local button = toolbar:CreateButton("Neon it up", "", "")

button.Click:Connect(function()
	local recording = ChangeHistoryService:TryBeginRecording("Set selection to neon")
	if not recording then
		-- Only one recording per plugin can be active at a time.
		return
	end

	for _, instance in Selection:Get() do
		if instance:IsA("BasePart") then
			instance.Material = Enum.Material.Neon
		end
	end

	ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
end)

TryBeginRecording() returns nil if your plugin already has a recording open — the docs are explicit that "you may only have one recording per plugin active at a time" — so the nil check above isn't defensive boilerplate, it's the documented failure mode. FinishRecording() takes the identifier plus an Enum.FinishRecordingOperation, which is one of three values, not just "commit or don't":

OperationWhat it does
CommitAdds the recorded changes to the undo/redo history as a new waypoint
CancelDiscards the recording and reverts the changes it captured; the identifier argument is ignored
AppendMerges the recording into the waypoint immediately before it instead of creating a new one

The older SetWaypoint(name) API still exists but its own reference page states it "will be deprecated soon in favor of TryBeginRecording()" — new plugins should skip it. If you do encounter it in older sample code, the convention documented for it is to call it after a set of changes, never before.

Build a dockable UI with a DockWidgetPluginGui

A toolbar button that just fires an action is one shape of plugin; a plugin with its own persistent panel is built on DockWidgetPluginGui. A custom widget starts from Plugin:CreateDockWidgetPluginGuiAsync(), which takes a unique string id and a DockWidgetPluginGuiInfo object built with exactly seven positional arguments, in this order:

#ParameterTypeDefault
1InitialDockStateEnum.InitialDockStateRight
2InitialEnabledbooleanfalse
3InitialEnabledShouldOverrideRestorebooleanfalse
4FloatingXSizenumber0
5FloatingYSizenumber0
6MinWidthnumber0
7MinHeightnumber0
local widgetInfo = DockWidgetPluginGuiInfo.new(
	Enum.InitialDockState.Float, -- initial dock state
	true,   -- initially enabled
	false,  -- don't override the previously saved enabled state
	200,    -- floating width
	300,    -- floating height
	150,    -- minimum width
	150     -- minimum height
)

local testWidget = plugin:CreateDockWidgetPluginGuiAsync("TestWidget", widgetInfo)
testWidget.Title = "Test Widget"

The pluginGuiId string ("TestWidget" above) is not cosmetic — it is "used to save the state of the widget's dock state and other internal details" between Studio sessions, which is what InitialEnabledShouldOverrideRestore is for: leave it false and a returning user's last enabled/disabled state wins over InitialEnabled; set it true and your InitialEnabled value always wins. MinWidth/MinHeight are floors, not guarantees — the datatype reference notes "each platform has its own absolute minimum that Roblox will enforce," giving the example that "on a Mac, the width can never be less than ~80 pixels" to leave room for the window's close/minimize/maximize buttons.

This is also the exact spot where Roblox's own documentation disagrees with itself, noted at the top of this guide: the class reference marks the older Plugin:CreateDockWidgetPluginGui() (no "Async") as deprecated in favor of the method above, but the tutorial page walking through widget construction still uses the deprecated, synchronous name in its sample code as of this writing. Both methods take identical arguments and both currently work — use CreateDockWidgetPluginGuiAsync, since it's the one the reference page says to use going forward.

Fill the widget with ordinary GuiObjects parented to it just like a ScreenGui — the UI design basics guide covers Frame/TextButton/UDim2 mechanics that apply unchanged here. Roblox also maintains a StudioWidgets GitHub repo of pre-built, dark-theme-aware checkboxes, radio buttons, and input fields specifically so third-party widgets don't look out of place next to Studio's own panels.

Two gaps a widget has that a game script does not

Theme. A widget that ignores Studio's light/dark setting looks broken next to native panels. Sync it by reading Enum.StudioStyleGuideColor values through the current theme and re-applying them on change:

local function setColors(objects)
	for _, guiObject in objects do
		guiObject.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
		guiObject.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainText)
	end
end

setColors({ testButton })
settings().Studio.ThemeChanged:Connect(function()
	setColors({ testButton })
end)

Input. UserInputService, the service most in-game UI relies on for key and mouse events, "doesn't work [in a widget] since these services expect the main game window to be in focus." The documented workaround is a transparent Frame stretched over the widget that listens for InputBegan directly, rather than reaching for UserInputService at all:

local frame = Instance.new("Frame")
frame.BackgroundTransparency = 1
frame.Size = UDim2.new(1, 0, 1, 0)
frame.Parent = testWidget

frame.InputBegan:Connect(function(inputObject)
	-- handle keyboard/mouse input scoped to this widget
end)

Persist settings across sessions with GetSetting and SetSetting

Plugin:SetSetting(key, value) and Plugin:GetSetting(key) store a value under your plugin's own storage and return it on later Studio sessions — the standard way to remember, say, whether a user has seen an onboarding message before:

local RAN_BEFORE_KEY = "RanBefore"
local didRunBefore = plugin:GetSetting(RAN_BEFORE_KEY)

if didRunBefore then
	print("Welcome back!")
else
	plugin:SetSetting(RAN_BEFORE_KEY, true)
end

The documented gotcha: "multiple instances of the same plugin can run simultaneously (for example, if multiple Studio windows are open)," and GetSetting() "can silently fail and return nil if multiple instances of the same plugin are actively reading and writing data." Roblox's own recommendation for a plugin that writes settings frequently is to re-check the returned value after a short delay before treating a nil as "this key has never been set," rather than trusting the first read.

Bind a keyboard shortcut without a toolbar button

Not every plugin action needs a visible button. Plugin:CreatePluginAction(actionId, text, statusTip, iconName, allowBinding) creates a PluginAction — an action Studio can list in its keyboard-shortcuts screen and let a user bind a hotkey to — with no toolbar button attached at all:

local pluginAction = plugin:CreatePluginAction(
	"HelloWorldAction",
	"Hello World",
	"Prints a greeting",
	"rbxasset://textures/sparkle.png",
	true -- allowBinding
)

pluginAction.Triggered:Connect(function()
	print("Hello world!")
end)

Set allowBinding to false for an action meant to be triggered contextually — say, from a right-click menu built with the related Plugin:CreatePluginMenu() — rather than something a user should be assigning a global hotkey to.

Publish it, then decide whether to sell it

Publishing and monetizing are separate steps. To publish: select the plugin script in the Explorer, choose Publish as Plugin from the Plugins menu, optionally upload a 512×512 thumbnail, fill in Name, Description, and Creator, then submit — it's now yours under the Toolbox's Inventory and Creations tabs.

Selling it on the Creator Store is a separate, optional distribution step, and Roblox's current documentation is specific about the split: you can "sell a Model or Plugin and earn 100% of net proceeds on transactions... as only taxes and payment processing fees are deducted," explicitly "bypassing platform fees and DevEx rates" — a different, USD-denominated payout path than the Robux-based DevEx system this vertical's monetization guide covers for game passes and developer products. Setting a USD price requires a seller account, administered through Stripe, which per Roblox's account-requirements page needs:

  • Being 18 or older, or 13–17 with a parent/guardian reviewing the Creator Store terms and completing the Stripe form
  • Roblox account 2-Step Verification enabled
  • Residency in a country Stripe's cross-border payouts support — Roblox's own docs flag Brazil, China, India, and Russia as currently unsupported for seller onboarding

Distribution itself — even free plugins — is capped per 30-day window, and the cap depends on account verification status:

Asset typeVerified accountUnverified account
Plugins102
Mesh / Image / Model assets20010
Audio10010

Account verification here means passing Roblox's age check or government-ID verification — explicitly not phone verification, which the docs call out as insufficient for this purpose.

Quick Action Checklist

  • Enable Plugin Debugging Enabled in Studio's settings before saving anything, so PluginDebugService exposes real-time reload.
  • The plugin global is not passed into ModuleScripts automatically — pass it explicitly through an Initialize() call.
  • Build the toolbar with Plugin:CreateToolbar() + PluginToolbar:CreateButton(), and set ClickableWhenViewportHidden = true if the button should still work while a widget is docked over the viewport.
  • Save via Save as Local Plugin, delete the original script in ServerStorage, and keep editing the copy under PluginDebugService — editing the original silently orphans your changes.
  • Wrap every place-modifying action in ChangeHistoryService:TryBeginRecording() / FinishRecording(); the service does nothing at runtime, so this only matters in Studio.
  • TryBeginRecording() returns nil, not an error, if a recording is already open for your plugin — check for it.
  • Use Plugin:CreateDockWidgetPluginGuiAsync(), not the deprecated synchronous CreateDockWidgetPluginGui() that Roblox's own tutorial page still demonstrates.
  • UserInputService does not work inside a widget; capture input with an InputBegan-listening transparent Frame instead.
  • Plugin:GetSetting() can return a false nil when multiple Studio windows run the same plugin at once — don't treat a single nil read as proof a setting was never set.
  • Selling on the Creator Store pays 100% of net proceeds (taxes/processing only) but requires a Stripe seller account, 2-Step Verification, and residency outside a short list of unsupported countries — and plugin distribution itself is capped at 10 per 30 days even for verified accounts.

Frequently Asked Questions

Why is my Roblox Studio plugin global (`plugin`) nil inside a ModuleScript?
Roblox's own Plugin class reference states that the plugin global reference is not passed to ModuleScripts within the plugin automatically. To use it inside a ModuleScript, the plugin's main Script must explicitly pass the plugin object into a function on the module — for example, calling pluginModule:Initialize(plugin) from the entry-point script and storing that reference inside the module. Accessing a bare `plugin` global from within a required ModuleScript, the way a normal Script would, returns nil.
Does ChangeHistoryService work while a Roblox game is running, not just in Studio?
No. Roblox's ChangeHistoryService class reference states directly that the service is not enabled at runtime, so calling its methods, including TryBeginRecording, FinishRecording, Undo, and Redo, in a running experience has no effect. It exists purely to let Studio plugins register undo/redo history for edits made to a place while it's open in the editor, not for gameplay-time actions.
Should I use Plugin:CreateDockWidgetPluginGui or CreateDockWidgetPluginGuiAsync?
Use CreateDockWidgetPluginGuiAsync. Roblox's Plugin class reference explicitly marks the older CreateDockWidgetPluginGui method as deprecated, stating it has been superseded by CreateDockWidgetPluginGuiAsync. Both methods currently accept the same pluginGuiId and DockWidgetPluginGuiInfo arguments and both still function, but Roblox's own Studio widgets tutorial page has not yet been updated to reflect the deprecation and still demonstrates the older synchronous method in its sample code.
Why does UserInputService not work inside a Roblox Studio plugin widget?
Roblox's Studio widgets documentation states that UserInputService doesn't work inside a DockWidgetPluginGui because that service expects the main game window to be in focus, which a floating or docked plugin widget is not. The documented workaround is to create a transparent Frame that covers the widget and listen for its InputBegan event directly, rather than relying on UserInputService for keyboard or mouse input inside the widget.
How much money do developers actually keep from selling a Roblox Studio plugin?
According to Roblox's current Creator Store documentation, sellers earn 100% of net proceeds on Creator Store transactions for a Plugin or Model, with only taxes and payment processing fees deducted — Roblox states this bypasses its own platform fees and DevEx rates entirely. Setting a USD price requires a seller account administered through Stripe, which requires being 18 or older (or 13-17 with parental consent), enabling 2-Step Verification on the Roblox account, and residing in a country Stripe's cross-border payouts support; Roblox's documentation names Brazil, China, India, and Russia as currently unsupported for seller onboarding.
How many plugins can I distribute on the Roblox Creator Store per month?
Roblox's Creator Store documentation caps plugin distribution at 10 per 30-day period for a verified account (age-checked or government-ID verified) and 2 per 30-day period for an unverified account. This limit applies to distributing a plugin at all, whether it's offered free or for sale, and is separate and lower than the limits for mesh, image, and model assets (200 verified / 10 unverified) or audio assets (100 verified / 10 unverified).

Keep Reading

Sources & Further Reading
Last updated September 16, 2026.

Related Guides

The Roblox Studio Explorer window showing the default service list — Workspace, Players, Lighting, MaterialService and ReplicatedFirst — with the Workspace branch expanded.
🧠Advanced StrategySep 12, 2026·11 min read

Roblox Custom Loading Screen: ReplicatedFirst + Preload

A LocalScript anywhere other than ReplicatedFirst does not run until the game has already loaded, which is why your loading screen never shows up. Here is what ReplicatedFirst actually guarantees, why the default Roblox screen vanishes on its own a few seconds after you put anything in there, and why game:IsLoaded() returning true does not mean a single texture has downloaded.

Read article
Roblox Creator Documentation diagram showing how a character's distance from a ProximityPrompt object determines whether the prompt appears on screen, illustrating the MaxActivationDistance property.
🧠Advanced StrategySep 4, 2026·11 min read

Roblox ProximityPrompt Guide: Setup, Style, Limits

Put three doors in a row, tag each with a ProximityPrompt bound to E, and only one shows a prompt at a time — not a bug, a property called Exclusivity that ships on a default almost nobody reads. Here is every ProximityPrompt and ProximityPromptService property, its actual default value, and the two events whose names quietly change depending on where you connect them.

Read article
The Funnels page of the Roblox Creator Dashboard showing a nine-step onboarding funnel for the Plant reference game, with a 70.48% churn at step 2, Plant Seed.
🧠Advanced StrategySep 2, 2026·13 min read

Roblox AnalyticsService: Custom Events, Funnels, Limits

Roblox ships a full event-tracking pipeline into every experience for free, and the single sentence that decides whether it works is buried in a warning box: events can only be sent from the server and in published games. Here is what AnalyticsService logs, the published cardinality and rate limits, the three custom-field slots that do most of the work, and one number Roblox’s own docs disagree with themselves about.

Read article
Roblox Creator Documentation diagram of two frames, each split into a blue Parallel Execution Phase and a red Serial Execution Phase — the lower row shows one long parallel computation pushing the serial phase past the frame boundary and creating lag.
🧠Advanced StrategyAug 23, 2026·13 min read

Roblox Parallel Luau: What Actually Runs in Parallel

Roblox runs your scripts on one thread until you opt in twice — once by parenting a script to an Actor, once by desynchronizing it. Then the rules change: you can raycast in parallel but not Shapecast, read tags but not add them, and fire no remotes at all. Here is the actual allow-list, straight off the class reference.

Read article
Roblox experiences carousel showing game tiles including Driving Empire, Adopt Me, Field Trip Z, and DOORS fanned around a featured racing game.
🏆Tier ListsMay 30, 2026·11 min read

Best Roblox Games to Play in 2026

Roblox's front page is engagement bait. This is the filtered version: the games with real, sustained player counts and actual staying power, sorted by what you're in the mood for.

Read article
Roblox in-game Buy Item dialog showing an item priced in Robux with a subscription discount applied to the purchase.
🎮Game GuidesMay 30, 2026·11 min read

How to Get Robux Safely (Legit Ways + Scams to Avoid)

There is no free Robux generator. There never was. Here are the actual legit ways to get Robux without overpaying, the earning methods that really work, and the scams that exist purely to steal your account.

Read article