---
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.
