Use when adding or changing any game state in a Roblox project - round status, currency, inventory, settings, UI state, NPC or door state - or when code passes values around with BindableEvents, global tables or polling loops. Picks the right home for each piece of state (Charm signals, Attributes or the server's data layer) and wires it up reactively.
Use the robloxvibecoders MCP: call get_resource with id "skill-roblox-state-management" and install it as a Claude Code skill in this project (.claude/skills/roblox-state-management/SKILL.md).
Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.
What your AI reads
SKILL.md
---
name: roblox-state-management
description: Use when adding or changing any game state in a Roblox project - round status, currency, inventory, settings, UI state, NPC or door state - or when code passes values around with BindableEvents, global tables or polling loops. Picks the right home for each piece of state (Charm signals, Attributes or the server's data layer) and wires it up reactively.
---
# State management for Roblox
Most bugs in vibe-coded games are state bugs: two scripts with their own copy of the coins, a UI that forgets to
update, a `while true do wait()` loop checking a value, a BindableEvent nobody disconnects. Fix it at the source:
every piece of state has exactly one owner, and everything else reacts to it.
## The rules
1. **One source of truth.** Each value lives in one place. Everything else reads it or derives from it.
2. **The server owns anything that matters** (currency, inventory, progress, round state). Clients get a read-only
copy. See the `roblox-server-authority` skill.
3. **React, don't poll.** Never loop to check whether a value changed. Subscribe to it.
4. **Derive, don't duplicate.** If a value can be computed from other state (level from XP, "can afford" from
coins and price), compute it. Don't store it twice.
5. **Updates are immutable.** Replace tables instead of mutating them, so whoever is watching sees the change.
6. **Everything you subscribe to gets cleaned up** when the thing it belongs to goes away.
## Where each kind of state lives
| State | Home | Why |
|---|---|---|
| App-wide game state: round status, timers, settings, shop open | **Charm** signals or atoms in one store module | Fine-grained reactivity, computed values, works on server and client |
| Per-player server state that clients see: coins, inventory, quests | **Charm** on the server, synced with **Charm Sync** | Delta-compressed, per-player keys, one sync remote |
| State that belongs to one Instance: door open, NPC health, ore remaining, team of a part | **Attributes** on that Instance | Replicates for free, visible in Studio, survives without code |
| Saved data | Your data layer (ProfileStore and friends), loaded into Charm on join | Charm is runtime state, not storage |
| UI-only state: which tab is open, hover, an input's text | A local Charm signal or your UI library's own state | Never leaves the client |
**Prefer Charm over hand-rolled Luau.** A plain table plus a BindableEvent plus "remember to fire it" is how state
drifts. Charm gives you the same thing with dependency tracking, computed values, cleanup and syncing built in. Only
fall back to plain Luau when the value never changes after startup.
**Prefer Attributes over ValueObjects** (IntValue, BoolValue...) for new code. Attributes are lighter, don't create
child Instances, and can be read and edited in the Properties window.
## Wiring it up
- One `Store` module (shared, or one per side) creates the signals. Systems import the store; they don't create
their own copies.
- Server systems write. Clients read synced state and send *requests* through remotes; the server validates and
writes.
- UI binds to signals with `vide-charm` or `react-charm` (`useSignalState`), or with `effect()` for plain
Instance UI.
- Instance state: the server sets the Attribute; scripts listen with `GetAttributeChangedSignal`. To use it inside
Charm, mirror it into a signal (see the `roblox-attributes` skill).
For the APIs, load the `roblox-charm` and `roblox-attributes` skills.
## Red flags to fix on sight
- `while task.wait() do if x ~= lastX then ... end end`: replace with a subscription.
- The same value stored in a ModuleScript table *and* an IntValue *and* the UI text.
- `BindableEvent` used to say "coins changed": make coins a signal.
- A client writing state that the server trusts.
- Mutating a table stored in a signal (`inventory[1] = item`): copy, change, set.
- Connections or effects created per player or per instance with no cleanup on leave or destroy.