Edge cases
The things that surprise people. Most of these are deliberate, and knowing them up front saves an afternoon.
Files and naming
Loose files at the project root do nothing
Only meta.inst.json and folders holding an init.meta.json compile at the top level. A .luau file sitting alone at the root is ignored with no warning, because a project root is usually full of things that are not game code.
If you want a root file in the tree, point at it with $Path from the project file, which bypasses this rule.
meta.inst.json is the only loose root file that compiles
It is where instances that have no file of their own are declared, mostly $FromModel references to content already in the place. Any other .inst.json sitting at the root is ignored. Deeper in the project the name is not special and any .inst.json works.
Entries in meta.inst.json need $Name and $Parent
There is no folder around them to supply either. Without a $Name every entry is called meta after the file, and without a $Parent it falls back to the project Default. $FromModel entries need neither, because the path they point at supplies both.
A folder without an init.meta.json never compiles at the root
Nested folders do not need one. Only the top level does, because that is what tells RoFlux the folder is game content and where it belongs.
scripts/ is reserved
It holds compile time hooks and is never scanned as game content, even if you put an init.meta.json in it.
.d.luau is always ignored
Type definition files never become instances, anywhere in the project. This is on by default and cannot be turned off.
A sidecar with no partner is silently unused
Thing.meta.json applies to Thing.luau next to it. If you rename or delete the script and leave the meta file behind, it matches nothing and does nothing. It will not warn.
Names come from the file, not the path
Inventory.server.luau becomes an instance named Inventory. The suffix picks the class and is removed from the name. Use $Name in a meta file to override.
Dots in names
Instance paths are split on both / and ., so a $Parent can be written either way. That also means an instance name containing a dot cannot be addressed in a path.
Tree building
Two things can land on the same name
If a folder and an .inst.json both produce an instance with the same name in the same parent, they merge rather than one winning. Properties, attributes, tags and children are combined, and a class name that is not Folder wins over one that is. This is what lets a meta file add children to something a file already created.
An empty declaration produces nothing
An .inst.json with no class, path, children, properties, attributes or tags is skipped. An empty file is fine and produces nothing.
Unknown $Directives are read as children
Writing "$MyThing": { "$ClassName": "Folder" } is almost certainly a typo for a child named MyThing. RoFlux reads it as that child and warns, rather than dropping it.
A tree with nowhere to go is skipped
A top level folder with no $Parent, in a project with no Default, has no home. RoFlux warns and leaves it out. Set Default in the project file to give everything a fallback.
$Parent works at any depth
It is not limited to top level folders. A nested folder with a $Parent is lifted out of its position on disk and placed where you asked, which is handy for keeping related code together in the repo while it lives apart in the DataModel.
Missing services are reported, not created
Services cannot be created, so a $Parent pointing at something the place does not have is reported as a failure for that tree. Intermediate containers that are not services are created as plain folders.
$FromModel never syncs
Reference nodes are stripped from the payload before it is sent. They show up in the sourcemap and nowhere else. They are also skipped by build, so they never appear in a place file.
Properties
An object value makes a child, not a property
Written straight into a declaration, "Size": { "x": 4, "y": 1, "z": 4 } builds a child Folder called Size, and the part keeps its default size. Use the array [4, 1, 4], or put the object inside $Properties. See Value types.
Hex colors and $Type do not survive roflux build yet
They work when syncing, but a place file written by roflux build turns them black or zero. The same goes for named UDim2 objects, single number ranges, sequences and FontFace. Arrays work in both. The full list is on the Value types page.
Types are guessed from the live instance
The plugin reads what type a property currently is and converts your JSON to match. This is why [0, 10, 0] becomes a Vector3 for Position without you saying so. It also means a property that does not exist on that class cannot be guessed, and is reported as a failure.
Colour ranges are inferred
All three components at or below 1 are read as 0 to 1. Anything above 1 is read as 0 to 255. A colour like [1, 1, 1] is white either way, but [1, 0, 0] is pure red rather than nearly black. Use $Type if you need to be sure.
Name, Parent and ClassName are not settable
They are controlled by the tree, so they are skipped if you put them in a properties table. Use $Name and $Parent instead.
Attributes are cleared only when they were declared before
Removing an attribute from a meta file removes it from the instance, because the diff knows it used to be there. An attribute you set by hand in Studio, that RoFlux never declared, is left alone.
Tags are replaced, not merged
If a node declares tags at all, its tag list becomes exactly what you declared, and tags added by hand in Studio are removed. If it declares no tags, they are left alone entirely.
Deleting
Drift is only cleaned inside managed trees
An instance you add by hand inside a folder RoFlux manages is deleted on the next full sync, because your project says exactly what belongs there. The same instance placed directly in a service is untouched, because services are passthrough.
A class change destroys the instance
Class cannot be changed in place. Renaming Thing.luau to Thing.server.luau turns a ModuleScript into a Script, which means the old instance is destroyed and a new one created. Anything living inside it goes too.
Renaming is a delete plus an add
Instances are matched by name. Renaming a file removes the old instance and creates a new one, so anything a person had parented underneath it by hand is lost.
Live deletions do not ask
The review runs once, when you connect. After that, deleting a file removes the instance straight away with no prompt. Every apply is wrapped in a change history recording, so Ctrl+Z brings it back.
Studio
Nothing happens during a playtest
Studio re-runs plugins for the play DataModel. RoFlux checks RunService:IsEdit() and stops immediately if it is not in edit mode, so it does not sync into a running game or start a second copy of itself. Changes that arrive during a run are caught up when you stop.
The review is per connection
Disconnecting and reconnecting shows the review again, including the catch up after a playtest if you had disconnected. Cancelling it drops the connection rather than applying part of the sync.
Studio needs HTTP requests enabled
Even for 127.0.0.1. If the plugin cannot connect at all, this is the first thing to check.
The console only shows failures
Routine syncing is not printed. Everything is in the Changes panel instead. Only real failures are sent to the Studio output, because the panel may be closed when one happens.
The server is local only
It binds to 127.0.0.1, so Studio has to be on the same machine. There is no remote mode.
Hook scripts
const does not work in a hook script
Hooks run on the server's Luau, which is older than the one in Studio. Use local. This only applies to files in scripts/. Your game code is passed through untouched and can use anything Roblox supports.
A hook that yields loses its result
Callbacks run in coroutines so they cannot block the build. If one yields, RoFlux warns, ignores its result for that pass, and resumes it later. Keep transform hooks straightforward and quick.
The top level of a hook must not yield at all
Loading fails with an error if it does. Register callbacks and return.
Hooks are reloaded on every edit
Saving anything in scripts/ reloads all hooks and rebuilds. Any state a hook held at the top level is lost, which is usually what you want.
A transpiler replaces built in handling
Claiming json means your hook handles every .json file in the project, including ones you may not have been thinking about. Return nil for the ones you do not want to handle and RoFlux falls back to its own behaviour.
Reads are sandboxed to the project
Absolute paths and .. are rejected. A hook cannot read outside the project folder.
Reading a file does not make it a dependency
Rebuilds are triggered by the watcher, not by what a hook read. A file inside the project folder triggers a rebuild when it changes, so this works out in practice, but there is no dependency tracking.
Editor and types
FindFirstChild is never typed
Sourcemap typing works through dot access. script.Child resolves to the real instance and autocompletes. script:FindFirstChild("Child") is typed as Instance? no matter what the sourcemap says. This is how luau-lsp works and is not something RoFlux controls.
Siblings are not children
A child declared in a folder's init.meta.json is a child of the folder, so it is a sibling of the scripts in that folder. To give a script its own children, use a sidecar Thing.meta.json or turn it into a folder with an init.luau.
The sourcemap is written, not generated by the editor
luau-lsp.sourcemap.autogenerate is set to false on purpose. RoFlux writes the file itself on every compile and every rebuild. Leaving autogenerate on means two tools fighting over one file.
Your other settings are kept
RoFlux only sets its own keys in .vscode/settings.json and leaves everything else in that file alone.
Place files
Unknown properties are dropped, with a warning
Building a place file needs real Roblox types, which come from a reflection database. A property that is not real for its class is left out rather than written as something invalid. This is stricter than syncing, where the live instance decides.
The reflection database has a version
It is baked into the server at build time. A property added to Roblox after that version will not be known to build, even though syncing handles it fine.
Place files are a snapshot
build reads the project and writes a file. It has nothing to do with a running server or a connected Studio, so it is safe to run in CI.