Project format
RoFlux describes a game with files rather than one large manifest. Files become instances, folders become containers, and small JSON files say where a tree belongs and what it is.
default.project.json
This file is deliberately small. It holds project info and lets you set properties on Roblox services. Everything else is expressed by files on disk.
{
"ProjectID": "Scope/Project",
"Name": "MyGame",
"Default": "ReplicatedStorage",
"Lighting": {
"ClockTime": 14,
"Brightness": 2
},
"ReplicatedStorage": {
"Config": { "$ClassName": "Configuration" }
}
}
| Key | Meaning |
|---|---|
ProjectID | An identifier for the project, sent to the plugin on connect. |
Name | Name of the DataModel node in the sourcemap. Defaults to the folder name. |
Default | Where top level trees go when they do not declare a $Parent. |
Include | Only these top level folders compile. Leave it out to compile everything. |
Exclude | These top level folders never compile. |
| anything else | Treated as a service path. Its $Properties are applied and its children are created. |
If there is no default.project.json, RoFlux warns and carries on with defaults.
Multiple places from one repo
A lot of games are more than one place. A lobby, a main game, a test map. They usually share code but each one only needs part of the tree.
Add an Include list to say which top level folders a place should compile:
-- lobby.project.json
{
"ProjectID": "MyGame/Lobby",
"Name": "Lobby",
"Include": ["Shared", "Lobby"]
}
Then point any command at it by name. RoFlux adds .project.json for you:
roflux serve lobby
roflux build lobby --output Lobby.rbxl
With no name at all you get default.project.json, so a single place project needs nothing extra.
With Include set, only those folders are scanned. Everything else on disk is left out of the tree, so it is never created in that place and never appears in that sourcemap. Exclude works the other way and drops folders from a build that would otherwise compile.
If neither is set, the whole project compiles as normal. This is opt in, so an existing project keeps working exactly as it did.
They filter what gets scanned at the project root. Once a folder is included, everything inside it compiles as usual. meta.inst.json is always read, because references only affect the sourcemap.
The services and children declared in the manifest itself are always part of that place, so each place file can also set its own service properties.
Project root rules
The top level of a project is special. Only two kinds of thing compile there:
meta.inst.json- folders containing an
init.meta.json, along with everything inside them
Everything else loose at the root is ignored. That keeps a README, a Cargo file, a build script or a stray note from accidentally becoming an instance. scripts/ is reserved for compile time hooks and is never scanned as content.
meta.inst.json is the exception because it is the one place to describe instances that have no file of their own. It gets its own section below.
Once inside a folder that does compile, every file in it is scanned normally, no init.meta.json required per level.
meta.inst.json
This is the one instance file that is allowed to sit in the main project folder, next to your project file. It is where you describe things that have no file of their own.
MyGame/
├─ default.project.json
├─ meta.inst.json -- right here, at the top
├─ scripts/
├─ Shared/
└─ Server/
Everywhere else in a project, an instance comes from a file. A script is a .luau file, a folder is a folder on disk. But some instances do not belong to any file:
- content that already exists in the place and that you do not want RoFlux to manage, such as a map you built by hand
- small empty instances that exist just to hold something, like a folder of remote events
Both of those go in meta.inst.json.
Pointing at what is already in the place
This is the main reason the file exists. A $FromModel entry tells RoFlux that something is there, what class it is, and what is inside it, without RoFlux ever touching it.
[
{
"$FromModel": "Workspace/Map",
"$ClassName": "Model",
"Spawns": { "$ClassName": "Folder" }
}
]
Your map is now in the sourcemap, so workspace.Map.Spawns autocompletes and type checks in your editor, but RoFlux will never create it, change it, or delete it. It is a description, not an instruction. See $FromModel for the details.
Declaring loose instances
The file also takes ordinary declarations. Give each one a $Name and a $Parent and it is built like anything else, and RoFlux does own these.
[
{
"$Name": "Remotes",
"$ClassName": "Folder",
"$Parent": "ReplicatedStorage",
"Ping": { "$ClassName": "RemoteEvent" },
"Buy": { "$ClassName": "RemoteFunction" }
}
]
That saves making a folder on disk that would only ever hold a meta file.
The shape of the file
It is an array when you have several entries, or a single object when you only have one. Both are fine.
-- one entry
{ "$FromModel": "Workspace/Map", "$ClassName": "Model" }
| Rule | Why |
|---|---|
| It is always read | Even when Include is set, because references only affect the sourcemap and every place wants them. |
Entries need $Parent | There is no folder around them to say where they go. Without one they fall back to Default. |
Entries need $Name | Otherwise they are all named after the file, which is meta. |
| Empty entries are skipped | An entry with no class, children or properties produces nothing. |
Deeper in the project any .inst.json works and is placed by the folder it sits in. meta.inst.json is only a convention for the root, where the root rules would otherwise ignore a loose file.
How files map
| File | Becomes |
|---|---|
Thing.luau | ModuleScript |
Thing.server.luau | Script |
Thing.client.luau | LocalScript |
Thing.d.luau | ignored, always |
Thing.inst.json | one or more declared instances |
meta.inst.json | the same, but the only one allowed at the project root |
Thing.meta.json | metadata applied to Thing.luau beside it |
init.meta.json | metadata for the folder it sits in |
Thing.txt, .md, .csv, .html, .css, .toml, .yml | StringValue |
Thing.json | ModuleScript returning the decoded data as a Luau table |
| anything else | ignored, unless a hook script claims the extension |
.lua works everywhere .luau does.
Script source is read as text and sent verbatim. RoFlux does not compile, transpile or rewrite it, so any syntax Roblox accepts is fine. The only thing that can change your source is a hook script you wrote yourself.
Folders and init files
A plain folder becomes a Folder. A folder containing an init script becomes that script instead, and everything else in the folder becomes its children.
Signal/
├─ init.luau -- Signal is now a ModuleScript
└─ Connection.luau -- a child of Signal
| Init file | Folder becomes |
|---|---|
init.luau | ModuleScript |
init.server.luau | Script |
init.client.luau | LocalScript |
Only the first init file found is used, checked in the order server, client, module.
Metadata files
There are three JSON shapes, and they all take the same directives.
init.meta.json
Describes the folder it lives in. This is where you say where a tree belongs.
{
"$Parent": "StarterPlayer/StarterPlayerScripts",
"$Attributes": { "Version": "1.2.0" },
"Settings": { "$ClassName": "Configuration" }
}
Sidecar Thing.meta.json
Sits beside Thing.luau and applies to it. This is how you give a script properties, attributes, tags, or children of its own.
-- Inventory.meta.json, next to Inventory.luau
{
"$Tags": ["Service"],
"Config": { "$ClassName": "Configuration" }
}
The alternative is the folder form: rename to Inventory/init.luau and put siblings next to it.
Thing.inst.json
Declares instances that have no source file of their own. It may be a single object, or an array to declare several at once.
{
"$ClassName": "Folder",
"Spawns": {
"$ClassName": "Configuration",
"Limit": 12
}
}
The instance takes the file's stem as its name unless you set $Name. A declaration with nothing in it produces nothing.
Directives
Keys beginning with $ are directives. They tell RoFlux something about the instance instead of setting a property on it. Every other key is read one of two ways:
- an object means a child instance
- anything else means a property
So you rarely need $Properties at all. Write the property straight in:
{
"$ClassName": "Part",
"Anchored": true, -- property
"Position": [0, 10, 0], -- property
"Weld": { -- object, so a child
"$ClassName": "WeldConstraint"
}
}
| Directive | In short |
|---|---|
$ClassName | The class to create. |
$Name | The instance name. |
$Parent | Where in the game it goes. |
$Path | A file or folder to put here. |
$FromModel | Something already in the place. Sourcemap only. |
$Properties | A table of properties. |
$Attributes | A table of attributes. |
$Tags | A list of tags. |
$Children | A table of children. |
$Ignore | Skip this one. |
$Type and $Value | Say exactly what type a value is. |
Where each one works
Most directives work in every JSON file. A few only make sense in one place. "Child" means a declaration nested inside another one.
| Directive | Project file service | Project file child | init.meta.json | Thing.meta.json | Thing.inst.json | Nested child |
|---|---|---|---|---|---|---|
$ClassName | no | yes | yes | no | yes | yes |
$Name | no | yes | yes | yes | yes | yes |
$Parent | no | no | yes | yes | yes | no |
$Path | yes | yes | no | no | no | no |
$FromModel | no | yes | no | no | yes | yes |
$Properties | yes | yes | yes | yes | yes | yes |
$Attributes | yes | yes | yes | yes | yes | yes |
$Tags | yes | yes | yes | yes | yes | yes |
$Children | yes | yes | yes | yes | yes | yes |
$Ignore | no | yes | yes | yes | yes | yes |
Where a directive does not work, it is simply not used. The reasons are in each section below. A declaration returned by a hook script's transpiler works like a Thing.inst.json.
$ClassName
The class of instance to create, like "Part", "ScreenGui" or "RemoteEvent". Leave it out and you get a Folder.
{ "$ClassName": "Configuration" }
- In an
init.meta.json, it sets the class of the folder. If the folder has aninitscript, the script wins, so a folder withinit.luauis always aModuleScript. - In a sidecar like
Thing.meta.json, the file beside it already decides the class.Thing.luauis always aModuleScript. - A service in the project file is already a class, so it can not be changed.
Changing the class of something that was already synced deletes it and creates a new one, along with everything inside it. See Deletion.
$Name
The name of the instance. Without it, the name comes from where the thing was declared:
- a file is named after its stem, so
Coins.server.luaubecomesCoins - a folder is named after the folder
- a child is named after its key
-- hud/init.meta.json
{ "$Name": "HUD" }
It is also the way to use a name your computer will not allow in a file name, such as one with : or ? in it. In meta.inst.json, every entry needs a $Name, or they would all be called meta.
$Parent
Where the thing goes in the game, written as a path with / between each part, starting from a service.
-- Client/init.meta.json
{ "$Parent": "StarterPlayer/StarterPlayerScripts" }
- It works at any depth. An
init.meta.jsondeep inside your project can move its folder somewhere else entirely. - Any part of the path that does not exist yet is made as a
Folder. RoFlux does not own those folders, so it will not delete other things you put in them. - The path can lead into something that already exists in the place, like a map you built or something declared with
$FromModel. RoFlux looks for it there and only adds your thing inside. See Putting your own things inside. - Without it, a folder at the top of the project goes to the
Defaultfrom the project file. Anything deeper stays inside the folder it sits in on disk. - Changing it is a move. The new copy is made and the old one is deleted, even if you changed it while not connected. See Moving things.
It is not used on children, because a child always goes inside the thing it is declared in. It is not used in the project file either, where the key already says where things go.
$Path
A file or folder from your project to put at this point. It only works in the project file.
{
"StarterGui": { "$Path": "GUI" },
"ServerScriptService": {
"Main": { "$Path": "src/bootstrap.server.luau" }
}
}
On a child, the file or folder becomes that child. On a service, the folder's contents go straight into the service. The full rules are in the $Path section below.
$FromModel
Describes something that is already in the place, such as a map you built by hand. It only puts it in the sourcemap, so your editor can autocomplete it. RoFlux never creates, changes or deletes it.
-- meta.inst.json
{ "$FromModel": "Workspace/Map", "$ClassName": "Model" }
In an .inst.json file, the value is the path to the thing in the game. On a child, the child is already placed by its key, so any value marks it as something that already exists. Everything inside a $FromModel is left alone too. The full rules are in the $FromModel section below.
$Properties
A table of properties to set.
{
"$ClassName": "TextLabel",
"$Properties": {
"Text": "Coins",
"FontFace": { "family": "rbxasset://fonts/families/GothamSSm.json", "weight": "Bold" }
}
}
Most of the time you can skip it and write properties straight into the declaration. You need it when a property's value is an object, like FontFace above. Written straight in, an object would turn into a child instead. Arrays like [1, 0, 0] are always properties, so they are safe anywhere.
How to write each kind of value is on the Value types page.
$Attributes
A table of attributes to set, the ones you read with GetAttribute.
{
"$Attributes": {
"Version": "1.2.0",
"MaxCoins": 50,
"Tint": { "$Type": "Color3", "$Value": [255, 200, 0] }
}
}
Text, numbers and true or false just work. An attribute has no fixed type to guess from, so anything else, like a color, needs $Type. If you remove an attribute from the file while connected, it is removed from the instance too.
$Tags
A list of tags, the ones you use with CollectionService.
{ "$Tags": ["Coin", "Spinning"] }
The list is the full set. Once an instance has $Tags, any tag on it that is not in the list is removed.
$Children
A table of children, keyed by name. Every entry inside it is a child, whatever it looks like.
{
"$ClassName": "Folder",
"$Children": {
"$Money": { "$ClassName": "IntValue" },
"Settings": { "$ClassName": "Configuration" }
}
}
You only need it for a child whose name starts with $, since that would look like a directive. It is also handy for keeping children apart from properties in a big declaration.
$Ignore
Set it to true to skip a declaration completely.
-- Experimental/init.meta.json
{ "$Ignore": true }
- In an
init.meta.json, the whole folder and everything in it is skipped. - In a sidecar like
Thing.meta.json, the file beside it is skipped. - On an entry or a child, just that one is skipped.
If the thing was already synced, skipping it deletes it from the place, the same as deleting its file. Set it back to false, or remove the line, to bring it back. It does not work on a service in the project file, since a service can not be removed.
$Type and $Value
These are not about the instance. They wrap a single value to say exactly what type it is.
"Tint": { "$Type": "Color3", "$Value": [255, 0, 0] }
An object with $Type is always a value, never a child, so it is safe to write straight into a declaration. You mostly need it for attributes. The list of types is on the Value types page.
Project file keys
The top level of a project file has a few settings of its own. ProjectID, Default, Include and Exclude can also be written with a $ in front, as $ProjectID and so on, and mean the same thing. Name and Scripts are written without one. These are covered in default.project.json.
Typos
An unknown $Key whose value looks like a declaration is read as a child named without the $, and RoFlux warns you. Any other unknown $Key is skipped with a warning. Either way a typo shows up in the output instead of quietly doing nothing.
Property values
Values are plain JSON. RoFlux turns each one into the type the property really has, so you rarely need to say what a value is.
{
"$ClassName": "Part",
"$Properties": {
"Position": [0, 10, 0], -- Vector3
"Color": [1, 0, 0], -- Color3, 0-1
"Size": { "x": 4, "y": 1, "z": 4 }, -- named axes also work
"Material": "Neon", -- enum by name
"Anchored": true
}
}
That example uses $Properties, so the Size object is read as a property. Written straight into the declaration, an object would make a child instance instead. Arrays like [4, 1, 4] are always properties, which is why they are the safe choice.
Colors, enums, UDim2, CFrames, sequences, fonts and attributes each have their own rules. The Value types page covers every one with examples, including what roflux build does not handle yet.
$FromModel
Declares content that already exists in the place so tooling knows about it. RoFlux never creates, edits or deletes these, and they are stripped out of the sync payload entirely. They exist purely so the sourcemap is complete.
-- meta.inst.json
[
{
"$FromModel": "Workspace/Map",
"$ClassName": "Model",
"Spawns": { "$ClassName": "Folder" }
}
]
The path is where it lives in the DataModel, $ClassName is its type, and children describe its shape. Declared children inherit the reference status, so they are equally untouched.
Putting your own things inside
You can still add things of your own to something declared with $FromModel. Point their $Parent into it:
-- meta.inst.json
[
{ "$FromModel": "Workspace/Map", "$ClassName": "Model" },
{ "$Name": "Lamp", "$ClassName": "Part", "$Parent": "Workspace/Map" }
]
The lamp is yours, so it is created, updated and deleted like anything else. The map is still not yours. RoFlux uses it only as a place to look for the lamp, and never changes its properties, attributes, tags or anything else inside it. The order of the entries does not matter, and the same works from any .inst.json or init.meta.json in the project.
If the map is missing from the place, a plain Folder with its name is made to hold your things, the same as any other part of a $Parent path that does not exist yet.
$Path
$Path says which file or folder should be inserted at that point. The file becomes the instance, and the key it is declared under becomes its name.
{
"ServerScriptService": {
"Main": { "$Path": "src/bootstrap.server.luau" }
}
}
That produces ServerScriptService.Main, a Script holding the contents of src/bootstrap.server.luau. Paths are relative to the project root, and $Path reaches files the root rules would otherwise ignore. Pointing at a folder mounts the whole folder, init file and all. If the target does not exist, RoFlux warns and carries on.
Filling a service from a folder
Put $Path right on a service to pour a folder's contents straight into it:
{
"StarterGui": {
"$Path": "GUI"
}
}
Everything inside GUI lands directly in StarterGui, so GUI/Hud becomes StarterGui.Hud. There is no extra GUI folder in between. The folder does not need an init.meta.json. If it has one, its properties, attributes and tags are set on the service, and the folder is not also compiled somewhere else.
Each thing from the folder is managed, so deleting a file deletes its instance. The service itself stays passthrough, so anything else already in StarterGui is left alone. Here the path must be a folder, since a service can not be replaced by a file.
The sourcemap
sourcemap.json is rewritten on every compile and every rebuild. It is what luau-lsp reads, and it is RoFlux's own picture of the project.
MyGame [DataModel] <- default.project.json
ReplicatedStorage
Shared [Folder] <- Shared/init.meta.json
Greeting [ModuleScript] <- Shared/Greeting.luau
Workspace
Map [Model] <- meta.inst.json
Every node carries the files it came from, including instances produced by a transpiler hook, so an editor can jump from an instance back to the file that made it.
A transpiled file can also have a stub written for it, in which case the stub is listed first so your editor reads types from it. Those live in a generated MetaStubs folder, which is never compiled into your game. See Types for files you transpile.