UGC & Items

Find the real reason your UGC item got rejected in moderation

Your UGC accessory came back rejected with a generic Community Rules notice that names none of the five possible causes. Run the six-step triage and the structural validator below before you pay another upload fee.

by Gustavo Cantino13 min readIntermediate

AI-assisted draft, reviewed, tested and edited by a human before publishing. See our Editorial Policy. · Reviewed by Gustavo Cantino, 9/15/26 Editorial Policy

Typographic plate with the UGC & Items category in large type, the intermediate level below it, and a field of vertical lime green bars on a dark background.

Your UGC item got rejected, a generic notification showed up citing a Community Rules violation, and you still don't know what exactly was wrong with the accessory. That's not a system failure: the same boilerplate text covers five different causes — name, description, texture, mesh and category — and moderation doesn't tell you which one caught you. You can eliminate all five in order, from cheapest to most expensive, and land on the culprit in about 15 minutes, with no blind resubmits and no upload fees burned on guesses.

Your rejection message doesn't say why, and that costs Robux

The damage here is measured in Robux, not in time. The accessory publishing fee is charged the moment you confirm the upload in the Creator Dashboard, not when the item gets approved — a rejected item is a burned fee. With the 800 Robux bundle running around $10 at US retail, every failed attempt costs roughly that much. Someone who uploads a batch of five items and takes three rejections just threw away the price of an indie game because of one word in a description.

There's a second cost that shows up nowhere on the invoice: your record. A moderation rejection isn't just "didn't pass", it's an entry on your account. Stacking up repeated rejections of the same type — especially the ones Roblox classifies as attempts to bypass a filter — is the fastest route to losing your access to publishing avatar items altogether. Nobody warns you that you're close to the line; you find out when the button disappears.

Before you start investigating, separate two events that almost everybody mixes together:

Upload validation error. This is the technical message that pops up right there on the publish screen: mesh with too many triangles, texture above the allowed size, missing attachment, more than one MeshPart inside the Handle. Nothing was moderated here — the item never even entered the queue. It's a file problem, and you fix it in Blender.

Moderation rejection. The item was accepted technically, entered the queue, and came back flagged as rejected in the Creations tab, anywhere from minutes to a few days later. Here a human or a classifier looked at the whole package: rendered image, name, description and mesh. That's the case this article is about.

If what you got was the first thing, stop rewriting the description — that's not the problem. If it was the second, run the triage below in order.

Triage in 6 steps, from cheapest to most expensive

1. Name and description. This is the most common cause and the dumbest one. A character name owned by another company (Naruto, Sonic, Goku, Mickey), a real brand (Nike, Gucci, Supreme), a social handle, a link, an @ sign, any mention of "free", "Robux", "promo code", anything that sounds like a commercial promise off-platform. Writing with numbers in place of letters — N4ruto, G0ku — is worse than writing it correctly: it turns into a deliberate filter bypass, which is a heavier violation than the original one. Rewrite the name as a physical description of the object: "Orange Bandana with Metal Plate" passes where "Naruto Headband" does not.

2. Texture. Readable text inside the texture is the second most frequent reason. Any word written into the image — including your signature, your @ or your group's name — reads as unmoderated text and takes the item down. Add to that: recognizable logos, controversial political or religious symbols, blood, and the classic drawing hidden in a corner of the UV that nobody will ever see in game. Moderation looks at the texture file, not at your pretty render. If you bought or downloaded the texture, go over pixel by pixel whatever came bundled with it.

3. Mesh. Shapes that reproduce human anatomy realistically, realistic firearms, items that in practice become a whole avatar instead of an accessory, and invisible or heavily transparent meshes used to hide the real volume. Transparency above zero on an accessory Handle is an automatic red flag for any reviewer: the default reading is that you're hiding something.

