roblox-datastores · gamedev-skills — Claude Code skill: what it does, how to install
Teaches the agent to persist Roblox player data through DataStoreService: load on join, save on leave and on BindToClose, retries. Useful when progress vanishes.
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a claude-codeWhat roblox-datastores does
The skill lays out a server-side loop: GetDataStore by name, a key like "Player_" .. player.UserId, a load on PlayerAdded, a save on PlayerRemoving and another on BindToClose. Code patterns ship inside, all in Luau: reads wrapped in pcall, writes through UpdateAsync, retries with backoff, a counter via IncrementAsync, and a leaderboard on OrderedDataStore read with GetSortedAsync.
The interesting part is failure handling. A network error on GetAsync is not a new player, so the skill flags the session and refuses to save, because defaults would overwrite real progress. SetAsync is out for read-modify-write for the same reason: two servers clobber the key. Worth checking your PlayerAdded handler if players have reported wiped data before.
Setup: enable Studio Access to API Services on a published place, and test in a separate place since Studio hits the same live data as players. DataStores run only from server Script, never LocalScript. General Luau, remotes and the client/server split are out of scope here; roblox-luau covers them.
What it helps with
- Load coins and inventory when a player joins
- Survive a server shutdown without losing progress via
BindToClose - Replace
SetAsyncwithUpdateAsyncso two servers stop clobbering a key - Build a coin leaderboard on
OrderedDataStore - Wrap network calls in
pcalland add retries
Example prompts for the agent
- Write a server
Scriptthat loads the player's coins withGetAsyncinsidepcalland saves them onPlayerRemoving. - Add a coin leaderboard using
OrderedDataStoreand print the top 10 withGetSortedAsync. - Progress got wiped on server restart — add a
BindToClosesave and stop defaults from overwriting real data.
How to install roblox-datastores
1Skills CLI (Vercel)
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a claude-code2Manually: put the files in place
~/.claude/skills/roblox-datastores/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/roblox-datastores/
1Skills CLI (Vercel)
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a codex2Manually: put the files in place
.agents/skills/roblox-datastores/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a cursor2Manually: put the files in place
.cursor/skills/roblox-datastores/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install gamedev-skills/awesome-gamedev-agent-skills roblox-datastores2Manually: put the files in place
.github/skills/roblox-datastores/SKILL.md1Skills CLI (Vercel)
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a gemini-cli2Platform CLI
gemini skills install https://github.com/gamedev-skills/awesome-gamedev-agent-skills --path skills/other-engines/roblox-datastoresManually: put the files in place
.devin/skills/roblox-datastores/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about roblox-datastores
How do I install roblox-datastores in Claude Code?
Run npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a claude-code. The skill lands in ~/.claude/skills/roblox-datastores/SKILL.md.
What does roblox-datastores do?
It gives the agent patterns for DataStoreService: load on join, save on leave and at shutdown, pcall around network calls, retries, and leaderboards through OrderedDataStore.
How is it different from roblox-luau?
roblox-luau handles general Luau, services and remotes. This skill sticks to persistence: GetAsync, SetAsync, UpdateAsync, BindToClose and OrderedDataStore.
Source text
SKILL.md · Apache-2.0
---
name: roblox-datastores
description: >
Persist player data in Roblox with DataStoreService: GetDataStore, GetAsync/
SetAsync/UpdateAsync/IncrementAsync wrapped in pcall, load-on-join and
save-on-leave plus BindToClose, retries, and OrderedDataStore leaderboards. Use
when saving or loading persistent data in a Roblox experience — when the user
mentions DataStore, DataStoreService, GetAsync, SetAsync, UpdateAsync, save player
data, or leaderboards. For general Luau scripting use roblox-luau.
---
# Roblox DataStores
Persist data across sessions in Roblox with `DataStoreService`: loading on join,
saving on leave and shutdown, safe updates, retries, and ordered stores for
leaderboards. Server-side only.
## When to use
- Use to save/load player progress (coins, inventory, levels), build persistent
leaderboards, or fix data loss, overwrites, and throttling.
- Use when server code calls `DataStoreService`, `GetDataStore`, `GetAsync`,
`SetAsync`, `UpdateAsync`, or `GetOrderedDataStore`.
**When *not* to use:** general scripting, services, remotes, the client/server
split → `roblox-luau`. High-frequency temporary state (matchmaking, per-round) →
memory stores (a different service). Engine-agnostic persistence theory →
`save-systems`.
## Core workflow
1. **Enable Studio access once.** In a published place, File → Experience Settings →
Security → *Enable Studio Access to API Services* (use a separate test place; Studio
hits the same live data as players). DataStores work only from server `Script`s,
never `LocalScript`s.
2. **Get a store, then read/write by key.** `DataStoreService:GetDataStore("Name")`;
key per player is usually `"Player_" .. player.UserId`.
3. **Wrap every call in `pcall`.** `GetAsync`/`SetAsync`/`UpdateAsync` are network
calls that can fail; an unguarded failure errors the thread and risks data loss.
4. **Load on `PlayerAdded`, save on `PlayerRemoving`, and also `BindToClose`.** A
leaving player and a shutting-down server both need a final save.
5. **Prefer `UpdateAsync` for read-modify-write** (multi-server safe) over `SetAsync`
(blind overwrite). On a failed load, do **not** overwrite with defaults — abort
the save so you don't wipe good data.
6. **Use `OrderedDataStore` for ranked data** (leaderboards) via `GetSortedAsync`.
Test by joining, changing data, rejoining, and confirming it persisted.
## Patterns
### 1. Load on join (pcall-guarded)
```lua
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("PlayerData")
local DEFAULT = { Coins = 0, Level = 1 }
Players.PlayerAdded:Connect(function(player)
local key = "Player_" .. player.UserId
local ok, data = pcall(function()
return store:GetAsync(key)
end)
if not ok then
-- Load FAILED (network). Do not treat as a new player; flag so we never save
-- over their real data with defaults.
warn("Load failed for", player.Name, data)
player:SetAttribute("DataLoaded", false)
return
end
player:SetAttribute("DataLoaded", true)
local profile = data or DEFAULT -- nil == genuinely new player
applyToLeaderstats(player, profile)
end)
```
### 2. Save with UpdateAsync (multi-server safe)
```lua
-- UpdateAsync reads the latest value, then writes what the callback returns.
-- The callback MUST NOT yield (no task.wait, no further Async calls inside it), and it
-- can run more than once if another server writes the key first, so keep it pure.
local function savePlayer(player)
-- Save only after a confirmed load. `~= true` also covers a player who leaves while
-- GetAsync is still in flight (attribute still nil), which `== false` would miss.
if player:GetAttribute("DataLoaded") ~= true then return end
local key = "Player_" .. player.UserId
local newData = gatherDataFor(player) -- a plain table of serializable values
local ok, err = pcall(function()
store:UpdateAsync(key, function(old)
-- merge/decide here; return nil to cancel the write
return newData
end)
end)
if not ok then warn("Save failed for", player.Name, err) end
end
```
### 3. Save on leave AND on shutdown
```lua
Players.PlayerRemoving:Connect(savePlayer)
-- BindToClose runs when the server shuts down; save everyone still in.
-- The server waits at most 30 seconds for it, so save in parallel and yield until done.
game:BindToClose(function()
local players = Players:GetPlayers()
local remaining = #players
if remaining == 0 then return end
for _, player in players do
task.spawn(function()
savePlayer(player)
remaining -= 1
end)
end
while remaining > 0 do task.wait() end
end)
```
### 4. Retry with backoff (transient failures)
```lua
local function withRetry(fn, attempts)
attempts = attempts or 3
for i = 1, attempts do
local ok, result = pcall(fn)
if ok then return true, result end
if i < attempts then task.wait(2 ^ i) end -- 2s, 4s, ... backoff
end
return false
end
local ok, data = withRetry(function() return store:GetAsync(key) end)
```
Retry only operations that are safe to repeat. A failed write is not proof the write did
not happen: the server may simply have missed the response. Re-running an `UpdateAsync`
that writes the full saved state is harmless; re-running `IncrementAsync` can add twice.
When the outcome matters, confirm it with an uncached read (`DataStoreGetOptions` with
`UseCache = false`) before retrying.
### 5. Increment a counter
```lua
-- IncrementAsync is a convenience for integer read-modify-write (still wrap it).
local ok, newTotal = pcall(function()
return store:IncrementAsync("Visits_" .. player.UserId, 1)
end)
```
### 6. Leaderboard with OrderedDataStore
```lua
local boards = DataStoreService:GetOrderedDataStore("Coins")
-- Write a player's score (call when it changes, not every frame).
pcall(function() boards:SetAsync("Player_" .. player.UserId, coins) end)
-- Read the top 10, descending.
local ok, pages = pcall(function()
return boards:GetSortedAsync(false, 10) -- ascending=false → highest first
end)
if ok then
for rank, entry in ipairs(pages:GetCurrentPage()) do
print(rank, entry.key, entry.value) -- entry.value is the number
end
end
```
## Pitfalls
- **Unhandled failure wipes progress** → always `pcall` Async calls; on a failed
*load*, mark the session and refuse to *save* so defaults never overwrite real data.
- **`SetAsync` race between servers** → two servers writing the same key can clobber
each other. Use `UpdateAsync` for read-modify-write so each write sees the latest.
- **Yielding inside the `UpdateAsync` callback** → the callback can't call
`task.wait` or other Async functions; compute the new value beforehand and return it.
- **Side effects inside the `UpdateAsync` callback** → Roblox re-runs the callback when
another server changed the key between read and write, so granting items, firing
remotes, or incrementing counters there can happen twice. Act after the call returns.
- **Saving a player who left mid-load** → `PlayerRemoving` can fire while `GetAsync` is
still yielding. Gate saves on "load succeeded", not on "load did not fail".
- **No `BindToClose` save** → players in the server at shutdown lose unsaved progress;
add `game:BindToClose` and wait for saves to finish within its budget.
- **Throttling / "too many requests"** → respect per-key and per-minute limits; don't
save on every value change. Batch and save on a timer / on leave. `GetAsync` is
cached for about four seconds, so immediate re-reads may be stale. Studio Run mode
uses its own lower static limits; Roblox recommends testing rate limits in Team Create.
- **Storing non-serializable values** → only JSON-serializable data persists: numbers,
strings, booleans, and tables with string/number keys. `Instance`s, `Vector3`,
`CFrame`, and functions do not — serialize them to plain tables first.
- **Testing without API access** → DataStore requests in Studio error (writes report
`StudioAccessToApisNotAllowed`) until *Enable Studio Access to API Services* is on (and
they don't work from a `LocalScript`).
- **`DataStoreKeyInfo` is nil for ordered stores** → `OrderedDataStore` doesn't
support versioning/metadata; use a regular `DataStore` when you need those.
## References
- For session locking (preventing duplicate data across servers), versioning/
metadata with `DataStoreSetOptions`, ordered-store pagination
(`AdvanceToNextPageAsync`), the key error codes and request limits, and
Right-to-be-Forgotten compliance, read `references/sessions-and-limits.md`.
## Related skills
- `roblox-luau` — services, instances, events, and the server/client model.
- `save-systems` — engine-agnostic serialization, slots, and migration.
© gamedev-skills · Apache-2.0 · gamedev-skills/awesome-gamedev-agent-skills
Similar skills
Everything for Game dev and 3D: Unity, Unreal, Godot, Blender →roblox-datastores by other authors comes first.