Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores -a claude-code
Open on GitHub

What 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 SetAsync with UpdateAsync so two servers stop clobbering a key
  • Build a coin leaderboard on OrderedDataStore
  • Wrap network calls in pcall and add retries

Example prompts for the agent

  • Write a server Script that loads the player's coins with GetAsync inside pcall and saves them on PlayerRemoving.
  • Add a coin leaderboard using OrderedDataStore and print the top 10 with GetSortedAsync.
  • Progress got wiped on server restart — add a BindToClose save 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-code

2Manually: put the files in place

~/.claude/skills/roblox-datastores/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/roblox-datastores/

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.

From the same set77

game-jam Plans and scopes game development projects under tight jam deadlines, keeping scope locked to the clock so a playable build gets submitted. CX Codex +1 5,700installs unity-animation Guides the agent through Animator Controllers in Unity 6.3 LTS: parameters, transitions, blend trees, layers, humanoid IK. Useful when one state machine has to cover walking, jumping and aiming. CX Codex +1 5,800installs threejs-gltf-loading Loads glTF/GLB models into three.js and plays their baked or skinned clips, with patterns for DRACO, Meshopt and KTX2. For the case where the file imports but nothing shows up on screen. CX Codex +1 5,600installs unity-physics Sets up 3D PhysX physics in Unity 6.3 LTS with proper Rigidbody motion, collision layers, and raycasts. Useful when writing character controllers or tuning physics interactions. CX Codex +1 5,600installs unity-input-system A Claude Code skill that wires player input in Unity 6.3 LTS with the Input System package: action maps, PlayerInput, values read in C#. CX Codex +1 5,700installs unity-scriptableobjects Architects data systems and event channels in Unity 6.3 LTS using ScriptableObjects. Helps decouple gameplay logic without static singletons. CX Codex +1 5,700installs phaser-arcade-physics Configures movement, gravity, and collisions in Phaser 4 games using Arcade Physics. Helps write top-down or platformer controls and resolve body interactions. CX Codex +1 5,500installs threejs-scene-setup Sets up the foundation of a three.js scene and helps diagnose a blank or black canvas. Use it for rendering loops, responsive sizing, camera setup, or OrbitControls. CX Codex +1 5,500installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor