roblox-luau
Skill92 starsChanged 47 days agoMITView on GitHub
Modern, type-safe Luau for Roblox, covering --!strict and the new type solver, generics, string interpolation, if-expressions, task library, require-by-string, module/OOP patterns, error handling with retries, buffer/vector, and legacy APIs to avoid. Use when writing, refactoring, or explaining any Roblox script, fixing type errors, or modernizing old Lua code.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- gamedev-skills/awesome-gamedev-agent-skillstheir own version, first seen Oct 23, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- gamedev-skills/awesome-gamedev-agent-skills★ 6.4ksame name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository25 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/agents.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- roblox-analytics-liveopsSkill
- roblox-architectureSkill
- roblox-audioSkill
- roblox-characters-animationSkill
- roblox-code-reviewSkill
- roblox-data-storesSkill
- roblox-game-systemsSkill
- roblox-inputSkill
- roblox-monetizationSkill
- roblox-networkingSkill
- roblox-npc-aiSkill
- roblox-open-cloudSkill
- roblox-performanceSkill
- roblox-physicsSkill
- roblox-securitySkill
- roblox-studio-mcpSkill
- roblox-teleport-matchmakingSkill
- roblox-testingSkill
- roblox-text-chatSkill
- roblox-toolingSkill
- roblox-uiSkill
- roblox-world-buildingSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 3 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 16
Use the task library: task.wait, task.spawn, task.defer, task.delay, task.cancel.
- Very specific: line 17
Never wait, spawn, delay (deprecated, throttled, imprecise).
- Very specific: line 19
Prefer the *Async variants of APIs.
- Very specific: line 97
Avoid any except at trust boundaries (remote payloads, decoded JSON) — validate there, then narrow.
- Distinctive: line 121
Never create circular requires.
- Very specific: line 122
Don't yield at module top level (no WaitForChild chains on things that may never exist, no DataStore calls): every requirer blocks until …
- Distinctive: line 146
Prefer plain modules of functions + data tables over deep inheritance.
- Very specific: line 178
Don't wrap everything in pcall — only calls that can fail for reasons outside your code.
- Very specific: line 194
Write handlers that work under deferred signal behavior (Workspace.SignalBehavior): handlers run later in the frame, not inline.
- Very specific: line 197
Never yield inside metamethods or Changed-style callbacks that must be synchronous.
- Very specific: line 225
Use :Once() for one-shot connections.
- Distinctive: line 270
Use it on math-heavy modules, not everywhere (compile time + memory limits).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Modern Luau for Roblox
Luau is Roblox's gradually-typed Lua 5.1 derivative. Engine and language move fast; many patterns from older tutorials (and older training data) are deprecated. Apply these defaults to every script you write unless the project clearly uses another convention.
Defaults for all generated code
--!strictat the top of new ModuleScripts (and Scripts when practical). Type public functions.- Get services with
game:GetService("Name")at the top of the file, nevergame.Name. - Use the
tasklibrary:task.wait,task.spawn,task.defer,task.delay,task.cancel. Neverwait,spawn,delay(deprecated, throttled, imprecise). - Yielding engine calls that can fail (DataStore, HTTP, Marketplace, Teleport,
*Async) go insidepcall. - Prefer the
*Asyncvariants of APIs; Roblox renamed most yielding methods (LoadCharacterAsync,GetProductInfoAsync,UserHasBadgeAsync, ...). See the table below. - Disconnect connections / destroy instances you create (see "Cleanup" below).
- Naming:
PascalCasefor services, modules, classes, types, and instance names;camelCasefor locals and functions;SCREAMING_SNAKE_CASEfor constants. - Tabs for indentation (StyLua default in the Roblox ecosystem), double quotes.
Modern syntax cheat sheet
local Players = game:GetService("Players")
local count = 0
count += 1 -- compound assignment: += -= *= /= //= %= ^= ..=
local label = if count > 1 then "many" else "one" -- if-expression (no `and/or` pitfalls)
print(`Player count: {#Players:GetPlayers()} ({label})`) -- string interpolation
for index, player in Players:GetPlayers() do -- generalized iteration: no pairs/ipairs needed
if player.AccountAge < 1 then
continue -- continue is a keyword
end
print(index, player.Name)
end
local half = 7 // 2 -- floor division → 3
local frozen = table.freeze({ Speed = 16 }) -- read-only table; writes throw
local list = table.create(100, 0) -- preallocated array
local copy = table.clone(frozen) -- shallow copy (unfrozen)
local mid = math.lerp(0, 10, 0.5) -- also: math.map, math.clamp, math.round, math.sign
print(half, list[1], copy.Speed, mid)pairs/ipairsstill work; generalized iteration is preferred and iterates arrays in order.#tis only defined for arrays without holes. Settingt[i] = nilin the middle creates holes.- Numbers are doubles: integers are exact to 2^53. Use
math.floor///for integer math. tick()is discouraged: useos.clock()for benchmarks,workspace:GetServerTimeNow()for synced time across clients/server,os.time()/DateTime.now()for wall-clock UTC.
Types (new type solver)
Roblox's new type solver is generally available (Nov 2025) and Workspace.UseNewLuauTypeSolver
controls it; Workspace.LuauTypeCheckMode sets the default mode. Write code that checks cleanly
under --!strict.
--!strict
export type Item = {
id: string,
name: string,
stack: number,
tags: { string }?, -- optional field
}
type Rarity = "Common" | "Rare" | "Legendary" -- singleton (literal) union
local function totalStack(items: { Item }, filter: ((Item) -> boolean)?): number
local total = 0
for _, item in items do
if filter == nil or filter(item) then
total += item.stack
end
end
return total
end
local function first<T>(list: { T }): T? -- generics
return list[1]
end
local rarity: Rarity = "Rare"
local part = workspace:FindFirstChild("Door") :: BasePart? -- cast with ::
print(totalStack({}, nil), first({ 1, 2 }), rarity, part)- Refine before use:
if part then ... end,if typeof(x) == "Instance" and x:IsA("BasePart") then. typeof(v)knows Roblox types ("Vector3","Instance", ...);type(v)only knows Lua types.- Instance types come from
IsArefinement orFindFirstChildOfClass/FindFirstChildWhichIsA. - Avoid
anyexcept at trust boundaries (remote payloads, decoded JSON) — validate there, then narrow.
Details: generics, type packs, read/write properties, user-defined type functions, typing
OOP classes, and migrating legacy code to strict: references/type-system.md.
Modules and require
ModuleScripts run once per environment (server or each client) and return one value, cached for every
later require. A module in ReplicatedStorage required by both sides runs twice, once per side, with
separate state.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Signal = require(ReplicatedStorage.Packages.Signal) -- instance path (always works)
local Config = require("./Config") -- require-by-string: sibling of this script (script.Parent)
local Utils = require("@self/Utils") -- child of this script
local Shared = require("@game/ReplicatedStorage/Shared") -- absolute from game
print(Signal, Config, Utils, Shared)- String paths:
./=script.Parent,../=script.Parent.Parent,@self/=script,@game/=game. They do not wait for instances to exist. Use instance paths withWaitForChildwhen the module may not have replicated yet. - Never create circular requires; they deadlock/error. Break cycles with dependency injection or events.
- Don't yield at module top level (no
WaitForChildchains on things that may never exist, no DataStore calls): every requirer blocks until the module returns.
Module / class pattern
--!strict
local Counter = {}
Counter.__index = Counter
export type Counter = typeof(setmetatable({} :: { value: number, step: number }, Counter))
function Counter.new(step: number?): Counter
return setmetatable({ value = 0, step = step or 1 }, Counter)
end
function Counter.increment(self: Counter): number
self.value += self.step
return self.value
end
return CounterPrefer plain modules of functions + data tables over deep inheritance. Use composition.
Errors and retries
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")
local function retry<T...>(attempts: number, fn: () -> T...): (boolean, T...)
local delaySeconds = 1
for attempt = 1, attempts do
local result = table.pack(pcall(fn))
if result[1] then
return table.unpack(result, 1, result.n)
end
if attempt < attempts then
warn(`attempt {attempt} failed: {result[2]}`)
task.wait(delaySeconds)
delaySeconds *= 2 -- exponential backoff
end
end
return false
end
local ok, data = retry(3, function()
return store:GetAsync("Player_1")
end)
print(ok, data)error("message", 2)blames the caller;error({ code = "X" })throws a table for structured errors.xpcall(fn, debug.traceback)keeps the stack trace.- Don't wrap everything in
pcall— only calls that can fail for reasons outside your code.
Concurrency: the task library
| Call | Behavior |
task.spawn(fn, ...) | Run now (until first yield) on a new thread. |
task.defer(fn, ...) | Run at the end of the current resumption cycle. Good for "after this finishes". |
task.delay(t, fn, ...) | Run after t seconds. Returns a thread you can task.cancel. |
task.wait(t?) | Yield ≥ t seconds (one frame if omitted); returns elapsed time. |
task.cancel(thread) | Stop a spawned/delayed thread. |
task.desynchronize() / task.synchronize() | Parallel Luau (inside Actors) — see roblox-performance. |
- Per-frame work:
RunService.Heartbeat(after physics),RunService.PreSimulation/PostSimulation,RunService:BindToRenderStep(client, before render, ordered). Don'twhile true do task.wait() endfor per-frame logic; connect to an event and use itsdeltaTime. - Write handlers that work under deferred signal behavior (
Workspace.SignalBehavior): handlers run later in the frame, not inline. New template places useDeferred,Defaultwill switch to it, and server authority requires it. Don't rely on handler side effects being visible right after:Fire(). - Never yield inside metamethods or
Changed-style callbacks that must be synchronous.
Cleanup (memory leaks are the #1 Roblox bug)
local Players = game:GetService("Players")
local connections: { RBXScriptConnection } = {}
table.insert(
connections,
Players.PlayerRemoving:Connect(function(player)
print(`{player.Name} left`)
end)
)
local function cleanup()
for _, connection in connections do
connection:Disconnect()
end
table.clear(connections)
end
cleanup()Instance:Destroy()disconnects that instance's connections and locksParent; it does not clear your Lua references — nil out tables that hold players/instances onPlayerRemoving.- Use
:Once()for one-shot connections. Use a Trove/Janitor/Maid-style helper for groups. - Keys that are Player/Instance objects in long-lived tables leak unless removed on leave/destroy.
Buffers and vectors (performance-sensitive data)
local packet = buffer.create(10)
buffer.writeu16(packet, 0, 513) -- item id
buffer.writef32(packet, 2, 12.5) -- amount
buffer.writei32(packet, 6, -4)
print(buffer.readu16(packet, 0), buffer.readf32(packet, 2), buffer.len(packet))
local v = vector.create(1, 2, 3) -- native 3-float vector (same type as Vector3 at runtime)
print(vector.magnitude(v), vector.normalize(v), vector.dot(v, vector.one))buffer is the right tool for compact network payloads, large grids, and binary serialization.
Standard-library quick reference: references/stdlib-cheatsheet.md.
Legacy → current (most common mistakes)
| Don't write | Write instead |
wait(), spawn(), delay() | task.wait(), task.spawn(), task.delay() |
game.Workspace, game.Players | workspace, game:GetService("Players") |
Instance.new("Part", parent) | create, set properties, then set .Parent last |
Humanoid:LoadAnimation() | Animator:LoadAnimation() |
Model:SetPrimaryPartCFrame() | Model:PivotTo() / Model:GetPivot() |
workspace:FindPartOnRay(Ray.new(...)) | workspace:Raycast(origin, direction, params) |
BodyVelocity, BodyPosition, BodyGyro | LinearVelocity, AlignPosition, AlignOrientation |
player:LoadCharacter() | player:LoadCharacterAsync() |
MarketplaceService:GetProductInfo() | :GetProductInfoAsync() |
BadgeService:AwardBadge() | :AwardBadgeAsync() |
player:GetRankInGroup() | GroupService:GetRolesInGroupAsync() / player:IsInGroupAsync() |
TeleportService:Teleport*() variants | TeleportService:TeleportAsync() |
Chat service / ChatService modules | TextChatService |
GuiObject.Draggable | UIDragDetector |
table.getn, table.foreach, getfenv | #t, for ... in, nothing |
The full deprecation map lives in the roblox-code-review skill.
Performance idioms (only where it matters)
- Measure first (
debug.profilebegin/profileend, MicroProfiler, Script Profiler). --!nativeat the top of a script, or@nativeon a function, compiles hot numeric code natively. Use it on math-heavy modules, not everywhere (compile time + memory limits). Type annotations help it.- Cache
Instancelookups and service references outside loops; avoidFindFirstChildchains per frame. - Build large strings with
table.concatorbuffer, not repeated..in loops. - Reuse
RaycastParams/OverlapParamsobjects instead of creating them per call.
Related skills
roblox-architecture (project structure, client/server), roblox-networking (remotes),
roblox-performance (profiling, Parallel Luau), roblox-code-review (review checklist, full deprecation map).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

