Lua Scripting

Leaderboards in Roblox: the folder that has to be called leaderstats, in lowercase

The built-in Roblox leaderboard has three tabs and two different data sources. The Here tab comes from a folder called leaderstats — and its name, in lowercase, is the difference between working and showing nothing at all.

by Gustavo Cantino9 min readBeginner

Produced with AI from the sources cited below, and checked by automated verification against those same sources. No person read this text before publication. Editorial Policy

Typographic plate with the Lua Scripting category in large type, the beginner level below it, and a field of vertical lime green bars on a dark background.

There is a Roblox bug with an unmistakable signature: the script runs without error, the output is clean, and the leaderboard simply shows nothing. No message, no clue.

The cause is almost always a capital letter. This guide explains the built-in leaderboard from beginning to end — the three tabs, the two data sources, and why the name of a folder is the most important thing about the subject.

The problem

Roblox has a built-in leaderboard, inside the PlayerList. Which means: before drawing any interface, there is a ready-made place where the player's stats appear, and it is already on screen.

What confuses people is that this place has three tabs, and they do not come from the same source. Here shows the session ranking, of the players on the current server. Friends and Global show a persistent ranking, which comes from an ordered data store registered in the Creator Hub — Global compares every player in the experience.

The division of labour is clean and worth memorising: leaderstats feeds the Here tab; the ordered data store feeds Friends and Global. They are independent systems. Building leaderstats does not populate Global, and registering an ordered data store does not change Here.

Most games need only the first, and that is what the rest of this text is about.

It is worth saying why to use the built-in one instead of drawing your own. It is not laziness: the PlayerList leaderboard is already in the place the player looks for, it already works on mobile and console, it already handles people joining and leaving, and it does not spend a pixel of your interface. A leaderboard of your own has to solve all of that again, and most of the ones you see out there solve it worse.

The legitimate case for your own leaderboard is when the information does not fit the shape of columns — a progress bar, an icon, a colour, grouping by team. If what you want to show is a number beside a name, the built-in one is the right answer, and the discussion ends there.

There is also a second-order effect that usually settles the question: what is in leaderstats is read by other parts of the ecosystem. It is the structure Roblox knows, and third-party code, plugins and moderation tools expect to find it. Keeping the progress only in a table of your own works and isolates your game from all of that.

Step by step

1. Create the script. The official step is a Script in ServerScriptService called Leaderboard. Server, not client: the leaderboard is game state, and game state lives on the server.

2. Connect to PlayerAdded. Everything happens when someone joins.

3. Create the folder. Inside the connected function, create a Folder, call it leaderstats and set the player as its parent.

The documentation stops the explanation here to say one thing out loud, and it is worth repeating literally: it is essential that the folder is called leaderstats, all lowercase. Roblox does not add the player to the leaderboard if the name is anything else.

4. Add the stats. The leaderboard uses value objects to store and show the numbers: IntValue for an integer, and there are also NumberValue and StringValue.

Three details come together and define the behaviour. The object's name is exactly how the stat appears on the leaderboard — there is no separate label, the name is the label. The value of IntValue, NumberValue and StringValue has to be set through the Value property, not assigned to the object. And it is the object having the leaderstats folder as its parent that puts it on the leaderboard: while Parent is not set, the object exists and does not appear.

5. Update during the match. Updating means changing the Value property of that stat inside the player's folder. The official example is a collectible object that, on being touched, destroys itself and adds to the stat of whoever picked it up.

6. Control the order. There are three ways, and they combine in a specific manner.

The first is the simplest: add the stats in the order they should appear. The second is a BoolValue child called IsPrimary, set to true, which puts that stat first. The third is a NumberValue child called Priority, with an integer value — higher priority appears earlier, and a stat with no priority counts as zero by default.

The combination rule is written down and saves an afternoon of trial and error: IsPrimary takes precedence over any Priority. And if several stats have IsPrimary true, then their Priority decides the order among them.

The artifact

The script below does what the official example does, with an organisational difference that pays off in the third month: the stats are declared in a table, not created one by one in the middle of the function.

The gain is concrete. The leaderboard order becomes readable on a single screen, instead of being implicit in the order of scattered lines. Adding a stat becomes adding a line. And IsPrimary and Priority become fields of the declaration, instead of four more Instance.new lines each.

It also brings an empty persistence hook, kept separate on purpose: the leaderboard should not know what a DataStore is. When the time comes, filling in one function is enough.

And a helper — Leaderboard.add — which exists for a specific reason: without it, every point in the game that touches the leaderboard navigates the tree on its own, and a mistyped name fails silently. With it, it fails with a warning.

Common mistakes

The champion mistake is the folder name, and it deserves a paragraph of its own because the way it fails is the worst possible.

