From roblox-agent-skills
How Roblox behaves at runtime. Use whenever writing, reviewing, or debugging server/client Roblox code that touches services, Instances, replication, RemoteEvents/Functions, character lifecycle, physics, DataStore, or platform caveats. Engine layer only — language rules live in luau-expert; UI libraries live in roblox-ui; Rojo/Wally workflow lives in roblox-toolchain.
How this skill is triggered — by the user, by Claude, or both
Slash command
/roblox-agent-skills:roblox-devThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
How the Roblox platform behaves at runtime. Sources: [create.roblox.com/docs](https://create.roblox.com/docs) for APIs and concepts; [devforum.roblox.com](https://devforum.roblox.com) for engineer clarifications and RFCs.
examples/character-lifecycle.luauexamples/connection-tracker.luauexamples/datastore-session.luauexamples/remote-handler.luauexamples/services-header.luaureferences/buffer-library.mdreferences/cloud-services.mdreferences/datastore-rules.mdreferences/parallel-luau.mdreferences/performance-optimization.mdreferences/query-descendants.mdreferences/replication-model.mdreferences/streaming-enabled.mdHow the Roblox platform behaves at runtime. Sources: create.roblox.com/docs for APIs and concepts; devforum.roblox.com for engineer clarifications and RFCs.
For deep dives see:
references/replication-model.md — what replicates from where, per-property rules, deferred-signal timingreferences/cloud-services.md — picker for data stores / memory stores / configs / secrets / in-memoryreferences/datastore-rules.md — rate limits, retry/backoff, session lockingreferences/streaming-enabled.md — chunk semantics, ModelStreamingMode, client-side rulesreferences/query-descendants.md — full Instance:QueryDescendants selector grammarreferences/buffer-library.md — full buffer library APIreferences/parallel-luau.md — actor model, ConnectParallel, SharedTable, thread safetyreferences/performance-optimization.md — full pitfall + mitigation listThis skill describes preferred patterns; it does not authorize rewriting existing code. Apply rules to new code and code the user explicitly asks to refactor. Match the codebase's existing style (casing, indentation, quote style, module shape) before applying skill defaults. Don't introduce tooling the project doesn't already use; surface conflicts once and default to the existing pattern.
Full rules: ../../shared/integration-policy.md.
GetService Everythinglocal ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
game:GetService("Name") always. Not game.ServiceName.
GetService creates them; dot-indexing throws.RunService is internally "Run Service").Workspace via GetService over the lowercase workspace global. Both work; the global predates GetService and exists for compatibility. Sticking to GetService keeps every service access one consistent shape and surfaces typos as undefined-variable errors.GetService calls at the top of the file, alphabetical, no others scattered later.game. Services are top-level children. Everything else is an Instance.Parent = nil AND no script holds a reference. Setting Parent = nil is not destruction. Use :Destroy() to disconnect events, mark it as destroyed, and set Parent to nil.:Destroy() is irreversible. A destroyed Instance can't be re-parented; further property access throws.:Clone() requires Archivable = true (the default). Cloning a model includes its descendants; references between siblings are NOT rebound.:GetChildren() — direct children only.
:GetDescendants() — full subtree, unfiltered. Cheap for small trees, costly for large ones (e.g. a streamed Workspace).
:FindFirstChild(name) / :FindFirstChild(name, true) — second arg = recursive.
:FindFirstChildOfClass(class) / :FindFirstChildWhichIsA(class) — class-specific direct child.
:QueryDescendants(selector: string) → { Instance }. Engine-side filtered descendant query with a CSS-like selector grammar (ClassName, .Tag, #Name, [prop = val], [$attr], [$attr = val], combinators > >> ,, pseudo-classes :not(...) :has(...)). Prefer this over GetDescendants + Luau-side filter for large trees. Thread-unsafe. Full grammar + examples in references/query-descendants.md.
Workspace:QueryDescendants("MeshPart.SwordPart") -- MeshParts tagged SwordPart
Workspace:QueryDescendants("Model >> [$OnFire = true]") -- attribute-matching descendants of any Model
Workspace:QueryDescendants(":not(SpotLight, PointLight)") -- excluding lights
WaitForChild RulesReplicatedStorage, Workspace) may not exist on the first frame. Use :WaitForChild("Name") for anything you didn't put there yourself.:FindFirstChild is enough.WaitForChild without a timeout in production code — it yields forever if the child never arrives. :WaitForChild("Name", 5) returns nil after 5 seconds.Three script types:
Script — runs on server or client depending on Script.RunContext and location.
RunContext = Legacy (default) → server-only, only runs in server containers (ServerScriptService, Workspace, etc.).RunContext = Server → server, can run from ReplicatedStorage too (but don't — clients can read source).RunContext = Client → client, runs from ReplicatedStorage. Recommended for new client scripts (clearer than LocalScript).LocalScript — client-only. No RunContext. Runs only in client containers: StarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack, ReplicatedFirst.ModuleScript — require()-able from either side. Runs in the requirer's context.| Where | What goes here | RunContext |
|---|---|---|
ServerScriptService | Server scripts + server-only ModuleScripts | Server |
ServerStorage | Server-only data (assets, tables) — clients never see contents | n/a |
ReplicatedStorage | Shared ModuleScripts (server + client) and client scripts | Client for client scripts |
ReplicatedFirst | Minimal client loader (e.g. loading screen) | Client |
StarterPlayerScripts / StarterCharacterScripts / StarterGui / StarterPack | LocalScripts that run per-player | n/a |
Workspace | World content. Server-side Scripts for object behaviors. | Server if you want explicit |
Common practice: keep most code as ModuleScripts in ReplicatedStorage (shared) or ServerStorage/server-only spots, with one server entry-point Script in ServerScriptService that requires the modules, and one client entry-point in ReplicatedStorage (RunContext = Client).
ReplicatedStorage. Clients can read source; assume anything there is leaked.RunContext explicitly on scripts inside models or packages — removes ambiguity when the model is reparented or shared.RunService:IsServer() / :IsClient() for runtime branching in shared ModuleScripts. Don't infer from script.Parent.LocalScripts in Workspace don't run. Client-runnable containers only. Common bug.Workspace run, but RunContext = Server in ServerScriptService is clearer and less surprising when geometry moves.Source: create.roblox.com/docs/scripting/locations.
Workspace, ReplicatedStorage, Players[X].PlayerGui, StarterGui-cloned-to-PlayerGui). Property changes and parenting changes replicate.Players[X].Character (filtering era + character ownership), input events, and explicit RemoteEvent calls.Player.PlayerScripts, Player.PlayerGui after initial replication of StarterGui).Parent.Workspace.SignalBehavior = Enum.SignalBehavior.Deferred (the modern default in new places) batches signal firings until the next resumption point — code that assumes synchronous-fire ChildAdded is brittle. Test with deferred signals on.references/replication-model.md for the full table.-- Server
local TakeDamage = ReplicatedStorage:WaitForChild("TakeDamage") :: RemoteEvent
TakeDamage.OnServerEvent:Connect(function(player, amount: unknown)
if typeof(amount) ~= "number" or amount < 0 or amount > 100 then return end
-- validated; apply
end)
-- Client
TakeDamage:FireServer(25)
OnServerEvent first arg is always the firing Player. It's authentic — Roblox provides it. Don't trust anything else.unknown and validate at the boundary. Typing as number makes the type checker accept exploiter input. (See luau-expert skill for the full pattern.)RemoteFunction is :InvokeServer() / :InvokeClient() — synchronous (yields). Avoid client→server InvokeClient when you can; a malicious client can hang the call. Prefer RemoteEvent + reply event.kind field — types stay clearer and validation simpler.local function onCharacterAdded(character: Model)
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
-- ...
end
Players.PlayerAdded:Connect(function(player)
if player.Character then onCharacterAdded(player.Character) end
player.CharacterAdded:Connect(onCharacterAdded)
end)
PlayerAdded fires. Check player.Character and connect to future CharacterAdded.Model. References to the old character become stale. Reconnect every per-character signal in CharacterAdded; use character.AncestryChanged or humanoid.Died to clean up the previous set.Humanoid.Died fires once per character. After death the model lingers (default ~5s, controlled by Players.RespawnTime) before automatic respawn destroys it.Player.CharacterRemoving fires before the model leaves Workspace — last chance to read state from it.Modern places enable Workspace.StreamingEnabled. Parts of the world load and unload around each player.
WaitForChild (with timeout) or nil-check.Model.ModelStreamingMode:
Default — model streams normally, descendants stream independently.Atomic — the whole model streams in/out as a single unit (no partial loads).Persistent — the model is always loaded for every client.PersistentPerPlayer — always loaded for players added via the relevant Add/Remove API; off by default for others.Atomic for objects whose descendants must arrive together (vehicle assemblies, NPC rigs). Use Persistent sparingly — it costs memory on every client.BasePart.CFrame updates on streamed-out objects — wasted bandwidth.references/streaming-enabled.md for chunk semantics and ModelStreamingMode interactions.BasePart has a network owner. Default: server. When a player's character touches/steps on/holds a part, ownership transfers to that player's client.CFrame for game-state decisions.part:SetNetworkOwner(player) — pass nil to give it back to the server. :GetNetworkOwner() reads it.SetNetworkOwner errors on an anchored part. Check with BasePart:CanSetNetworkOwnership() first if you don't control the input.:SetNetworkOwner(player) or :SetNetworkOwner(nil), auto-transfer is off for that part — ownership stays where you set it. Call :SetNetworkOwnershipAuto() to put it back on auto.SetNetworkOwner(nil) once, never SetNetworkOwnershipAuto().Roblox offers several storage options. Pick by access pattern, not by reflex.
| Service | Persistence | Scope | Use for |
|---|---|---|---|
Standard data stores (DataStoreService:GetDataStore) | Permanent | Cross-server | User progress, inventory, save data. Numbers/strings/booleans/tables. NoSQL-like. |
Ordered data stores (:GetOrderedDataStore) | Permanent | Cross-server | All-time leaderboards (numbers only, sortable). |
Memory stores (MemoryStoreService) | ≤ 45 days | Cross-server | Matchmaking, daily/monthly leaderboards, ephemeral cross-server queues. Faster than data stores. |
| Configs (Creator Hub) | Permanent | Cross-server | Feature flags, tunable values. Read-only from in-game. |
| Secrets stores | Permanent | Cross-server | API keys, third-party tokens. Read-only from in-game. |
| In-memory Luau table | Session | Single server | Temporary state (timers, status effects, current health). Free, instant. |
Cloud services are server-only. Clients can't read DataStores, MemoryStores, etc. Route any client need through a RemoteEvent.
pcall. Reads/writes throw on network failure, throttling, and quota exhaustion.UpdateAsync for concurrent state (not SetAsync) — gives you the previous value and merges atomically. Return nil from the transform to abort without burning quota.PlayerAdded, mutate in-memory for the session, save on PlayerRemoving + BindToClose. Don't read on every change.game:BindToClose has a ~30-second total budget across all players. Save in parallel via task.spawn.See references/cloud-services.md for the full picker (when to use which) and references/datastore-rules.md for data-store rate limits, retry/backoff, and session-locking patterns.
Source: create.roblox.com/docs/cloud-services/data-stores-vs-memory-stores.
The task library replaces classic wait/spawn/delay. Use it.
| Call | When |
|---|---|
task.spawn(f, ...) | Run f on a new thread immediately (resumes after current resumption point). Use to detach a yielding body from an event handler. |
task.defer(f, ...) | Run f after the current resumption cycle finishes — later than spawn. Use to coalesce work without yielding. |
task.delay(t, f, ...) | Run f after t seconds on a new thread. Replaces the legacy delay. |
task.wait(t) | Yield current thread for t seconds (or one frame if omitted). Replaces wait. |
task.cancel(thread) | Kill a thread started by task.spawn/task.delay. Pair with explicit cleanup. |
task.synchronize() / task.desynchronize() | Parallel-Luau actor coordination. See the Parallel Luau section below. |
wait, spawn, delay. They throttle (wait clamps to ~30 Hz) and leak in patterns the task versions don't.task.spawn inside an event handler lets fast-firing events run concurrently. Add your own queue/lock if the body mutates shared state.task.defer before task.spawn when you want the work to happen after the current event-handling pass — useful inside :Connect bodies that mutate the world and trigger more signals.task.spawn/task.delay if they need cancellation; nothing auto-cancels on Instance destruction.buffer is Luau's fixed-size mutable byte block. Source: buffer.yaml in creator-docs.
Use it for:
string.pack / string.unpack use cases.local b = buffer.create(16) -- 16-byte buffer, zero-initialized
buffer.writeu32(b, 0, 0xCAFEBABE) -- 4 bytes at offset 0
buffer.writef32(b, 4, 1.5) -- 4 bytes at offset 4
buffer.writestring(b, 8, "abc") -- 3 bytes at offset 8
local magic = buffer.readu32(b, 0)
local payload = buffer.readstring(b, 8, 3)
Key rules:
buffer.create rejects larger; allocations near that may fail anyway).buffer.copy).Functions in three groups:
buffer.create(size), buffer.fromstring(s), buffer.tostring(b), buffer.len(b).readi8/u8/i16/u16/i32/u32/f32/f64 and matching write*.readbits(b, bitOffset, bitCount) / writebits (0–32 bits per call), readstring(b, offset, count) / writestring(b, offset, value, count?), buffer.copy(target, targetOffset, source, sourceOffset?, count?), buffer.fill(b, offset, value, count?).Full signature reference in references/buffer-library.md.
By default Luau is single-threaded. Parallel Luau lets independent work run on multiple OS threads via the Actor model. Use it for CPU-bound work that can be split: per-player raycast validation, procedural generation, large per-instance computations.
Actor Instance are eligible to run in parallel.Code runs serially by default. Switch in/out:
-- Inside an Actor's script
RunService.Heartbeat:ConnectParallel(function()
-- parallel phase
local result = computeExpensive()
task.synchronize()
-- serial phase
applyResult(result)
end)
task.desynchronize() — switch current thread to parallel. Yields, resumes at next parallel-execution opportunity.task.synchronize() — switch back to serial. Required before mutating most Instances.Signal:ConnectParallel(callback) — schedule a signal callback to run in parallel automatically; no need for desynchronize inside.require() is not allowed in a parallel phase. Require modules in serial first.API members carry a thread-safety tag:
| Level | Properties | Functions |
|---|---|---|
| Unsafe | No parallel read or write | No parallel call |
| Read Parallel | Read OK, write not | n/a |
| Local Safe | Same-actor any; cross-actor read-only | Same-actor only |
| Safe | Read + write | Call from anywhere |
Default is Unsafe if untagged. Most read-heavy access (geometry queries, raycasts) is safe; most mutations are not. Check the API reference per-member.
Actor:SendMessage(topic, ...) + Actor:BindToMessage(topic, fn) / :BindToMessageParallel(topic, fn) — async one-way messaging. Multiple bindings per topic allowed.SharedTable — table-like structure shared across actors with atomic updates. No copy on send. Use for "common world state not stored in DataModel".task.synchronize'd, defeating the point.Full deep-dive: references/parallel-luau.md. Source: create.roblox.com/docs/scripting/multithreading.
Performance optimization is a cycle: design for it up front, identify hot spots with the MicroProfiler, mitigate, monitor. Below is the high-signal subset; full pitfall list in references/performance-optimization.md.
RunService.Heartbeat plays the same role.RunService.Heartbeat (or PreSimulation/PostSimulation/PreRender/PreAnimation). Heartbeat fires every frame — gate by interval (if now - last < 0.1 then return end) or move work to a slower loop.TweenService. Replicates the tweened property every frame; stutters under latency. Tween on the client unless the result must be authoritative.Player / character that never disconnect. Player Instances aren't auto-destroyed on leave (see Workspace.PlayerCharacterDestroyBehavior to opt in to auto-destroy). Track and disconnect explicitly, or destroy manually in PlayerRemoving.playerInfo[player] = ... on join without playerInfo[player] = nil on leave is a classic server leak.luau-expert skill).buffer.Anchored = true removes them from physics simulation entirely.CollisionFidelity. Precise is expensive in CPU and memory. Use Box/Hull for anything where the player won't notice.PreloadAsync over the entire Workspace for a "no pop-in" loading screen. Preload only what the loading screen and starting area need.--!native) for hot, unyielding compute paths is a low-effort win — verify the function actually runs hot first.RunService.Heartbeat / PreRender / PreSimulation / PostSimulation — your scripts.physicsStepped / worldStep — physics cost.ProcessPackets / Allocate Bandwidth — network cost.updateInvalidatedFastClusters — humanoid/skinned-mesh churn.Tag your own scopes with debug.profilebegin("Name") / debug.profileend().
Sources: create.roblox.com/docs/performance-optimization, /improve.
-- ❌ every CharacterAdded adds another connection; old ones never disconnect
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(char)
char.Humanoid.Died:Connect(function() ... end) -- leaks per respawn
end)
end)
Track and disconnect connections per scope. A common pattern: store { RBXScriptConnection } keyed by the lifecycle owner (player, character, instance), iterate and :Disconnect() on cleanup.
local character = player.Character
task.wait(5)
character.Humanoid.Health = 0 -- ❌ player may have respawned; old model is destroyed
Re-fetch player.Character after any yield, or capture+verify with if character.Parent == nil then return end.
RemoteEvent.OnServerEvent:Connect(function(player)
task.wait(1) -- ❌ all subsequent fires from this player wait behind this one
grantReward(player)
end)
Either don't yield in handlers, or task.spawn the body so each fire runs independently. Be careful with task.spawn — concurrent state updates need a lock.
Instance:Destroy() disconnects events and removes from the DataModel, but doesn't break Lua-side references. Set the variable to nil if it's long-lived.:Connect(...) keep those tables alive as long as the connection exists.-- ❌ tag handler runs once per existing tagged instance, then once per new one
CollectionService:GetTagged("Door"):forEach(setupDoor)
CollectionService:GetInstanceAddedSignal("Door"):Connect(setupDoor)
Always pair with GetInstanceRemovedSignal for cleanup. Track per-instance state in a { [Instance]: State } map, not in closures. Use instance.AncestryChanged (when parent == nil) or the removed signal.
PlayerAdded fires for players already in the server when your script starts (binding via :Connect only catches future fires) — handle existing players manually:
for _, player in Players:GetPlayers() do onPlayerAdded(player) end
Players.PlayerAdded:Connect(onPlayerAdded)
luau-expert for module/file structure rules.)UserId you read off any real Player Instance (the OnServerEvent first arg, Players:GetPlayers(), Players:GetPlayerByUserId) is authentic. A UserId the client sends as a RemoteEvent argument is just a number — validate or ignore.MessagingService alone. It's best-effort, not guaranteed delivery, and rate-limited per-game. Use it for "interesting events" notifications, not for synchronizing state.ServerScriptService/ServerStorage; client logic in StarterPlayerScripts/StarterCharacterScripts/StarterGui; shared modules in ReplicatedStorage. Don't put server-only logic in ReplicatedStorage — clients can read it.Instance.Parent, BasePart.CFrame, Humanoid.Health, etc.). Roblox renames, deprecates, and adds. When unsure, point the user at create.roblox.com/docs and state your uncertainty.Workspace.StreamingMinRadius = 256" — the property may be renamed; the principle won't.Workspace properties, new RunService methods, scripting-related betas): verify before recommending.--!strict behavior, defer to luau-expert. If it's about Rojo or filesystem layout, defer to roblox-toolchain. If it's about UI libraries, defer to roblox-ui.| ❌ Don't | ✅ Do |
|---|---|
game.Workspace / workspace global | game:GetService("Workspace") |
game.Players.PlayerAdded:Connect(f) only | Iterate :GetPlayers() then :Connect for joins-during-startup |
child = parent:FindFirstChild("X") on client | :WaitForChild("X", timeout) |
| Trust client RemoteEvent args by type annotation | Type as unknown, validate, narrow |
RemoteFunction:InvokeClient | RemoteEvent round-trip |
One Remote multiplexing many actions via kind field | One Remote per action |
SetAsync for concurrent state | UpdateAsync |
DataStore call without pcall | Always pcall + retry/backoff |
| Read DataStore on every change | Session-cached; write on PlayerRemoving + BindToClose |
| Store character ref across yields | Re-fetch player.Character after any yield |
| Connect-without-disconnect across respawns | Track + disconnect connections per character |
Instance.Parent = nil for cleanup | :Destroy() |
WaitForChild without timeout | :WaitForChild("X", 5) and handle nil |
| Client-side validation only | Always validate on server |
| Yield inside an event handler that fires often | task.spawn the body; serialize via your own queue |
| Auto network ownership for sensitive parts | Explicit SetNetworkOwner(nil) |
pairs over a tag set without removed-signal pair | Pair GetInstanceAddedSignal with GetInstanceRemovedSignal |
Server logic in ReplicatedStorage | ServerScriptService / ServerStorage |
LocalScript in Workspace | Client containers (StarterPlayerScripts, etc.) or Script with RunContext = Client in ReplicatedStorage |
wait / spawn / delay legacy globals | task.wait / task.spawn / task.defer / task.delay |
Heavy work directly on RunService.Heartbeat | Throttle / move to slower loop / task.delay |
Server-side TweenService for visual tweens | Tween on the client; replicate target only |
| Cloning large rigs at runtime per event | Pool / pre-instance |
CollisionFidelity = Precise on small parts | Box / Hull |
| Mutating Instances inside a parallel phase | task.synchronize() first |
| Storing API keys in DataStores | Secrets stores |
| Save data in MemoryStore | Standard data stores |
examples/services-header.luau — canonical service-import block.examples/character-lifecycle.luau — joining, character add/remove, per-character cleanup.examples/remote-handler.luau — RemoteEvent server handler with validation and rate limiting.examples/datastore-session.luau — load on join, session-cached, save on remove + BindToClose.examples/connection-tracker.luau — disposable scope for batched :Disconnect().Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub afrxo/roblox-agent-skills --plugin roblox-agent-skills