Live Config
Live Config lets you change game values while the game is running: lucky block drops and chances, shop prices, event switches, multipliers. You don’t publish a new version of the game.
It is built on Roblox’s native experience configs . Roblox stores the values and delivers them to every server. RoControl is the visual editor and publisher on top of them.
| Piece | Role |
|---|---|
| Roblox experience configs | Hold the values and deliver them to live servers in about 15 seconds to 1 minute after a publish, with no game update |
| RoControl Live Config | A canvas editor for categories and items, with templates, images, chances and references. It publishes to Roblox for you |
LiveConfig.luau | A ModuleScript for your game: read values, roll drops, react to updates |
Example: in Escape from Granny, lucky blocks drop brainrots with chances. The team changes a chance or adds a brainrot on the website, clicks Publish, and running servers use the new drop table within a minute.
How values reach your game
- You edit categories and items on the canvas. RoControl saves as you go and counts the changes that are not live yet.
- Publish writes one Roblox config key per category, plus a
RoControlmanifest key, to the experience’sInExperienceConfigrepository, and publishes it. - Roblox delivers the new values to running servers.
- The SDK refreshes, and
LiveConfig.onChangedfires for the categories that changed.
Setup
- Open the game from the sidebar.
- Open Live Config. It is always available: there is no module switch.
- Choose how RoControl publishes to Roblox:
| Option | What to do |
|---|---|
| A. Open Cloud API key | In Creator Hub, create an API key with configs access: the universe read and write permissions (universe:read, universe:write), restricted to this experience. Paste it in Live Config → Roblox access (the shield button) |
| B. Connect Roblox (OAuth) | Connect your Roblox account with the universe:read and universe:write scopes, and select this experience on Roblox’s consent screen |
Reusing the Event Automation key. If the game already has an Event Automation API key, Live Config reuses it. Add the universe read and write permissions to that same key instead of creating a second one.
Option B needs the universe:read and universe:write scopes enabled on the
RoControl OAuth app.
When more than one is set, RoControl publishes with the Live Config API key first, then the Roblox connection, then the Event Automation key. You can build categories before any of this; access is only needed to publish and to upload images.
Team permissions
| Action | Allows |
|---|---|
| View | See the canvas and the publish history |
| Edit | Change categories, items, values and images |
| Publish | Publish to Roblox and manage the API key or connection |
See Team Permissions.
The canvas
Each category is a card on a canvas. Drag cards to arrange them; the layout is
only for you and does not change what is published. Cards show the category’s
and items’ images. Reference fields are drawn as links between categories, for
example from LuckyBlocks to Brainrots.
Right-click for shortcuts:
| Right-click on | Gives |
|---|---|
| A card | Open, add item, rename, image, color, edit fields, copy key, delete |
| An item (on a card or in a category) | Edit, image, duplicate, hide from game / show, copy id, delete |
| Empty canvas | New category or settings at that spot, a template there, fit everything |
Collection and settings categories
| Kind | Holds | Published as | Read in game with |
|---|---|---|---|
| Collection | A list of items. Each has an id, a name, an optional image and your fields | { "items": [ ... ] } | LiveConfig.list, LiveConfig.get |
| Settings | One record of values | { "coinMultiplier": 2, ... } | LiveConfig.settings, LiveConfig.value |
- Category key: made from the name (
Lucky BlocksbecomesLuckyBlocks). It is the Roblox config key and the name your code uses.RoControlis reserved for the manifest. - Item id: made from the name (
Golden Blockbecomesgolden_block). Renaming an item keeps its id, so use the id in code and in saved player data. Change an id only while nothing refers to it yet: references and saved data keep the old one. - Disabled items (hidden from the game) are not published.
Templates
Templates add ready-to-fill categories in one click.
| Template | Categories |
|---|---|
| Lucky blocks | Brainrots (rarity, income /s, sell price) and LuckyBlocks (price, rarity, drops: brainrot + chance) |
| Eggs and pets | Pets (rarity, multiplier) and Eggs (price, hatches: pet + chance) |
| Shop | Shop (price, currency Coins / Gems / Robux, on sale, description) |
| Game settings | GameSettings, a settings category: coin multiplier, luck boost, event active, announcement |
If a key is already taken, the template adds a number (Brainrots2), and its
references follow the new key.
Fields
| Type | Holds | Published as |
|---|---|---|
| Text | Names, descriptions, messages | "Double coins!" |
| Number | Prices, income, multipliers. Optional min, max and unit | 100 |
| Boolean | On/off switches | true |
| Image | A Roblox image asset | "rbxassetid://123" |
| Select | One of a few options, like a rarity | "Epic" |
| Color | A hex color | "#ff8800" |
| Reference | An item of another category | The item’s id, "tralalero" |
| List | Rows with their own columns | [ { "brainrot": "tralalero", "chance": 50 } ] |
Fields with no value are left out of the published JSON (or take the field’s
default when it has one). id, name, image and items are built in and
can’t be used as field keys. Lists can’t contain lists.
Lists and chances
A list can name one of its number columns as its chance column, the drop weight of each row. The editor shows each row’s percent of the total.
Weights don’t need to add up to 100:
| Brainrot | Chance | Shown as |
|---|---|---|
| Tralalero | 50 | 90.9% |
| Bombardiro | 5 | 9.1% |
In the game, LiveConfig.roll and LiveConfig.rollDrop use the same weights,
and LiveConfig.chances returns the same percentages.
Images
Images are Roblox image asset IDs, published as rbxassetid://ID strings that
your game can put straight into ImageLabel.Image.
- Paste an ID (
123456789), anrbxassetid://string, or a Creator Store or library link. - Upload a PNG, JPG, BMP or TGA image (up to 6 MB). RoControl uploads it to
Roblox as a Decal owned by the experience’s owner. Uploading needs the
Assets permission (
asset:write) on the API key or Roblox connection.
Decal ID or Image ID? A Decal is its own asset that wraps an image, and the two IDs can differ. An
ImageLabelmay not show a Decal ID. For images used in UI, copy the Image ID from Studio’s Asset Manager and paste it.
Publishing
- The header shows how many changes are pending: categories whose published value would change.
- Publish opens a summary of the changes, with an optional message and a rollout choice:
| Rollout | Effect |
|---|---|
| Immediate | All servers in about 15 seconds to 1 minute |
| Gradual | Rolls out over about 15 minutes |
- Only changed keys are sent. Deleting a category removes its key from Roblox.
- Conflict protection: before publishing, RoControl reads the live configs. If a key it manages was changed outside RoControl since its last publish (for example in Creator Hub), RoControl lists those keys and asks before overwriting them.
- History: every publish is recorded with its version, message, changed keys and author. The publishes also appear on the Configs History page in Creator Hub.
Limits
| Limit | Value |
|---|---|
| Config keys per experience | 100. Each category uses one key, the manifest uses one, and configs created outside RoControl count too |
| Characters per value | 100,000 per category (its JSON). Split a category that is too large |
Import from your game
Your game can fill the board with its own current values, so you don’t retype
anything. A game server sends them on start with the game’s SDK key (the
rocontrol_sdk HttpService secret the RoControl SDK already uses):
POST https://api-rocontrol.duckdns.org/api/sdk/v1/live-config/defaults
Authorization: Bearer <SDK key>
{ "universeId": "123", "categories": [ { "key": "Brainrots", "name": "Brainrots",
"kind": "collection", "fields": [ ... ], "items": [ { "key": "TungTungSahur",
"name": "Tung Tung Sahur", "image": "rbxassetid://1", "values": { ... } } ] } ] }- An empty board is filled with it right away. Nothing changes in game until you press Publish.
- Later reports show an Import from game button with what is missing. It adds missing categories and items and never overwrites your edits.
- References to items the report doesn’t contain are dropped, and listed in the
response’s
warnings. - The report’s
universeIdmust match the key’s game.
A game should keep its own values as the fallback and apply published ones on
top, value by value. Escape From Granny’s LiveConfigBridge does this: a value
that is missing, malformed or out of range keeps the game’s local value, and a
LiveConfig category with enabled = false switches everything back to local.
Using it in your game
Install
- Download
LiveConfig.luaufrom Use in your game (the>_button in the Live Config toolbar), or fromhttps://rocontrol.duckdns.org/sdk/LiveConfig.luau. - In Studio, add a ModuleScript named
LiveConfigto ReplicatedStorage and paste the file’s contents. - Require it from any script:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LiveConfig = require(ReplicatedStorage.LiveConfig)ConfigService only works on the server. The SDK reads it there, and mirrors
the values to clients when the server calls LiveConfig.replicate().
Example: open a lucky block
A server Script that opens a block when a client asks. The client only sends the block id; the server checks the price, rolls and gives the reward.
-- ServerScriptService/LuckyBlocks.server.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LiveConfig = require(ReplicatedStorage.LiveConfig)
local openLuckyBlock = ReplicatedStorage:WaitForChild("OpenLuckyBlock") -- a RemoteEvent
LiveConfig.replicate() -- let clients show blocks and drop rates
openLuckyBlock.OnServerEvent:Connect(function(player, blockId)
if typeof(blockId) ~= "string" then
return
end
local block = LiveConfig.get("LuckyBlocks", blockId)
if not block then
return -- unknown or disabled block
end
local coins = player.leaderstats.Coins
local price = block.price or 0
if coins.Value < price then
return
end
-- Rolls the block's drops list and returns the brainrot it points to.
local brainrot = LiveConfig.rollDrop("LuckyBlocks", blockId)
if not brainrot then
warn(block.name, "has nothing to drop")
return
end
coins.Value -= price
giveBrainrot(player, brainrot.id) -- your inventory code: save the id, not the name
print(player.Name, "got", brainrot.name, brainrot.rarity, brainrot.income, "/s")
end)rollDrop picks the item’s first list with a chance column (drops here),
rolls it, and resolves the row’s reference (brainrot) to the Brainrots
item. It returns (brainrot, row). Rows that point at a brainrot that is no
longer published (deleted or disabled) are skipped, so a disabled brainrot
stops dropping.
Example: show drop percentages
-- StarterPlayerScripts/LuckyBlockInfo.client.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LiveConfig = require(ReplicatedStorage.LiveConfig)
local function showDrops(blockId: string)
local block = LiveConfig.get("LuckyBlocks", blockId)
if not block or not block.drops then
return
end
local percents = LiveConfig.chances(block.drops)
for index, row in block.drops do
local brainrot = LiveConfig.get("Brainrots", row.brainrot)
local label = if brainrot then brainrot.name else row.brainrot
print(string.format("%s: %.1f%%", label, percents[index]))
end
end
showDrops("golden_block")
LiveConfig.onChanged(function()
showDrops("golden_block")
end, "LuckyBlocks")Clients
ConfigService is server-only, so clients read a copy:
- The server calls
LiveConfig.replicate()once. - The SDK mirrors each category’s JSON into
ReplicatedStorage.RoControlLiveConfig, a Folder of StringValues named by key, plus the manifest. - On clients, the same API reads that folder, read-only, and
onChangedworks too.
On a client, the first read waits up to 10 seconds for the folder, then returns empty results with a warning. Always roll drops and check prices on the server: the client copy is for UI.
Live updates
-- Any category
LiveConfig.onChanged(function(categoryKey)
print(categoryKey, "was updated from RoControl")
end)
-- One category
local connection = LiveConfig.onChanged(function()
refreshShop() -- your function
end, "Shop")
connection:Disconnect() -- when you no longer need itonChangedfires only when a category’s value really changed, including the first time values arrive.- Each callback runs in its own thread, so an error in one doesn’t stop the others.
- New values apply as soon as they arrive. To keep values steady during a round, read them once when the round starts and keep them in a local.
Settings
local multiplier = LiveConfig.value("GameSettings", "coinMultiplier", 1)
local luck = LiveConfig.value("GameSettings", "luckBoost", 1)
if LiveConfig.value("GameSettings", "eventActive", false) then
print(LiveConfig.value("GameSettings", "announcement", ""))
endvalue returns the default when the value is missing or has a different type
than the default.
Loading, errors and Studio
- The first read on a server loads the configs and can yield briefly. Call
LiveConfig.waitUntilReady()at startup to do this wait up front. - If configs can’t load, reads return empty tables instead of erroring, a
warning is printed once, and the server keeps retrying with backoff. When
values arrive,
onChangedfires. - Returned tables are frozen (
table.freeze, deep) so gameplay code can’t change them by accident. They are handed out without copying, which keepslistcheap. Usetable.cloneto get a copy you can sort. - Studio: play sessions see the configs staged in Creator Hub, so you can
try values there before publishing.
ConfigService:SetTestingValue(key, value)overrides one key on the current server only (in Studio, the current play session), and the SDK picks it up. Clear it withConfigService:ClearTestingValue(key).
API reference
| Function | Returns | Description |
|---|---|---|
LiveConfig.list(categoryKey) | {Item} | Items of a collection, in editor order. Empty if unknown |
LiveConfig.get(categoryKey, id) | Item? | One item by id |
LiveConfig.settings(categoryKey) | {[string]: any} | The record of a settings category. Empty if unknown |
LiveConfig.value(categoryKey, fieldKey, default?) | any | One setting, or default when missing or of another type |
LiveConfig.roll(rows, chanceKey?, rng?) | Row? | Weighted random pick. chanceKey defaults to "chance"; missing or negative chances count as 0; nil when the total is 0. rng is an optional Random |
LiveConfig.chances(rows, chanceKey?) | {number} | Percent per row, same order, adding up to 100 |
LiveConfig.rollDrop(categoryKey, id, listField?, rng?) | (Item?, Row?) | Rolls an item’s list (the one given, or the first with a chance column) and returns the referenced item and the row |
LiveConfig.onChanged(callback, categoryKey?) | { Disconnect } | Calls callback(categoryKey) when a category changes |
LiveConfig.ready() | boolean | True once values have loaded |
LiveConfig.waitUntilReady(timeout?) | boolean | Yields until loaded or timeout seconds (default 30) |
LiveConfig.replicate() | Server only: mirror all categories to clients |
Published data
You can also read the configs without the SDK, with
ConfigService:GetConfigAsync(). A JSON value can come back as a table or as a
JSON string, so decode strings with HttpService:JSONDecode.
{
"LuckyBlocks": {
"items": [
{
"id": "golden_block",
"name": "Golden Block",
"image": "rbxassetid://123",
"price": 100,
"rarity": "Epic",
"drops": [
{ "brainrot": "tralalero", "chance": 50 },
{ "brainrot": "bombardiro", "chance": 5 }
]
}
]
},
"GameSettings": { "coinMultiplier": 2, "eventActive": true, "announcement": "Double coins!" },
"RoControl": {
"v": 1,
"categories": {
"LuckyBlocks": {
"name": "Lucky Blocks",
"kind": "collection",
"fields": [
{ "key": "price", "type": "number" },
{
"key": "drops",
"type": "list",
"chance": "chance",
"of": [
{ "key": "brainrot", "type": "reference", "ref": "Brainrots" },
{ "key": "chance", "type": "number" }
]
}
]
}
}
}
}Each top-level key above is a separate Roblox config. The RoControl manifest
tells the SDK which list column is the chance and which category a reference
points to.
Automate with AI (Claude Code / MCP)
RoControl hosts a remote MCP server, so an AI assistant such as Claude Code can edit Live Config for you. You sign in with your RoControl account in the browser: there is no key to copy.
| Setting | Value |
|---|---|
| Server URL | https://api-rocontrol.duckdns.org/mcp |
| One game only | https://api-rocontrol.duckdns.org/mcp/GAME_ID |
| Authentication | OAuth sign-in (or a personal rcp_… token for scripts) |
-
Add the server to Claude Code:
claude mcp add --transport http rocontrol https://api-rocontrol.duckdns.org/mcp -
In Claude Code, run
/mcp, pick rocontrol and choose Authenticate. RoControl opens in your browser: check the account and press Allow.
In the Claude app or on claude.ai, add it under Settings → Connectors → Add custom connector with the same URL.
The connection acts as you, with your workspace permissions, and can’t change your account, password or sign-ins. Disconnect it any time from Live Config → Edit with AI (the robot button in the toolbar).
Games: ids and names
Every game has an ID and a name. list_games shows them and which game is
selected; tools work on the selected game unless you name another (by ID,
Roblox universe ID or name). Ask Claude to switch (“work on Escape from
Granny”) and it calls select_game, which is remembered for that connection,
like picking the open place in Studio.
To tie a project folder to one game, run this inside the folder. It is saved in
the project’s .mcp.json, so Claude always starts on that game there. The
Edit with AI panel shows the command with your game’s ID filled in.
claude mcp add --scope project --transport http rocontrol https://api-rocontrol.duckdns.org/mcp/GAME_IDScripts and CI
For automation without a browser, create a personal token in Edit with AI → Scripts and CI and send it as a header:
claude mcp add --transport http rocontrol https://api-rocontrol.duckdns.org/mcp --header "Authorization: Bearer rcp_YOUR_TOKEN"Tools
| Tool | Does |
|---|---|
list_games | List your games with their IDs, names and which one is selected |
select_game | Choose the game the other tools work on |
list_templates | List the built-in templates |
get_live_config | Read a game’s categories, items and pending changes |
create_category | Create a category |
update_category | Change a category: name, key, fields or settings values |
delete_category | Delete a category |
upsert_item | Create an item, or update the one with the same name or id |
delete_item | Delete an item |
apply_template | Add a template’s categories to a game |
import_from_game | Add what the game reported (missing categories and items only) |
preview_publish | Show what would be published and what changed |
publish | Publish to Roblox |
Wherever a tool takes:
game(optional), it accepts a game ID, a universe ID or the game’s name;category, it accepts an id, a key or a name;item, it accepts an id, a key or a name.
Example prompts
- “Add a brainrot called Tung Tung Sahur with income 25/s and put it in the Golden Lucky Block with chance 3, then publish”
- “In Escape from Granny, list every lucky block with the percent chance of each drop”
- “Apply the Game settings template, set the coin multiplier to 2, and preview the publish”
REST API
The same API is available over HTTPS at https://api-rocontrol.duckdns.org,
with the header Authorization: Bearer rcp_…. :gameId is the RoControl game
id from the game page URL (/games/[gameId]); category and item ids come from
GET /games/:gameId/live-config.
| Method | Path | Does |
|---|---|---|
GET | /games/:gameId/live-config | Categories, items, pending changes and access status |
POST | /games/:gameId/live-config/categories | Create a category |
PATCH | /live-config/categories/:id | Update a category |
DELETE | /live-config/categories/:id | Delete a category |
POST | /live-config/categories/:id/items | Create an item |
PATCH | /live-config/items/:id | Update an item |
DELETE | /live-config/items/:id | Delete an item |
POST | /games/:gameId/live-config/templates/:templateKey | Apply a template |
GET | /games/:gameId/live-config/preview | The JSON that would be published, changed keys and sizes |
POST | /games/:gameId/live-config/publish | Publish. Body: { "message"?, "force"?, "gradual"? } |
GET | /games/:gameId/live-config/history | Recent publishes |
- Item
valuesare merged: send only the fields you change. A list field is replaced as a whole. publishanswers409with the conflictingkeyswhen a key was changed outside RoControl. Send"force": trueto overwrite."gradual": truerolls out over about 15 minutes.
TOKEN=rcp_YOUR_TOKEN
API=https://api-rocontrol.duckdns.org
# Read the game's Live Config
curl -H "Authorization: Bearer $TOKEN" "$API/games/GAME_ID/live-config"
# Change the Golden Block drops
curl -X PATCH "$API/live-config/items/ITEM_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"values":{"drops":[{"brainrot":"tralalero","chance":50},{"brainrot":"bombardiro","chance":8}]}}'
# Publish now
curl -X POST "$API/games/GAME_ID/live-config/publish" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"message":"Golden Block: bombardiro 5 -> 8"}'Troubleshooting
| Problem | Fix |
|---|---|
| Roblox refused the publish (401 or 403) | The API key or connection needs universe:read and universe:write for this experience |
| Image upload refused | Add the Assets permission (asset:write), or paste an existing image ID |
| A category is too large | Split it into two categories (100,000 characters per value) |
| The game reads empty values | Check the output for [LiveConfig] warnings. In Studio, open a place that belongs to the published experience |
| Clients read empty values | Call LiveConfig.replicate() in a server Script |
An image doesn’t show in an ImageLabel | Use the Image ID instead of the Decal ID |