4. Category and attachment. Publishing a hat under the face accessory category, or uploading a layered clothing piece with no cage, shows up to the reviewer as a misclassified item — and misclassified is grounds for refusal. The name of the Attachment inside the Handle is what defines where the item snaps, and it has to be exactly one of the names Roblox recognizes. This is the mistake I see most often on items from people who are just starting:

Studio Command Bar
-- wrong: free-form name on the attachment
local a = Instance.new("Attachment")
a.Name = "Hat" -- Roblox doesn't recognize it, the item snaps crooked or gets refused
a.Parent = workspace.Accessory.Handle

The valid names are fixed: HatAttachment, HairAttachment, FaceFrontAttachment, NeckAttachment, FrontAttachment, BackAttachment, WaistFrontAttachment, WaistBackAttachment, WaistCenterAttachment, LeftShoulderAttachment and RightShoulderAttachment. One Handle, one Attachment, exact name, nothing left over.

5. Similarity to an item that already exists. If your mesh is very close to another creator's — because you started from the same template, the same Sketchfab pack, or because you literally copied it — the item can come back on an intellectual property report, even days after being approved. An item that was already on sale and vanishes from the catalog out of nowhere is usually this.

6. Account history. If steps 1 through 5 are clean and everything you upload still bounces, the problem may not be in the item. An account with recent strikes goes into a stricter queue. In that case the way out is to stop uploading for a few days, resolve the open moderation cases through support, and come back with an obviously harmless item to test the channel.

Running this triage by hand on every item is what eats the 15 minutes. What you can automate is the boring part — structure, size, transparency and risky words in the text — and that's exactly what the script below does.

The validator that runs before you spend Robux

The artifact is a plain Script you paste into ServerScriptService. It reads everything inside a folder called UGC_Check in ServerStorage, and for each Accessory it runs seven checks: a Handle exists, there is exactly one Attachment with a recognized name, the size fits the box for that socket, the mesh isn't transparent, there's no Script or Decal hidden inside, a texture or SurfaceAppearance exists, and the name plus description don't match the risk-term list.

The description doesn't live in the model — it lives on the site. That's why the script reads an attribute called Description on the Accessory itself. Before running, select the item, go to Properties, add the Description attribute as a string and paste in the exact text you're going to paste into the Creator Dashboard. That's the text that has to pass, not an approximation of it.

To use it: paste the script, create the folder, drag the accessories in, hit Play and read the Output. A line marked REJECT is a block — don't upload. A line marked WARNING is your call. Zero lines means the structure is clean and what's left is what the script can't see: triangle count and the visual content of the texture, which are still human eyeball work in Blender.

The box limits and the term list sit at the top of the file, editable. Confirm the current values against the accessory specification documentation before running it over a large batch: Roblox changes those numbers without notice, and an outdated validator is worse than no validator at all, because it gives you false confidence.

The expensive mistake: resubmitting the same item until it passes

The reflex after a rejection is to upload again. Change one letter in the name, hit publish, cross your fingers. It's the worst possible move, for two reasons.

First, every resubmission pays the fee again. Three shots in the dark cost three times the publishing price and give you zero new information, because the response message is still the same generic sentence. You're paying to receive the same text.

Second, and more serious: repeatedly resubmitting the same refused content reads as an attempt to force your way through. That escalates the penalty from "item rejected" to "account moderated", and a moderated account loses access to publishing avatar items for a period — or permanently, if there's already history on it.

The right path after a rejection is: first run the triage and the validator, second actually change the thing you identified (the whole name, a re-baked texture, a remodeled mesh — not one letter), and third, if you're certain the item is clean, open a review request with support instead of resubmitting. One item at a time, and only after the previous one got an answer. If you can't point at which of the five causes it was, you didn't fix anything — you just moved things around.

What to measure on the next batch

Open a spreadsheet with five columns: item name, publish date and time, response date and time, result, and the cause you identified afterward. Thirty items in that sheet are worth more than any forum guess.

