About scenario-roblox-luau
A Roblox Luau engineering skill for developers working in Roblox Studio with the scenario-roblox-expert toolkit. It emphasizes Deferred-safe behavior, strict typing, cleanup ownership, and measured optimization, with audit, typecheck, probe, and benchmark procedures.
Key features
- 01Audit scheduler and event usage
- 02Typecheck with both Luau solvers
- 03Track cleanup and resource leaks
- 04Probe behavior in playtests
- 05Benchmark native and parallel code
- 06Measure buffers before adoption
Use cases
- 01Find Deferred event handling issues
- 02Check strict types across solvers
- 03Verify cleanup after player or object removal
- 04Measure whether optimizations help
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ scenario-labs/ skills --skill scenario-roblox-luauAfter installing, type/scenario-roblox-luauin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/critique.mdexpert-notes.mdgui-paths.mdprocedures.mdsources.mdscripts/rbx_luau.pyscripts/agentkit/luau/Bench.luauBufferCodec.luauLeak.luauLuauAudit.luaulune_luau_audit.luauParallel.luauProbe.luauscripts/agentkit/luau/templates/Class.luauParallelWorker.server.luauDetails
From the project READMERoblox Luau Engineering
Roblox Studio (October 2026 build) on macOS, driven through the scenario-roblox-expert toolkit.
Expert Luau here means code that stays correct under Deferred events and the task scheduler, type-checks on both solvers, releases everything it creates, and earns each optimization with a number measured on this build. Prove behavior in a playtest data model instead of reasoning from Immediate-era habits.
If a sibling skill named here is missing from your available skills, ask the user to install it
(npx skills add scenario-labs/skills --skill <name>); unattended, proceed from tool schemas and
flag the gap.
REQUIRED BACKGROUND: scenario-roblox-expert (channels, review loop, toolkit, current-Roblox traps).
Stance (the expert delta)
task.spawnis not concurrency. It runs the body up to its first yield before the caller continues (Crusherfire). Usetask.deferfor "after this step": a queue that keeps call order.task.wait()andtask.delay(0)resume at the next Heartbeat: the same frame from PreSimulation, the next frame from Heartbeat (Crusherfire; measured 0.07 ms vs 16.4 ms).- Write Deferred-safe code. Handlers run at the next resumption point, never inside
:Fire()or a property set (deferred events docs). Nested fires run in queue order, a listener connected inside a handler misses fires from the same step, andDestroyingorCharacterRemovinghandlers seeParentnil (Crusherfire; all measured). Read state inside the handler, use:Once(), and capture what cleanup needs beforeDestroy. Deferred alone cut one game's frame time from 6 to 8 ms to 4 to 5 ms (Drew Hamilton, RDC 2026). - Yield only where it is legal and cheap. Never yield in
__indexor other metamethods,table.sortcomparators orgsubcallbacks: "attempt to yield across metamethod/C-call boundary". Never yield in per-frame handlers:task.wait(0.25)in Heartbeat stacked 16 threads in 1 s [added, measured].pcallis no fix: a failedtask.waitunderpcallleft its thread registered with the scheduler [added, measured]. - Type for both solvers. New modules start
--!strict. Define methods asfunction Class.name(self: Class, ...): colon methods let field typos through on both solvers [added, tested]. Prefer composition: typed metatable inheritance raises false errors on both. Narrow withIsAorFindFirstChildOfClassinstead of:: any. Gate on the old and the new solver, because strict scripts followWorkspace.UseNewLuauTypeSolver, which scripts cannot read. - Every object owns its cleanup.
Destroyis idempotent and disconnects every connection it made (Roblox Learn, OOP). Tables keyed by player are cleared onPlayerRemoving. Old characters are destroyed: at the Default setting a respawned character was unparented but kept its connections (measured). A leak is memory that never comes down; growth alone is not (Roblox Learn, memory leaks). - Parallel Luau computes; serial code writes. One Actor per independent unit,
requirebefore the parallel phase, writes aftertask.synchronize()(docs; Suphi Kaner; Stewiepfing). Each Actor is its own Luau VM, so module state is not shared. Read-modify-write on a SharedTable goes throughSharedTable.incrementorupdate: a plain+= 1from 8 Actors lost 79% of updates (measured). - Native code and buffers only after measurement.
--!nativewins on typed numeric code: 3.3x on a number loop, 4.9x on a typedVector3loop, nothing when theVector3parameter is untyped (measured; docs say annotate). Native everywhere costs startup and a global memory budget (Roblox Learn, native). Buffers pay past about 50 KB/s sustained (Crusherfire), with range checks because integer writes wrap silently. - The injected chunk is not the game. An
execute_luauchunk on the Server has its own module cache: it sawCounter.n = 0where game Scripts saw 41 [added, measured]. Read game state through attributes or a BindableFunction hook.
Establish first
- Signal mode: Workspace > SignalBehavior. Scripts cannot read it; check the Properties panel,
the Rojo project file, or
rbx_luau.audit(place).Defaultmeans Immediate. A Rojo-built place without the property is Default. - Server Authority:
Workspace.AuthorityMode = Serverforces Deferred and fixed-step simulation; game logic then belongs to scenario-roblox-gameplay. - Type setup:
Workspace.LuauTypeCheckMode,UseNewLuauTypeSolver, and whether a Rojo project gives a sourcemap. - Where code runs: Server, Client, or under an Actor. Native code helps server scripts only.
- Baseline numbers before any optimization: ms per frame of the hot path, KB/s on the wire, heap slope per cycle.
- PlayerCharacterDestroyBehavior (Properties panel; not scriptable).
- Defaults when unknown: Deferred,
--!strict, both solvers gated, no native, no Actors, no buffers.
Procedure
- Audit what exists. Goal: find scheduler, yield, deferred, parallel, native and leak mistakes
before a playtest. How: offline
rbx_luau.audit("place.rbxl" or "src/", "warn")(Lune); in Studiorbx_luau.run(s, chunk, dm="Edit", kit=["LuauAudit"])withLuauAudit.tree(game); the toolkit'sAuditcovers legacy APIs. GATE: 0 error findings; every warn fixed or justified in one line. - Fix the idioms. Goal: current scheduler and event idioms. How:
task.*only;PreSimulation,PostSimulation,PreRenderinstead ofSteppedandRenderStepped;:Once(); yields out of frame handlers (busy flag, or one spawned worker loop); metamethods that never yield (precompute, or an explicitgetthat may yield). GATE: audit clean; behavior unchanged under Deferred (step 5). - Type pass. Goal: errors caught at edit time on both solvers. How:
--!strict, aFieldstype plustypeof(setmetatable({} :: Fields, Class)), dot-typedself, composition,table.create(n, value); thenrbx_luau.typecheck(paths, sourcemap=rbx_luau.sourcemap(project)). GATE: 0 errors inoldandnew. - Ownership and cleanup. Goal: nothing outlives its owner. How: the
class template (
Destroywired toDestroying);PlayerRemovingcleanup; character destruction. Prove it in a Server playtest withLeak.retained(make, release, n)andLeak.sample(cycle, 20)plusLeak.slope. GATE:alive0 after release; heap slope flat over 20 or more cycles; 0 connections left afterDestroy. - Probe behavior that depends on order. Goal: facts from this place, not assumptions. How:
Probe.scheduler(),Probe.delayTiming(),Probe.signals()throughrbx_luau.run(..., dm="Server")anddm="Client"in one playtest (procedures P3). GATE: results recorded; no code depends on an order the probe shows Deferred changes. - Optimize only with a number. Goal: keep only measured wins. How:
Bench.run({plain=..., native=...}, 7)in the Server data model (interleaved rounds, warm-up, median);Parallel.workerswith 1 vs N Actors and a work module;BufferCodecsize vs the table payload. GATE: native ratio 1.2 or more [added threshold], parallel speedup 1.5 or more [added threshold], buffers past the bandwidth gate; otherwise revert. - Hand off with evidence. Goal: a reviewer can trust the result without rerunning it. How:
attach typecheck output, audit JSON, probe and bench results with build, data model and date; a
playtest capture through
rbx_reviewwhen the change is visible. GATE: every claim in the handoff has a number or a test name.
Numbers
| Value | Measured or stated | Relative to |
|---|---|---|
task.delay(0) resume | 0.07 ms from PreSimulation, 16.42 ms from Heartbeat | Studio 0.743 Server, 60 Hz |
| Yielding Heartbeat handler | 16 concurrent threads in 1 s | task.wait(0.25) per call |
| Deferred frame-time gain | 6 to 8 ms down to 4 to 5 ms | one shipped game (RDC 2026) |
| Native speedup, numeric loop | 3.3x | plain Luau, Server, n = 200,000 |
Native speedup, typed Vector3 loop | 4.9x; untyped 0.96x | same kernel without --!native |
| Native size limits | 64K instructions per block, 32K blocks per function, 1M per script | docs; plus a global cap |
| Parallel speedup | 7.6x (144.5 ms to 19.0 ms) | 1 vs 8 Actors, 32 pure-compute jobs, 18-core Mac |
SharedTable plain += 1 | 127,107 of 160,000 updates lost | SharedTable.increment lost 0 |
| Buffer gate | about 50 KB/s sustained inbound | Crusherfire |
| Number over a remote | 9 bytes; Color3 13 vs 5 packed | Crusherfire |
| SharedTable keys | strings or integers 0 to 2^32 - 1 | SharedTable reference |
| Memory budget | about 6.25 GB server; about 2 GB phone with 700 MB used by the OS | Roblox Learn, memory leaks |
| Re-entrancy limit | 10 (docs); a self-refiring handler reached 50 without error | Deferred, BindableEvent |
Quality gates
Measurable:
rbx_luau.typecheck: 0 errors on both solvers, with the project sourcemap.rbx_luau.audit: 0 errors; warns resolved.- Toolkit
Audit: no legacy scheduler or deprecated API findings. Leak.retainedalive 0 and a flatLeak.slopeafter every cleanup fix.- Probe results for the place's signal mode recorded on Server and Client.
- Bench and Parallel ratios recorded before keeping native code or Actors.
BufferCodecround trip exact (or within f32 precision) and refusals tested.
Visual (when behavior is visible): a playtest capture through rbx_review showing the object
moving, cleaned up, or the state you claim; for profiling claims, a MicroProfiler or Script Profiler
capture taken by hand (GUI paths).
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
wait(), spawn(), delay() | Throttled, late resumes | task.wait, task.spawn, task.delay |
Logic after :Fire() that expects the handler ran | Stale state under Deferred | Act inside the handler, or task.defer |
Destroying handler reads Parent | nil errors | Capture before Destroy |
task.wait in a Heartbeat handler | Thread pile-up, doubled work | Busy flag, or one worker loop |
Yield in __index or a sort comparator | "attempt to yield across metamethod/C-call boundary" | Precompute; explicit getter |
function C:m() in strict code | Field typos pass | function C.m(self: C) |
(x :: any).Child | Typos pass the checker | Narrow with IsA, FindFirstChildOfClass |
| Typed inheritance chain | "does not have key" on valid calls | Composition |
players[player] = ... without PlayerRemoving | Server memory climbs per join | Clear on PlayerRemoving |
ConnectParallel outside an Actor | Error; handler never runs | Parent the script under an Actor |
require or Instance writes after desynchronize | "not safe to write in parallel" | Require first; write after synchronize |
task.synchronize() inside pcall | Place crash in one run (cause not isolated) | Keep phase switches out of pcall |
--!native on untyped Vector3 code | No speedup | Annotate parameters |
buffer.writeu8(b, 0, 300) | Stores 44 | Range-check, or BufferCodec |
collectgarbage("collect") | Error in Roblox | gcinfo() |
Handoffs
- From scenario-roblox-architecture: module layout, cleanup library choice (Trove, Janitor, Maid), signal modules. This skill reviews the Luau inside them.
- To scenario-roblox-networking:
BufferCodecschemas and measured payload sizes; remotes, rate limits and wire formats stay there. - To scenario-roblox-performance: Bench and Parallel results, native candidates, leak slopes; frame budgets and profiling sessions stay there.
- From and to scenario-roblox-gameplay: fixed-step and Server Authority code; this skill supplies scheduler and Deferred rules.
- From scenario-roblox-data: session state keyed by player; this skill checks
PlayerRemovingcleanup. - To scenario-roblox-tooling: the type gate and
lune_luau_audit.luaufor CI.
Current Roblox notes
- Deferred is the template default;
Defaultstill maps to Immediate; Server Authority forces Deferred.SignalBehavioris NotScriptable. - New type solver: general release 2025-11-20 for nonstrict and nocheck; strict follows
UseNewLuauTypeSolver(NotScriptable). The old solver stays through 2026. wait,spawn,delayare deprecated globals;SteppedandRenderSteppedare superseded.- Native code generation runs in server scripts;
debug.dumpcodesize()works only from the Command Bar (Server view). vector.createreturns aVector3in Roblox; buffers serialize to JSON as{"m":null,"t":"buffer","base64":...}.collectgarbageaccepts only"count"in Roblox scripts.
References
- references/expert-notes.md: principles by topic with sources, timestamps and live results.
- references/procedures.md: P1 to P10 with full code, test paths and results.
- references/critique.md: self-review rubric.
- references/gui-paths.md: Properties, profilers and Command Bar paths for the same work.
- references/sources.md: experts, docs and tools with URLs.
- scripts/rbx_luau.py:
typecheck(both solvers),sourcemap,audit(Lune),runwith kits. - scripts/agentkit/luau/LuauAudit.luau: static rules; lune_luau_audit.luau runs them offline.
- scripts/agentkit/luau/Probe.luau: scheduler, signal, yield and pile-up probes.
- scripts/agentkit/luau/Leak.luau: retention and heap-slope checks.
- scripts/agentkit/luau/Bench.luau: interleaved A/B timing.
- scripts/agentkit/luau/Parallel.luau with templates/ParallelWorker.server.luau: Actor workers.
- scripts/agentkit/luau/BufferCodec.luau: range-checked buffer schemas.
- scripts/agentkit/luau/templates/Class.luau: typed
class with idempotent
Destroy.