Leaderstats, LeaderStats, leaderStats — they all look right to the eye, they all create a perfectly valid folder inside the player, and none of them works. There is no error in the output, no exception, nothing to debug: the leaderboard is empty because Roblox did not recognise the folder. Anyone who does not know the rule spends hours checking the stat value, which was right the whole time.

The second trap is from the same family: assigning the value to the object instead of to the property. gold = 10 in place of gold.Value = 10. The first form throws away the reference to the object and the variable becomes a number. The leaderboard keeps showing the initial value, and nothing complains.

The third is about the order of operations. Setting the object's Parent before finishing its configuration makes the stat appear on the leaderboard half-built — the name still the default, the value still zero — and then change by itself in front of the player. Configuring everything and setting Parent last fixes it, and that is what the artifact does.

The fourth is about scope: building the leaderboard in a LocalScript. It works on the screen of whoever tested it, it does not exist for anyone else, and it disappears when the character respawns.

And there is a fifth, subtler one, that only shows up in a game with some age: building leaderstats inside CharacterAdded instead of PlayerAdded. The difference does not show in the first test, because the character is born right after the player joins and everything looks right. It shows when the player dies: the new character fires the event again, the script creates a second leaderstats folder, and from then on there are two — the old one, which the leaderboard is showing, and the new one, which the rest of the code is updating. The number freezes on screen while the real value keeps climbing.

The diagnosis is quick once you know what to look for: open the Explorer during a test, find the player and count how many folders exist underneath. If there is more than one, that is the bug. The rule that avoids it: leaderstats belongs to the player, not the character, and is built once per session.

Next step

With the Here tab working, the natural next step is deciding whether you want the other two. They require something different: an ordered data store registered in the Creator Hub, which is what supports a persistent ranking between players and between friends.

It is worth measuring the desire before building. A global leaderboard is powerful and expensive to keep honest — it becomes an exploit target, it has to deal with absurd values and with whoever found a shortcut, and a visibly rigged global leaderboard does more harm to the perception of a game than no leaderboard at all.

Before that, there is cheaper work with a bigger return: deciding which stats deserve to be there. The leaderboard appears on the whole screen, all the time, and every new column steals space and attention from the others. Two or three well chosen ones communicate progress; six communicate that nobody decided what matters.

Sources

How we correct mistakes

Ready-to-use artifact

Session leaderboard with controlled ordering and a door to persistence (ServerScriptService)
lua
-- ServerScriptService, a Script called "Leaderboard".
--
-- The folder MUST be called "leaderstats", all lowercase. Any other
-- name and the player simply does not enter the leaderboard — no error,
-- no warning, nothing in the output.

local Players = game:GetService("Players")

-- Declaring the stats in a table, instead of creating them one by one in the
-- middle of the function, settles the order and keeps the whole list on one screen.
--
--   name     -> is EXACTLY the text that appears in the column
--   class    -> IntValue for an integer, NumberValue for a decimal,
--               StringValue for text
--   initial  -> value at the start of the session
--   primary  -> IsPrimary: sends it to the first position
--   priority -> Priority: higher appears earlier. Without this, it is 0.
local STATS = {
	{ name = "Coins", class = "IntValue", initial = 0, primary = true },
	{ name = "Level", class = "IntValue", initial = 1, priority = 10 },
	{ name = "Time", class = "StringValue", initial = "00:00", priority = 5 },
}

-- Persistence hook. Return a table { [name] = value } to start the session
-- with whatever was saved, or nil to use the initial values.
-- Kept separate on purpose: the leaderboard should not know about DataStores.
local function loadSaved(_player)
	return nil
end

local function build(player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats" -- lowercase. Do not touch.

	local saved = loadSaved(player)

	for _, def in STATS do
		local stat = Instance.new(def.class)
		stat.Name = def.name
		stat.Value = (saved and saved[def.name] ~= nil) and saved[def.name] or def.initial

		if def.primary then
			local isPrimary = Instance.new("BoolValue")
			isPrimary.Name = "IsPrimary"
			isPrimary.Value = true
			isPrimary.Parent = stat
		end

		if def.priority then
			local priority = Instance.new("NumberValue")
			priority.Name = "Priority"
			priority.Value = def.priority
			priority.Parent = stat
		end

		-- the parent is what puts the stat on the leaderboard; last,
		-- so the object arrives already complete
		stat.Parent = leaderstats
	end

	leaderstats.Parent = player
end

-- Helper for the rest of the game: adds without having to navigate the tree
-- everywhere, and warns when the stat does not exist instead of failing
-- silently.
local Leaderboard = {}

function Leaderboard.add(player, name, amount)
	local leaderstats = player:FindFirstChild("leaderstats")
	local stat = leaderstats and leaderstats:FindFirstChild(name)
	if not stat then
		warn("stat does not exist on the leaderboard: " .. tostring(name))
		return
	end
	stat.Value += amount
end

Players.PlayerAdded:Connect(build)

return Leaderboard
In this article

Keep reading