# Threads & Replies

Threads allow users to reply to specific messages without cluttering the main channel conversation. A thread is created when a message is sent with a `parent_id` referencing another message.

## Starting a Thread

Send a message with a `parent_id` to start a thread or add a reply to an existing thread.

```csharp label="Unity"
var parentMessage = await channel.SendNewMessageAsync("Starting a thread");

var reply = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
  ParentId = parentMessage.Id,
  ShowInChannel = false,
  Text = "This is a reply in a thread",
});
```

### Thread Parameters

| Name            | Type    | Description                                                        | Default | Optional |
| --------------- | ------- | ------------------------------------------------------------------ | ------- | -------- |
| parent_id       | string  | ID of the parent message to reply to                               |         |          |
| show_in_channel | boolean | If true, the reply appears both in the thread and the main channel | false   | ✓        |

<Admonition type="info">

Messages in threads support the same features as regular messages: reactions, attachments, and mentions.

</Admonition>

## Paginating Thread Replies

When querying a channel, thread replies are not included by default. The parent message includes a `reply_count` field. Use `getReplies` to fetch thread messages.

```csharp label="Unity"
// Get the latest 20 replies (oldest-first)
var replies = await parentMessage.LoadRepliesAsync(limit: 20);

// Get older replies (before message with id "42")
var olderReplies = await parentMessage.LoadRepliesAsync(limit: 20, idLessThan: "42");
```

## Inline Replies

Reply to a message inline without creating a thread. The referenced message appears within the new message. Use `quoted_message_id` instead of `parent_id`.

```csharp label="Unity"
var message = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
  QuotedMessage = originalMessage,
  Text = "I agree with this point",
});
```

When querying messages, the `quoted_message` field is automatically populated:

```json
{
  "id": "new-message-id",
  "text": "I agree with this point",
  "quoted_message_id": "original-message-id",
  "quoted_message": {
    "id": "original-message-id",
    "text": "The original message text"
  }
}
```

<Admonition type="warning">

Inline replies are only available one level deep. If Message A replies to Message B, and Message B replies to Message C, you cannot access Message C through Message A. Fetch Message B directly to access its referenced message.

</Admonition>

## Thread List

Query all threads that the current user participates in. This is useful for building thread list views similar to Slack or Discord.

### Querying Threads

Threads are returned with unread replies first, sorted by the latest reply timestamp in descending order.

```csharp label="Unity"
var response = await Client.QueryThreadsAsync(new StreamQueryThreadsRequest
{
  Watch = true,
  Limit = 10,
});

foreach (var thread in response.Threads)
{
  // Threads are cached, watched and kept in sync with realtime events
  Debug.Log(thread.ParentMessage.Text);
  Debug.Log(thread.LatestReplies);
  Debug.Log(thread.ThreadParticipants);
  Debug.Log(thread.Read);
}
```

<Disclosure label="Example Response">

```json
{
  "threads": [
    {
      "channel_cid": "messaging:general",
      "channel": {
        "id": "general",
        "type": "messaging",
        "name": "General"
      },
      "parent_message_id": "parent-123",
      "parent_message": {
        "id": "parent-123",
        "text": "Original message",
        "type": "regular"
      },
      "created_by_user_id": "user-1",
      "reply_count": 5,
      "participant_count": 3,
      "thread_participants": [
        {
          "user_id": "user-1",
          "user": { "id": "user-1", "name": "Alice" }
        },
        {
          "user_id": "user-2",
          "user": { "id": "user-2", "name": "Bob" }
        }
      ],
      "last_message_at": "2024-12-11T15:30:00Z",
      "latest_replies": [
        {
          "id": "reply-1",
          "text": "Latest reply",
          "type": "reply"
        }
      ],
      "read": [
        {
          "user": { "id": "user-1" },
          "last_read": "2024-12-11T15:00:00Z",
          "unread_messages": 2
        }
      ]
    }
  ]
}
```

</Disclosure>

### Query Options

| Name              | Type    | Description                                       | Default | Optional |
| ----------------- | ------- | ------------------------------------------------- | ------- | -------- |
| reply_limit       | number  | Number of latest replies to fetch per thread      | 2       | ✓        |
| participant_limit | number  | Number of thread participants to fetch per thread | 100     | ✓        |
| limit             | number  | Maximum number of threads to return               | 10      | ✓        |
| watch             | boolean | If true, watch channels for the returned threads  | true    | ✓        |
| member_limit      | number  | Number of members to fetch per thread channel     | 100     | ✓        |

### Filtering and Sorting