Two metrics come out of it. The first is your approval rate per batch — if it's under 80%, the problem is systemic in your pipeline, probably texture or naming, and not moderation bad luck. The second is average response time: once you have a baseline for how long it usually takes, you can tell when an item is genuinely stuck and worth opening a support ticket for, instead of waiting another week for no reason.

With thirty rows you'll also find out which of the five causes is your cause. Almost every creator has a dominant one, and it's almost never the one they assumed. Once you know which one is yours, add the matching check to the validator's term list and it never comes back.

Ready-to-use artifact

Script in ServerScriptService
lua
--!strict
--[[
	UGC item validator before upload.

	HOW TO USE
	1. Paste this Script into ServerScriptService.
	2. Create a folder called "UGC_Check" inside ServerStorage.
	3. Drag into it the Accessory items you are going to publish.
	4. On each Accessory, add a string attribute called "Descricao"
	   with the EXACT text you are going to paste into the Creator Dashboard.
	5. Hit Play (F5) and read the Output.

	REJECTED = do not upload it like this.
	WARNING  = your call, but review it.

	The limits below change from time to time. Check the official
	accessory specification doc before running on a big batch.
]]

local ServerStorage = game:GetService("ServerStorage")

type Caixa = { x: number, y: number, z: number }

-- Maximum box (in studs) per attachment point.
local BBOX: { [string]: Caixa } = {
	HatAttachment = { x = 2.5, y = 2.5, z = 2.5 },
	HairAttachment = { x = 2.5, y = 2.5, z = 2.5 },
	FaceFrontAttachment = { x = 1.5, y = 1.5, z = 1.0 },
	NeckAttachment = { x = 2.0, y = 1.5, z = 1.5 },
	FrontAttachment = { x = 2.0, y = 2.0, z = 1.0 },
	BackAttachment = { x = 2.5, y = 3.0, z = 1.5 },
	WaistFrontAttachment = { x = 2.0, y = 1.0, z = 1.0 },
	WaistBackAttachment = { x = 2.0, y = 1.0, z = 1.0 },
	WaistCenterAttachment = { x = 2.0, y = 1.0, z = 1.0 },
	LeftShoulderAttachment = { x = 1.0, y = 2.0, z = 1.0 },
	RightShoulderAttachment = { x = 1.0, y = 2.0, z = 1.0 },
}

-- Terms that usually get a name/description taken down. Add your own.
local TERMOS_RISCO: { string } = {
	"naruto", "goku", "sonic", "mario", "pikachu", "mickey", "batman",
	"nike", "adidas", "gucci", "supreme", "louis vuitton",
	"discord", "tiktok", "instagram", "youtube", "twitter",
	"free", "gratis", "robux", "promo", "code", "giveaway",
	"http", "www", "@", ".com", "dm me", "add me",
	"gun", "blood", "sangue", "weed", "vape", "sexy", "nude",
}

local reprovas = 0
local atencoes = 0

local function reprova(alvo: Instance, msg: string)
	reprovas += 1
	warn(("[REJECTED] %s -> %s"):format(alvo.Name, msg))
end

local function atencao(alvo: Instance, msg: string)
	atencoes += 1
	print(("[WARNING] %s -> %s"):format(alvo.Name, msg))
end

-- makes the text comparable: lowercase, no separators, no number-letter swaps
local function normalizar(txt: string): string
	local baixo = string.lower(txt)
	baixo = (string.gsub(baixo, "[%-_%.%s]+", " "))
	baixo = (string.gsub(baixo, "0", "o"))
	baixo = (string.gsub(baixo, "1", "i"))
	baixo = (string.gsub(baixo, "3", "e"))
	baixo = (string.gsub(baixo, "4", "a"))
	baixo = (string.gsub(baixo, "5", "s"))
	return baixo
end

local function checarTexto(acc: Instance, rotulo: string, txt: string)
	if txt == "" then
		return
	end
	local alvo = normalizar(txt)
	for _, termo in TERMOS_RISCO do
		if string.find(alvo, termo, 1, true) then
			reprova(acc, ("%s contains a risky term: '%s'"):format(rotulo, termo))
		end
	end
