From genres
Persists player data in Roblox using DataStoreService: load on join, save on leave/shutdown, pcall-guarded reads/writes, retries, and OrderedDataStore leaderboards.
How this skill is triggered — by the user, by Claude, or both
Slash command
/genres:roblox-datastoresThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Persist data across sessions in Roblox with `DataStoreService`: loading on join,
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.
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.
Scripts, never LocalScripts.DataStoreService:GetDataStore("Name");
key per player is usually "Player_" .. player.UserId.pcall. GetAsync/SetAsync/UpdateAsync are network
calls that can fail; an unguarded failure errors the thread and risks data loss.PlayerAdded, save on PlayerRemoving, and also BindToClose. A
leaving player and a shutting-down server both need a final save.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.OrderedDataStore for ranked data (leaderboards) via GetSortedAsync.
Test by joining, changing data, rejoining, and confirming it persisted.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)
-- 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
end
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)
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)
-- 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)
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
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.UpdateAsync callback → the callback can't call
task.wait or other Async functions; compute the new value beforehand and return it.BindToClose save → players in the server at shutdown lose unsaved progress;
add game:BindToClose and wait for saves to finish within its budget.GetAsync is
cached briefly, so immediate re-reads may be stale.Instances, Vector3,
CFrame, and functions do not — serialize them to plain tables first.LocalScript).DataStoreKeyInfo is nil for ordered stores → OrderedDataStore doesn't
support versioning/metadata; use a regular DataStore when you need those.DataStoreSetOptions, ordered-store pagination
(AdvanceToNextPageAsync), the key error codes and request limits, and
Right-to-be-Forgotten compliance, read references/sessions-and-limits.md.roblox-luau — services, instances, events, and the server/client model.save-systems — engine-agnostic serialization, slots, and migration.npx claudepluginhub gamedev-skills/awesome-gamedev-agent-skills --plugin workflowsScripts a Roblox experience in Luau: services, Instances, events, server/client split, and RemoteEvents. Activates when user mentions Roblox, Luau, or common Roblox API patterns.
Provides expert guidance on Roblox game development: Lua scripting, Roblox Studio, game systems, monetization, and building experiences. Activates on Roblox-related queries.
Implements Redis patterns for games using sorted sets for leaderboards, hashes for sessions, pub/sub for messaging, rate limiting, and ephemeral state with TTLs.