Filter and sort threads using MongoDB-style [query operators](https://getstream.io/docs/platform/query-syntax-operators/).

#### Supported Filter Fields

| Field                | Type                      | Operators                           | Description               |
| -------------------- | ------------------------- | ----------------------------------- | ------------------------- |
| `channel_cid`        | string or list of strings | `$eq`, `$in`                        | Channel CID               |
| `channel.disabled`   | boolean                   | `$eq`                               | Channel disabled status   |
| `channel.team`       | string or list of strings | `$eq`, `$in`                        | Channel team              |
| `parent_message_id`  | string or list of strings | `$eq`, `$in`                        | Parent message ID         |
| `created_by_user_id` | string or list of strings | `$eq`, `$in`                        | Thread creator's user ID  |
| `created_at`         | string (RFC3339)          | `$eq`, `$gt`, `$lt`, `$gte`, `$lte` | Thread creation timestamp |
| `updated_at`         | string (RFC3339)          | `$eq`, `$gt`, `$lt`, `$gte`, `$lte` | Thread update timestamp   |
| `last_message_at`    | string (RFC3339)          | `$eq`, `$gt`, `$lt`, `$gte`, `$lte` | Last message timestamp    |

#### Supported Sort Fields

- `active_participant_count`
- `created_at`
- `last_message_at`
- `parent_message_id`
- `participant_count`
- `reply_count`
- `updated_at`

Use `1` for ascending order and `-1` for descending order.

```csharp label="Unity"
var since = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);

var request = new StreamQueryThreadsRequest
{
  Filter = new IFieldFilterRule[]
  {
    ThreadFilter.CreatedByUserId.EqualsTo("user-1"),
    ThreadFilter.UpdatedAt.GreaterThanOrEquals(since),
  },
  Sort = ThreadSort.OrderByDescending(ThreadSortFieldName.CreatedAt),
  Limit = 10,
};

var page1 = await Client.QueryThreadsAsync(request);

// Get next page using the cursor returned by the previous response
if (!string.IsNullOrEmpty(page1.Next))
{
  request.Next = page1.Next;
  var page2 = await Client.QueryThreadsAsync(request);
}
```

### Getting a Thread by ID

Retrieve a specific thread using the parent message ID.

```csharp label="Unity"
// The returned IStreamThread is auto-watched (watch defaults to true) and stays in
// sync with realtime events via Updated / ReplyReceived / ReadStateChanged.
var thread = await Client.GetThreadAsync("parent-message-id",
    replyLimit: 10, participantLimit: 25);
```

### Updating Thread Title and Custom Data

Assign a title and custom data to a thread.

```csharp label="Unity"
var thread = await Client.GetThreadAsync("parent-message-id");

// Set title and custom fields; unset a previously set field
await thread.UpdatePartialAsync(
    setFields: new Dictionary<string, object>
    {
        { "title", "Project Discussion" },
        { "priority", "high" },
    },
    unsetFields: new[] { "priority" });
```

## Thread Unread Counts

### Total Unread Threads

The total number of unread threads is available after connecting.

```csharp label="Unity"
// Available immediately after connect on IStreamLocalUserData
var unreadThreads = Client.LocalUserData.UnreadThreads;
Debug.Log(unreadThreads);

// The same total is also available on demand from the server
var unreadCounts = await Client.GetLatestUnreadCountsAsync();
Debug.Log(unreadCounts.TotalUnreadThreadsCount);
```

### Marking Threads as Read or Unread

```csharp label="Unity"
var thread = await Client.GetThreadAsync("parent-message-id");

// Mark this thread as read for the local user
await thread.MarkReadAsync();

// Mark this thread as unread starting from the parent message
await thread.MarkUnreadAsync();

// Equivalent helpers from the parent message of the thread
IStreamMessage parentMessage = thread.ParentMessage;
await parentMessage.MarkThreadAsReadAsync();
await parentMessage.MarkThreadAsUnreadAsync();

// Or by parent message id when you already have the channel
await thread.Channel.MarkThreadAsReadAsync(thread.ParentMessageId);
await thread.Channel.MarkThreadAsUnreadAsync(thread.ParentMessageId);
```

### Unread Count Per Thread

```csharp label="Unity"
var unreadCounts = await Client.GetLatestUnreadCountsAsync();

Debug.Log(unreadCounts.TotalUnreadThreadsCount);

foreach (var thread in unreadCounts.UnreadThreads)
{
  Debug.Log(thread.ParentMessageId);
  Debug.Log(thread.UnreadCount);
  Debug.Log(thread.LastRead);
  Debug.Log(thread.LastReadMessageId);
}
```

## Thread Manager

The `ThreadManager` class provides built-in pagination and state management for threads.

```csharp label="Unity"
// Get notified when a thread starts or stops being tracked locally
Client.ThreadTracked += thread => { /* a new IStreamThread is now tracked */ };
Client.ThreadUntracked += thread => { /* an IStreamThread is no longer tracked */ };

// Load threads. Watch defaults to true so realtime updates are delivered.
var response = await Client.QueryThreadsAsync(new StreamQueryThreadsRequest
{
  Watch = true,
  Limit = 10,
});

// Each returned IStreamThread is stateful: it is cached and kept in sync with
// realtime events automatically. Subscribe to per-thread events to react to
// changes (e.g. new replies, title / custom data updates, read state changes).
foreach (var thread in response.Threads)
{
  thread.Updated += changedThread => { /* title or custom data changed */ };
  thread.ReplyReceived += (changedThread, reply) => { /* new reply arrived */ };
  thread.ReadStateChanged += changedThread => { /* unread state changed */ };
}
```

### Event Handling

Register subscriptions to receive real-time updates for threads.

```js
const { threads } = await client.queryThreads({ watch: true, limit: 10 });
const [thread] = threads;

// Register event handlers for a single thread
thread.registerSubscriptions();

const unsubscribe = thread.state.subscribe((state) => {
  console.log(state.replies);
});
```

<Admonition type="info">

The `watch` parameter is required when querying threads to receive real-time updates.

</Admonition>

For `ThreadManager`, call `registerSubscriptions` once to automatically manage subscriptions for all loaded threads:

```js
const threadManager = client.threads;
threadManager.registerSubscriptions();

await threadManager.loadNextPage();

// All threads are now listening to channel events
const { threads } = threadManager.state.getLatestValue();
```


---

This page was last updated at 2026-09-08T08:33:18.542Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/unity/threads/](https://getstream.io/chat/docs/unity/threads/).