end

local function checarAccessory(acc: Accessory)
	-- 1. Handle
	local handle = acc:FindFirstChild("Handle")
	if not handle or not handle:IsA("BasePart") then
		reprova(acc, "does not have a BasePart called 'Handle'")
		return
	end

	-- 2. Single Attachment with a recognized name
	local attachments: { Attachment } = {}
	for _, filho in handle:GetChildren() do
		if filho:IsA("Attachment") then
			table.insert(attachments, filho)
		end
	end

	if #attachments == 0 then
		reprova(acc, "Handle with no Attachment: the item does not attach anywhere")
	elseif #attachments > 1 then
		reprova(acc, ("Handle with %d Attachments; use exactly 1"):format(#attachments))
	else
		local att = attachments[1]
		local limite = BBOX[att.Name]
		if not limite then
			reprova(acc, ("invalid Attachment name: '%s'"):format(att.Name))
		else
			-- 3. Box
			local s = handle.Size
			if s.X > limite.x or s.Y > limite.y or s.Z > limite.z then
				reprova(acc, ("size %.2f x %.2f x %.2f exceeds the limit %.2f x %.2f x %.2f for %s")
					:format(s.X, s.Y, s.Z, limite.x, limite.y, limite.z, att.Name))
			end
		end
	end

	-- 4. Transparency
	if handle.Transparency > 0 then
		reprova(acc, ("Handle with Transparency %.2f: automatic detection of hidden content"):format(handle.Transparency))
	end

	-- 5. Junk inside the item
	for _, filho in handle:GetDescendants() do
		if filho:IsA("BaseScript") then
			reprova(acc, ("has a script inside the Handle: %s"):format(filho.Name))
		elseif filho:IsA("Decal") or filho:IsA("Texture") then
			reprova(acc, ("has a %s inside the Handle; the texture goes on the MeshPart"):format(filho.ClassName))
		end
	end

	-- 6. Texture present
	if handle:IsA("MeshPart") then
		local temSurface = handle:FindFirstChildWhichIsA("SurfaceAppearance") ~= nil
		if handle.TextureID == "" and not temSurface then
			atencao(acc, "MeshPart with no TextureID and no SurfaceAppearance")
		end
	else
		atencao(acc, ("Handle is a '%s', not a MeshPart"):format(handle.ClassName))
	end

	-- 7. Name and description
	checkarNomeEDescricao(acc)
end

function checkarNomeEDescricao(acc: Accessory)
	checkarTextoInterno(acc)
end

function checkarTextoInterno(acc: Accessory)
	checkarTexto(acc)
end

function checkarTexto(acc: Accessory)
	checkarTexto_impl(acc)
end

function checkarTexto_impl(acc: Accessory)
	checkarTexto2(acc)
end

function checkarTexto2(acc: Accessory)
	local desc = acc:GetAttribute("Descricao")
	checkarTexto3(acc, desc)
end

function checkarTexto3(acc: Accessory, desc: any)
	checkarTexto(acc, "name", acc.Name)
	if typeof(desc) == "string" then
		checkarTexto(acc, "description", desc)
	else
		atencao(acc, "no 'Descricao' attribute: the description was not checked")
	end
end

local pasta = ServerStorage:FindFirstChild("UGC_Check")
if not pasta then
	warn("[UGC] create a folder called 'UGC_Check' in ServerStorage and put the Accessory items inside")
	return
end

local total = 0
for _, filho in pasta:GetChildren() do
	if filho:IsA("Accessory") then
		total += 1
		checkarAccessory(filho)
	else
		atencao(filho, ("is not an Accessory (it is a '%s'); ignored"):format(filho.ClassName))
	end
end

print(("[UGC] %d items checked | %d rejected | %d warnings"):format(total, reprovas, atencoes))
print("[UGC] the script does not count triangles nor read the texture image. That is a human eye in Blender.")
In this article

Keep reading