/roblox-datastores
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
$ npx -y skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/roblox-datastores
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
roblox-datastores.SKILL.mdname: 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.** File → Game Settings → Security → *Enable Studio Access to API Services* (use a test place; Studio hits live data). 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)
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)
-- 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).
local function savePlayer(player)
if player:GetAttribute("DataLoaded") == false then return end -- never overwrite on a bad load
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
end3. Save on leave AND on shutdown
Players.PlayerRemoving:Connect(savePlayer)
-- BindToClose runs when the server shuts down; save everyone still in.
-- It has a limited time budget, 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)
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)5. Increment a counter
-- 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
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
endPitfalls
- **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 clo
Read more
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.** File → Game Settings → Security → *Enable Studio Access to API Services* (use a test place; Studio hits live data). 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)
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)
-- 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).
local function savePlayer(player)
if player:GetAttribute("DataLoaded") == false then return end -- never overwrite on a bad load
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
end3. Save on leave AND on shutdown
Players.PlayerRemoving:Connect(savePlayer)
-- BindToClose runs when the server shuts down; save everyone still in.
-- It has a limited time budget, 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)
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)5. Increment a counter
-- 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
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
endPitfalls
- **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 clo
<img src="docs/assets/banner.png" width="820" alt="awesome-gamedev-agent-skills — game-dev skills for AI coding agents.
Repo: gamedev-skills/awesome-gamedev-agent-skills
Other skills on awesome-gamedev-agent-skills.
- /audio-design
Implement game audio practice — bus/mixer architecture and gain in decibels, ducking (sidechain), adaptive/dynamic music via layering and re-sequencing, SFX variation, and beat synchronization. Engine-neutral. Use when the user mentions audio mixing, audio buses,
Open skill - /camera-systems
Build game cameras that feel good — 2D follow with a deadzone, look-ahead, smoothing, and level-bounds clamping; 3D third-person orbit with collision and first-person look; plus multi-target framing and a shake hook. Engine-neutral techniques that pair with the engine's camera
Open skill - /create-game-assets
Plan, generate, source, normalize, and validate cohesive visual game assets. Use for art direction, style bibles, sprites, tilesets, backgrounds, UI art, icons, textures, concept art, or 3D asset briefs.
Open skill - /dialogue-systems
Build branching dialogue and narrative — a node/choice graph with conditions, variables, and localization hooks — and choose between authoring tools Ink and Yarn Spinner or a custom data-driven runner. Engine-neutral. Use when the user mentions dialogue system, branching
Open skill - /game-ai
Design NPC and enemy decision-making with finite state machines, behavior trees, steering behaviors, and A* pathfinding — engine-neutral algorithms that pair with the detected engine's navigation API. Use when building enemy AI, an FSM or behavior tree, steering/flocking, or
Open skill - /game-feel
Add "juice" and game feel that makes actions satisfying — screen shake, hit-stop/freeze frames, tweened/eased motion, squash & stretch, knockback, and layered audio-visual feedback — as engine-neutral techniques that pair with the detected engine's tween, particle, and camera
Open skill

