The site is under maintenance, so logging in is temporarily unavailable. All resources and the MCP still work.
Text chat and chat safety rules
By Roblox Vibecoders · roblox-text-chat
Use when touching in-game chat in Roblox - TextChatService, TextChannels (RBXGeneral), SendAsync, OnIncomingMessage, ShouldDeliverCallback, TextChatCommand admin commands, bubble chat, NPC bubbles, chat tags - or when showing any player-written text (pet names, signs, boards) that needs TextService filtering. Also covers the 2025-2026 age-check chat rules, CanUserChatAsync, CanUsersChatAsync, CanUsersDirectChatAsync, GetChatGroupsAsync, Quick Words and platform-integrated chat.
Install it
mkdir -p ~/.claude/skills/roblox-text-chat && curl -fsSL https://robloxvibecoders.com/skills/roblox-text-chat/SKILL.md -o ~/.claude/skills/roblox-text-chat/SKILL.md
New-Item -ItemType Directory -Force "$HOME\.claude\skills\roblox-text-chat" | Out-Null; Invoke-WebRequest https://robloxvibecoders.com/skills/roblox-text-chat/SKILL.md -OutFile "$HOME\.claude\skills\roblox-text-chat\SKILL.md"
mkdir -p .cursor/rulescurl -fsSL https://robloxvibecoders.com/skills/roblox-text-chat/SKILL.md -o .cursor/rules/roblox-text-chat.mdc
Use the robloxvibecoders MCP: call get_resource with id "skill-roblox-text-chat" and install it as a Claude Code skill in this project (.claude/skills/roblox-text-chat/SKILL.md).
Needs the Roblox Vibecoders MCP connected.
Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.
What your AI reads
---
name: roblox-text-chat
description: Use when touching in-game chat in Roblox - TextChatService, TextChannels (RBXGeneral), SendAsync, OnIncomingMessage, ShouldDeliverCallback, TextChatCommand admin commands, bubble chat, NPC bubbles, chat tags - or when showing any player-written text (pet names, signs, boards) that needs TextService filtering. Also covers the 2025-2026 age-check chat rules, CanUserChatAsync, CanUsersChatAsync, CanUsersDirectChatAsync, GetChatGroupsAsync, Quick Words and platform-integrated chat.
---
# Text chat and chat safety rules
Chat on Roblox is regulated. This skill separates **policy** (Roblox rules your game must follow) from **API** (how to
implement it). Policy changed a lot in 2025-2026; re-check the linked pages before shipping chat features.
## Policy (Roblox rules)
From the Creator Hub chat guidelines and Roblox announcements, as of late 2026:
- **All player-to-player text chat must go through `TextChatService`** and its `TextChannel`s. Legacy chat and custom
chat systems that bypass it break the Community Standards.
- **Any player-written text shown to other players must be filtered** with `TextService:FilterStringAsync`, even if it
isn't chat (pet names, signs, bulletin boards, text loaded from DataStores). Games found not filtering get removed
until they add it.
- **Editable shared text (signs, boards) needs a rate limit of at least 1 minute** and can't be used for two-way
conversation.
- **Age checks gate chat.** Since January 2026 (December 2025 in Australia, New Zealand and the Netherlands), users must
complete an age check (Facial Age Estimation, or ID for 13+) to use chat. Roblox's November 2025 announcement lists
the age groups Under 9, 9-12, 13-15, 16-17, 18-20 and 21+ (Roblox says groups may vary by region). Users chat with
their own and similar age groups; age-checked users 13+ can also chat with Trusted Connections. Chat in experiences
is off by default for under-9s without parental consent.
- **What players see:** a message to someone who can't chat with the sender just doesn't appear for them. Many players
in a server can't talk to each other, so don't make core gameplay depend on free chat.
- **Preset systems** (chat wheels) are for gameplay coordination only: labeled "system preset", visually distinct from
chat, no terminal punctuation, every preset passed through `FilterStringAsync`, 10 seconds between sends, 12 or fewer
presets per universe, nothing that can spell out messages. Prefer Roblox's built-in **Quick Words** (Creator Hub >
your game > Configure > Chat > Quick Words: up to 3 categories of 3-10 phrases, each under 25 characters).
- **Cross-server chat** (the Global tab) is on by default; toggle it in Creator Dashboard > Audience > Communication
Settings. It is disabled in private servers.
- **Platform-integrated chat** is an opt-in `TextChatService.PlatformIntegratedChat` setting today and becomes
**required for all games in May 2027**. When on, message text is unreadable by your code
(`TextChatMessage.Text`, `PrefixText` and `Translation` return `""`), `OnIncomingMessage` and `Player.Chatted` can't
drive commands, and custom chat UIs must use `TextChannelWindow`. Build new chat features so they keep working:
commands as `TextChatCommand`, no reading message text, no custom prefixes.
- Creators may not show social media links in experiences (announced November 2025).
## API
### Setup
`TextChatService.ChatVersion` must be `TextChatService`. With `CreateDefaultTextChannels` on, the server creates
`TextChatService.TextChannels` with `RBXGeneral`, `RBXSystem`, `RBXTeam<BrickColor>` per team colour, and
`RBXWhisper:<id>_<id>` channels. `CreateDefaultCommands` adds `/help`, `/mute`, `/team`, `/whisper`, `/e` and others.
| Member | Side | Use |
|---|---|---|
| `TextChannel:SendAsync(text, metadata?)` | client | send a message the user typed |
| `TextChannel:DisplaySystemMessage(text, metadata?)` | client | local-only system line (not filtered, not seen by others) |
| `TextChannel:AddUserAsync(userId)` | server | add a user to a custom channel; returns `nil, false` if they can't chat |
| `TextChannel:SetDirectChatRequester(player)` | server | required for channels you create for direct chat |
| `TextChannel.ShouldDeliverCallback` | server | per receiver; return truthy to deliver |
| `TextChatService.OnIncomingMessage` | client | decorate messages (define once) |
| `TextChatService:DisplayBubble(partOrCharacter, text)` | client | bubbles for NPCs |
| `TextChatService.BubbleChatConfiguration` | Studio/client | `Enabled`, `MaxDistance`, `BubbleDuration` (default 30), `MaxBubbles`, `TextSize`, `FontFace`, colours |
### Who can chat (age checks, settings, blocks)
All yield; wrap them in `pcall`.
| Method | Side | Returns |
|---|---|---|
| `TextChatService:CanUserChatAsync(userId)` | server (client: local player only) | can this user chat at all |
| `TextChatService:CanUsersChatAsync(fromUserId, toUserId)` | server | can these two exchange messages |
| `TextChatService:CanUsersDirectChatAsync(requesterUserId, { userIds })` | server | the users who can direct chat with the requester |
| `TextChatService:GetChatGroupsAsync(playerList)` | server | arrays of chat group ids; players sharing an id can chat |
`GetChatGroupsAsync` needs **Chat & Voice Groups APIs** enabled in Experience Settings > Communication and only works in
Team Test in Studio. Use it for matchmaking and teleports instead of pairwise checks. Don't store the ids after a
player leaves.
```luau
-- Server: show a hint to players who can't chat, instead of a broken social feature
local ok, canChat = pcall(function()
return TextChatService:CanUserChatAsync(player.UserId)
end)
player:SetAttribute("CanChat", ok and canChat == true)
```
### Admin commands
```luau
-- Server Script
local Players = game:GetService("Players")
local TextChatService = game:GetService("TextChatService")
local ADMINS = { [1234567] = true } -- UserIds
local command = Instance.new("TextChatCommand")
command.Name = "KickCommand"
command.PrimaryAlias = "/kick" -- matching is case-insensitive; the message is not sent to others
command.Parent = TextChatService
command.Triggered:Connect(function(source: TextSource, text: string)
if not ADMINS[source.UserId] then
return
end
local targetName = string.split(text, " ")[2]
local target = if targetName then Players:FindFirstChild(targetName) else nil
if target and target:IsA("Player") then
target:Kick("Removed by an admin")
end
end)
```
Check permissions on the server with `source.UserId`. Never parse `Player.Chatted` or message text for commands.
### Proximity chat
```luau
-- Server
local Players = game:GetService("Players")
local TextChatService = game:GetService("TextChatService")
local general = TextChatService:WaitForChild("TextChannels"):WaitForChild("RBXGeneral") :: TextChannel
local function rootOf(userId: number): BasePart?
local player = Players:GetPlayerByUserId(userId)
local character = player and player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
return if root and root:IsA("BasePart") then root else nil
end
general.ShouldDeliverCallback = function(message: TextChatMessage, receiver: TextSource)
local source = message.TextSource
local a = source and rootOf(source.UserId)
local b = rootOf(receiver.UserId)
return a ~= nil and b ~= nil and (a.Position - b.Position).Magnitude <= 60
end
```
This only adds restrictions on top of Roblox's own; it can't let incompatible users talk. The sender always sees their
own message.
### Chat tags (client)
```luau
local Players = game:GetService("Players")
local TextChatService = game:GetService("TextChatService")
TextChatService.OnIncomingMessage = function(message: TextChatMessage)
local properties = Instance.new("TextChatMessageProperties")
local source = message.TextSource
local player = source and Players:GetPlayerByUserId(source.UserId)
if player and player:GetAttribute("VIP") then
properties.PrefixText = `<font color="#F5CD30">[VIP]</font> {message.PrefixText}`
end
return properties
end
```
Prefixes stop working under platform-integrated chat; keep them cosmetic.
### Filtering custom text (server)
```luau
local TextService = game:GetService("TextService")
local function filterForEveryone(text: string, fromUserId: number): string?
local ok, filtered = pcall(function()
return TextService:FilterStringAsync(text, fromUserId):GetNonChatStringForBroadcastAsync()
end)
return if ok then filtered else nil -- on failure, show nothing
end
```
- `FilterStringAsync` yields, retries internally (don't retry), and errors if `fromUserId` isn't in this server.
- `GetNonChatStringForBroadcastAsync()` for text everyone sees; `GetNonChatStringForUserAsync(toUserId)` for one viewer.
- Filter once when the player submits, not per keystroke. Store the raw text; filter again when showing it later.
## Mistakes to avoid
- Building a custom chat or "message" feature on RemoteEvents instead of `TextChannel`s.
- Showing pet names, sign text, team names or saved text to others without `FilterStringAsync`.
- Signs or boards without a 1-minute rate limit per player.
- Commands built on `Player.Chatted`, `MessageReceived` or `message.Text` (they break under platform-integrated chat).
- Calling `CanUsersChatAsync` or `CanUsersDirectChatAsync` from the client (server only).
- Assuming everyone in a server can read each other. Design co-op so it works with Quick Words or no chat.
- A preset wheel or alphabet/number picker that lets players compose messages.
- `ShouldDeliverCallback` that returns nil on some path (nobody receives those messages).
- Setting `TextChatService.OnIncomingMessage` in more than one script; keep one owner.
Sources: create.roblox.com/docs/chat/guidelines, /chat/platform-integrated-chat, /chat/preset-system-guidelines,
/chat/quick-words, /chat/cross-server-chat, /ui/text-filtering; the TextChatService and TextChannel API reference;
Roblox newsroom (November 19, 2025 and January 7, 2026); DevForum "Age Check Requirement to Chat Now Live Globally"
and the March 26, 2026 fast-follow roadmap.