Direct Messages & Group Chats
Find who you can message, start a 1:1 or group conversation, and manage group membership and admins with startDirectMessage, createGroupChat, and the membership mutations.
Direct messages and group chats are Discussion rows — the same type that already backed workspace chat threads — distinguished by kind: CHANNEL for a workspace chat, DM for a 1:1, GROUP for a named multi-person conversation. A DM/group Discussion has project: null (it has no workspace anchor) and carries a members: [ChatMember!]! list that a CHANNEL discussion does not use. Replies in any of the three are posted the same way — as Comment rows with category: DISCUSSION — see Create, edit & delete comments.
messageableUsers
Before starting a conversation, use messageableUsers to find who you’re allowed to message. Organization OWNER/ADMIN can message anyone in the organization; everyone else can message organization OWNER/ADMINs plus anyone they share at least one active workspace with. The same rule is enforced server-side on startDirectMessage, createGroupChat, and addGroupChatMembers — this query only lists it.
Request
query WhoCanIMessage {
messageableUsers(query: "pri", first: 25) {
id
fullName
email
}
}Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | String | No | — | Filter by name (first or last). Matched on name only, not email. |
first | Int | No | 25 | Maximum number of users to return, clamped to 100. |
Response
{
"data": {
"messageableUsers": [
{ "id": "clm4n8qwx000108l0a1b2c3d4", "fullName": "Priya Shah", "email": "priya@example.com" }
]
}
}messageableUsers returns [User!]! and never includes yourself.
startDirectMessage
Open (or return) the canonical 1:1 conversation with another user. Calling it again for the same pair of users returns the same Discussion rather than creating a duplicate — DMs are deduplicated per organization, keyed by the two user IDs regardless of who calls first.
Request
mutation MessagePriya {
startDirectMessage(userId: "user_456") {
id
kind
members {
user {
id
fullName
}
role
}
}
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | String! | Yes | The user to start (or resume) a DM with. |
Response
{
"data": {
"startDirectMessage": {
"id": "clm4n8qwx000308l0i9j0k1l2",
"kind": "DM",
"members": [
{ "user": { "id": "clm4n8qwx000108l0h2pyerm8", "fullName": "You" }, "role": "MEMBER" },
{
"user": { "id": "clm4n8qwx000108l0a1b2c3d4", "fullName": "Priya Shah" },
"role": "MEMBER"
}
]
}
}
}Returns
Returns the Discussion — either newly created or the existing DM between you and userId in this organization.
Errors
| Code | When |
|---|---|
FORBIDDEN | Direct messaging is disabled for the organization, or you don’t share a workspace with userId (and neither of you is an org OWNER/ADMIN). |
BAD_USER_INPUT | userId is your own ID, or doesn’t belong to a member of your organization. |
UNAUTHENTICATED | No valid token was supplied. |
createGroupChat
Create a named (or unnamed) group conversation. You become its first admin automatically.
Request
mutation StartLaunchCrew {
createGroupChat(input: { title: "Launch crew", userIds: ["user_456", "user_789"] }) {
id
kind
title
members {
user {
id
fullName
}
role
}
}
}Parameters
CreateGroupChatInput
| Parameter | Type | Required | Description |
|---|---|---|---|
title | String | No | Display name. Left blank (or omitted), the group has no title — clients typically render the member names instead. |
userIds | [String!]! | Yes | The other people to add. Your own ID is stripped automatically if included; duplicates are de-duplicated. |
A group needs at least one other person besides you, and at most 100 total members (a hard-coded ceiling — there’s no technical reason it couldn’t be higher, it’s there so a group chat can’t quietly become a second all-company channel). Unlike DMs, groups are never deduplicated by membership — calling createGroupChat twice with the same userIds creates two separate groups, told apart by title or recency.
Response
{
"data": {
"createGroupChat": {
"id": "clm4n8qwx000408l0m3n4o5p6",
"kind": "GROUP",
"title": "Launch crew",
"members": [
{ "user": { "id": "clm4n8qwx000108l0h2pyerm8", "fullName": "You" }, "role": "ADMIN" },
{
"user": { "id": "clm4n8qwx000108l0a1b2c3d4", "fullName": "Priya Shah" },
"role": "MEMBER"
},
{
"user": { "id": "clm4n8qwx000508l0mtu2ivpc", "fullName": "Sarah Mensah" },
"role": "MEMBER"
}
]
}
}
}Returns
Returns the created Discussion with kind: GROUP.
Errors
| Code | When |
|---|---|
FORBIDDEN | Direct messaging is disabled for the organization, or you don’t share a workspace with one of userIds (and neither of you is an org OWNER/ADMIN). |
BAD_USER_INPUT | userIds (after removing your own ID and duplicates) is empty, exceeds 100 total members, or references someone outside your organization. |
UNAUTHENTICATED | No valid token was supplied. |
addGroupChatMembers
Add people to an existing group. Admin-only. A person re-added after being removed regains access to the entire message history, exactly like a brand-new member — their role also resets to MEMBER, even if they were an admin before being removed.
Request
mutation AddToLaunchCrew {
addGroupChatMembers(input: { discussionId: "discussion_123", userIds: ["user_999"] }) {
id
members {
user {
id
fullName
}
role
removedAt
}
}
}Parameters
GroupChatMembersInput
| Parameter | Type | Required | Description |
|---|---|---|---|
discussionId | String! | Yes | The group chat to add members to. |
userIds | [String!]! | Yes | The people to add. |
Response
{
"data": {
"addGroupChatMembers": {
"id": "clm4n8qwx000408l0m3n4o5p6",
"members": [
{
"user": { "id": "clm4n8qwx000608l0u1v2w3x4", "fullName": "New Person" },
"role": "MEMBER",
"removedAt": null
}
]
}
}
}Returns
Returns the updated Discussion.
Errors
| Code | When |
|---|---|
DISCUSSION_NOT_FOUND | No group chat matches discussionId. |
FORBIDDEN | You’re not an active admin of the group, discussionId isn’t a GROUP (e.g. it’s a DM), or direct messaging is disabled for the organization. |
BAD_USER_INPUT | userIds (after removing your own ID) is empty, the addition would exceed 100 total members, or a target isn’t messageable (outside your organization, or no shared workspace and not an org OWNER/ADMIN). |
UNAUTHENTICATED | No valid token was supplied. |
removeGroupChatMember
Remove someone from a group. Admin-only, and the row is soft-removed rather than deleted — the removed member keeps read-only access to everything up to the moment they were removed, but nothing after.
Request
mutation RemoveFromLaunchCrew {
removeGroupChatMember(input: { discussionId: "discussion_123", userId: "user_999" }) {
id
members {
user {
id
}
removedAt
}
}
}Parameters
GroupChatMemberInput
| Parameter | Type | Required | Description |
|---|---|---|---|
discussionId | String! | Yes | The group chat to remove someone from. |
userId | String! | Yes | The person to remove. |
To remove yourself, use leaveGroupChat instead — passing your own userId here is rejected.
Response
{
"data": {
"removeGroupChatMember": {
"id": "clm4n8qwx000408l0m3n4o5p6",
"members": [
{ "user": { "id": "clm4n8qwx000608l0u1v2w3x4" }, "removedAt": "2026-07-15T10:02:00.000Z" }
]
}
}
}Returns
Returns the updated Discussion.
Errors
| Code | When |
|---|---|
DISCUSSION_NOT_FOUND | No group chat matches discussionId. |
FORBIDDEN | You’re not an active admin of the group. |
BAD_USER_INPUT | userId is your own ID (use leaveGroupChat), or isn’t an active member of the group. |
UNAUTHENTICATED | No valid token was supplied. |
leaveGroupChat
Leave a group chat you’re a member of. Only group chats can be left — passing a DM’s discussionId is rejected, which is what makes DMs permanent for as long as both people remain in the organization.
If you’re the last active admin, the longest-standing remaining member (by when they joined this conversation) is automatically promoted to admin. If you’re the last member of any role, the conversation is deleted entirely.
Request
mutation LeaveLaunchCrew {
leaveGroupChat(discussionId: "discussion_123")
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
discussionId | String! | Yes | The group chat to leave. |
leaveGroupChat returns a bare Boolean!.
Response
{
"data": {
"leaveGroupChat": true
}
}When the last admin leaves, the replacement is the active member with the oldest ChatMember row in this conversation (i.e. whoever joined earliest), not the oldest Blue account. Ties are broken by an internal ordering, not join time, since a batch of members added in the same createGroupChat/addGroupChatMembers call can share an identical timestamp.
Errors
| Code | When |
|---|---|
DISCUSSION_NOT_FOUND | No group chat matches discussionId. |
FORBIDDEN | discussionId refers to a DM, not a group chat (only group chats can be left), or you were never a member. |
BAD_USER_INPUT | You’re not currently an active member (you already left or were removed). |
UNAUTHENTICATED | No valid token was supplied. |
setGroupChatMemberRole
Promote a member to admin, or demote an admin to member. Admin-only.
Request
mutation PromoteToAdmin {
setGroupChatMemberRole(
input: { discussionId: "discussion_123", userId: "user_456", role: ADMIN }
) {
id
members {
user {
id
}
role
}
}
}Parameters
SetGroupChatMemberRoleInput
| Parameter | Type | Required | Description |
|---|---|---|---|
discussionId | String! | Yes | The group chat. |
userId | String! | Yes | The member whose role to change. |
role | ChatMemberRole! | Yes | ADMIN or MEMBER. |
Setting a member’s role to the role they already have is a no-op — it returns the unchanged Discussion rather than erroring.
Response
{
"data": {
"setGroupChatMemberRole": {
"id": "clm4n8qwx000408l0m3n4o5p6",
"members": [{ "user": { "id": "clm4n8qwx000108l0a1b2c3d4" }, "role": "ADMIN" }]
}
}
}Returns
Returns the updated Discussion.
Errors
| Code | When |
|---|---|
DISCUSSION_NOT_FOUND | No group chat matches discussionId. |
FORBIDDEN | You’re not an active admin of the group. |
BAD_USER_INPUT | userId isn’t an active member, or the change would demote the group’s only remaining admin — every group must keep at least one. |
UNAUTHENTICATED | No valid token was supplied. |
The Discussion type
The fields relevant to DMs and group chats. Discussion also backs workspace chat threads (kind: CHANNEL) — see Create, update & delete discussions for that surface’s full field list.
| Field | Type | Description |
|---|---|---|
id | ID! | The conversation’s unique identifier. |
title | String! | Display name. Empty for an unnamed group — clients typically render the member names instead. |
kind | ChatKind! | CHANNEL (workspace chat), DM (1:1), or GROUP. |
project | Project | null for DM/GROUP — they have no workspace anchor. Set for CHANNEL. |
members | [ChatMember!]! | Membership rows for DM/GROUP conversations, including removed members (removedAt set). Unused for CHANNEL. |
people | [User!] | Participants in the thread. |
commentCount | Int! | Number of replies (Comment rows with category: DISCUSSION) in the thread. |
isRead | Boolean | Whether you’ve read this thread. Evaluated per caller. |
isSeen | Boolean | Whether you’ve seen this thread in a feed. Evaluated per caller. |
createdAt | DateTime! | When the conversation was created. |
updatedAt | DateTime! | When the conversation was last changed. |
ChatKind
enum ChatKind {
CHANNEL
DM
GROUP
}ChatMember
| Field | Type | Description |
|---|---|---|
id | ID! | The membership row’s identifier. |
user | User! | The member. |
role | ChatMemberRole! | ADMIN or MEMBER. |
createdAt | DateTime! | When this person joined the conversation. |
removedAt | DateTime | Set when the member was removed or left. null for an active member. |
addedBy | User | Who added them, when known. |
enum ChatMemberRole {
ADMIN
MEMBER
}Permissions
| Operation | Requires |
|---|---|
messageableUsers | Authenticated, active organization membership. |
startDirectMessage | Authenticated, active organization membership. Target must be messageable (see above). |
createGroupChat | Authenticated, active organization membership. Every invitee must be messageable. |
addGroupChatMembers | Active admin of the group. Every invitee must be messageable. |
removeGroupChatMember | Active admin of the group. |
leaveGroupChat | Authenticated, in-organization member of the group. (Does not require the organization’s license to be active — leaving is always allowed.) |
setGroupChatMemberRole | Active admin of the group. |
An organization can disable direct messaging entirely; when disabled, startDirectMessage, createGroupChat, and addGroupChatMembers all reject with FORBIDDEN.
Related
- Inbox & Messaging overview — how DM/group threads surface in the unified Inbox (
DM_THREAD). - Query your Inbox — read the conversations these mutations create.
- Manage inbox threads — mute, pin, and mark read/unread.
- Comments overview — post replies with
createComment(category: DISCUSSION, ...). - Query discussions — the read paths shared with workspace chat threads.
- Inbox subscriptions — stream new activity in real time.