Go to Slack

Threading messages together

Messages are islands floating in a sea of channels. Most stand alone, seen or unseen, but some generate such volcanic conversation that replies flow from an inciting message, forming a kind of archipelagic thread in its wake.

This is the story of how messages link together and how your app can read, write, and party with threads.

a threaded message, with replies waiting. "What happened to batch 41?"
A gorgeous message

Let's get knitting.

Forking conversations

A channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps. When one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not appear directly in the channel, instead relegated to a kind of forked timeline descending from the parent message.

Even though Slack clients treat replies this way, messages are delivered the same old way over RTM, Events API, or methods like channels.history. Easily identify & differentiate parent messages and replies — everything is thoughtfully marked.

Not familiar with threads yet? Consult our user-facing help center article.

Threading the needle

Threads are a big enough deal that we're going to wordily wrench and write all the hows and whys in this big long document.

But here is a shortcut through these hedges of threaded knowledge:

  1. Recognize threaded messages by looking for a thread_ts attribute: any message with a thread_ts attribute is part of a thread!
  2. Identify replies by noting when a threaded message's ts and thread_ts values differ. If they're equal, you're dealing with a parent.
  3. Attach your messages to a thread by specifying another message's ts value as your reply's thread_ts.
  4. When responding to a message that's part of a thread, you should keep your responses in the thread.
  5. When responding to a non-threaded message, consider creating a thread by replying to the message inciting your response.

Fun facts

Short on time, trying to follow the five steps above and don't want to read everything else written here?

These are the facts:

  • Message threads may have only one parent, they can only fork once.
  • Replies can't spawn from replies and must tie back to a single originating message.
  • Parent messages may have zero or any number of replies.
  • Threaded messages can be found in channels, private channels, group messages, and direct messages.
  • Users may "subscribe" to specific threads, but their subscriptions have no effect on whether threaded messages are sent via the RTM or Events APIs.
  • Replies are presented chronologically in the Web API, Events API, and RTM API, just like any old message.
  • Your app needs to parse thread_ts fields to understand whether a message is part of a thread.
  • To retain continuity, your bot's responses to in-thread invocations should stay within the thread.
  • Replies may contain attachments and message buttons, though there are some quirks.

Finding message threads in the wild

You'll discover threads treading through our Web, RTM, and Events APIs. Here's what to look for:

The message thread data model

Messages that are part of a thread will contain a thread_ts attribute, populated with a string timestamp-style identifier for the thread in a specific channel or conversation.

Example thread_ts (or ts) values include:

  • 1485051909.018632
  • 1482960137.003543
  • 1924992000.023230
  • 978307200.129921

These IDs are guaranteed unique within the context of a channel or conversation. They look like UNIX/epoch timestamps with specified milliseconds. They'll even sort like the same. But they're message IDs, even if they're partially composed in seconds-since-the-epoch.

Parent messages

A message that spawns a thread is called a parent. Its replies are its children.

When a message has a thread_ts value and it matches the same message's ts, the message is a thread parent.

The message will also contain a replies array with references to all the messages acting as replies to this parent.

A parent message remains a thread with a thread_ts and replies array even if all of its replies have been deleted.

Child messages, or replies

Child messages are like any other message, with their own channel-unique ts value. They'll also contain a thread_ts attribute, pointing to the ts value of the parent message. Child messages are replies to the parent.

When a message has a thread_ts and it differs from the same message's ts, the message is a reply.

A true parent's thread_ts should be used when replying. Providing a child's message ID will result in a new, detached thread breaking all context and sense. Imagine trying to read all this Gertrude Stein out of order?

Thread representation in RTM & Events API

Since replies are just messages, if you subscribe or listen to message events in both of our event-driven APIs, you will receive them. We also include some bonus message subtype events, letting your app know how the thread is knitting together.

1. Parent messages arrive as usual

You just don't know they're parent to a threaded conversation until it happens... They're messages just like any other message, ripe with potential to become conversation-starters.

{
        "type": "message",
        "channel": "C061EG9SL",
        "user": "U061F7TRS",
        "text": "Was there was there was there what was there was there what was there was there there was there.",
        "ts": "1482960137.003543",
        "source_team": "T061EG9R6"
}

Just an ordinary message, same as practically any other message. It's not a thread-bearer yet.

2. The dispatch of reply messages is also typical

A fellow team member found that Gertrude Stein quote intriguing, and to prove their cleverness adds another quote from the same piece (If I Told Him (A Completed Portrait of Picasso)).

Your stream receives this message and its notable attribute thread_ts pointing back to the originating message:

{
        "type": "message",
        "channel": "C061EG9SL",
        "user": "U061F7AUR",
        "text": "Shutters shut and shutters and so shutters shut and shutters and so and so shutters and so shutters shut and so shutters shut and shutters and so.",
        "ts": "1483037603.017503",
        "source_team": "T061EG9R6",
        "thread_ts": "1482960137.003543"
}

Our first message, the one known as "1482960137.003543", is now the happy parent of "1483037603.017503".

3. Events emit signifying the original message's updated status

Slack will send a message subtype event, message_replied. This event is about the parent message ("1482960137.003543" in the above example), describing how that message's metadata has changed to include an additional reply. This subtype's inner message object will contain an incremented reply_count and you should find a reference to the new reply in its replies array. See the field guide below for more detail on these mostly self-descriptive fields.

{
        "type": "message",
        "message": {
                "type": "message",
                "user": "U061F7TRS",
                "text": "Was there was there was there what was there was there what was there was there there was there.",
                "thread_ts": "1482960137.003543",
                "reply_count": 1,
                "replies": [
                        {
                                "user": "U061F7AUR",
                                "ts": "1483037603.017503"
                        }
                ],
                "ts": "1482960137.003543"
        },
        "subtype": "message_replied",
        "hidden": true,
        "channel": "C061EG9SL",
        "event_ts": "1483037604.017506",
        "ts": "1483037604.017506"
}

You may also notice thread_subscribed, thread_unsubscribed, thread_marked update_thread_state event types. These events are used by Slack clients to manage user interest in threads and is not yet meant for developer use.

Retrieving message replies

If you prefer to work with threaded conversations more holistically and deliberately, we provide a suite of Web API methods suitable for retrieving the entire conversation thread from a channel, private channel, or direct message.

While Slack app-based bot users can't access these methods directly, user tokens granted with the correlating scope can.

Each forked timeline method's response format is similar and includes both the parent message and its replies. Here's an example response from channels.replies:

{
    "ok": true,
    "messages": [
        {
            "type": "message",
            "user": "U061F7TRS",
            "text": "Was there was there was there what was there was there what was there was there there was there.",
            "thread_ts": "1482960137.003543",
            "reply_count": 2,
            "replies": [
                {
                    "user": "U061F7AUR",
                    "ts": "1483037603.017503"
                },
                {
                    "user": "U061F7TRS",
                    "ts": "1483051909.018632"
                }
            ],
            "subscribed": true,
            "last_read": "1483051909.018632",
            "unread_count": 0,
            "ts": "1482960137.003543"
        },
        {
            "type": "message",
            "user": "U061F7AUR",
            "text": "Shutters shut and shutters and so shutters shut and shutters and so and so shutters and so shutters shut and so shutters shut and shutters and so.",
            "thread_ts": "1482960137.003543",
            "parent_user_id": "U061F7TRS",
            "ts": "1483037603.017503"
        },
        {
            "type": "message",
            "user": "U061F7TRS",
            "text": "Let me recite what history teaches. History teaches.",
            "thread_ts": "1482960137.003543",
            "parent_user_id": "U061F7TRS",
            "ts": "1483051909.018632"
        }
    ]
}

And that's how you read and understand message threads.


Becoming party to threads

Your bot user or properly-scoped user token can join the fun of contributing to message threads too.

Posting replies

Today there are two ways to reply to messages: using the Web API and using the RTM API.

Using the Web API

Use chat.postMessage to post replies by specifying a thread_ts parameter pointing to the ts value for the message being replied to. The message will become part of that thread.

Our @gertrude_stein bot chimes in using chat.postMessage:

POST /api/chat.postMessage
Host: slack.com
Content-type: application/x-www-form-urlencoded

text=I%20judge%20judge.&channel=C061EG9SL&thread_ts=1482960137.003543

And this is what we get back in response:

{
    "ok": true,
    "channel": "C061EG9SL",
    "ts": "1483116860.020084",
    "message": {
        "text": "I judge judge.",
        "username": "gertrude_stein",
        "bot_id": "B012345AB6",
        "type": "message",
        "subtype": "bot_message",
        "thread_ts": "1482960137.003543",
        "parent_user_id": "U061F7TRS",
        "ts": "1483116860.020084"
    }
}

We've thrown our coin into this little conversation fountain.

Using the RTM API

If using a websocket connection and want to send a simple reply by sending a JSON message, just add a thread_ts attribute to your JSON message object, pointing to the ts value for the message being replied to. The message will become part of that thread.

Here's another contribution from @gertrude_stein:

{
    "id": 1,
    "type": "message",
    "channel": "C061EG9SL",
    "text": "Exactly do they do.",
    "thread_ts": "1482960137.003543"
}

And in response, our websocket answers:

{
    "ok": true,
    "reply_to": 1,
    "ts": "1483125339.020269",
    "text": "Exactly do they do.",
    "thread_ts": "1482960137.003543"
}

Posting your reply back to the channel

Set the reply_broadcast boolean parameter to true to indicate your reply is germane to all members of a channel. By default, reply_broadcast is set to false. Your message will still be considered part of the thread represented by thread_ts.

When your reply is broadcast to the channel, it'll actually be a reference to your reply, not the reply itself. So, when appearing in the channel, it won't contain any attachments or message buttons.

The broadcast message looks something like this (here as a reply_broadcast event):

{
    "attachments": [
        {
            "from_url": "https://lost-generation-authors.slack.com/archives/general/p1482960137003543",
            "fallback": "[December 28th, 2016 1:22 PM] confused: what was there?",
            "ts": "1482960137.003543",
            "author_subname": "confused",
            "channel_id": "C061EG9SL",
            "channel_name": "general",
            "text": "island",
            "author_link": "https://lost-generation-authors.slack.com/team/confused",
            "author_icon": "https://...png",
            "mrkdwn_in": [
                "text"
            ],
            "id": 1,
            "footer": "5 replies"
        },
        {
            "fallback": "Was there was there what was there?",
            "author_subname": "gertrude_stein",
            "text": "Was there was there what was there?",
            "mrkdwn_in": [
                "text"
            ],
            "author_link": "https://lost-generation-authors.slack.com/team/gertrude_stein",
            "ts": "1484678597.521003",
            "author_icon": "https://...png",
            "id": 2
        }
    ],
    "type": "message",
    "subtype": "reply_broadcast",
    "user": "U061F7AUR",
    "ts": "1484678597.521010",
    "channel": "C061EG9SL",
    "event_ts": "1484678597.521010"
}

Party hardier with interactive messages

Building evolving workflows without disturbing the whole channel or moving to DMs is now possible with threads. Bots can reply to messages and create threads while including attachments and message buttons.

When creating a new message as part of a message action response, create a reply by specifying a thread_ts attribute, whether responding directly or by using a response_url. The thread_ts must point to the same thread that the message button appears in. Your follow up message will become part of the thread.

Messages posted with reply_broadcast set to true won't render in-channel with message buttons; use a non-threaded message to post message buttons to an entire channel.

Best practices

  • If receiving some kind of invocation (like a keyword or an @mention) within a threaded message, your bot user should respond within the same thread by specifying its thread_ts.
  • Don't specify a message that is already a reply's ts value as a new message's thread_ts. Specify the parent message.
  • Tactfully use reply_broadcast when your bot's reply is useful for everyone in a channel. Keep in mind these broadcast replies will not contain the message's attachments or message buttons, but instead as a reference to your bot's actual message.
  • Consider moving your multi-step workflows to a thread format, still "public" but contained in a forked conversation.

Field guide

Here are the most pertinent fields related to reading and writing threaded messages. Of course many other message fields are important too (we're looking at you, text), and you're welcome to review all the message documentation.

Field Type Description
ts string The unique identifier of a message. When the same message contains a thread_ts that is equal to this ts value, the message is the parent of a thread and may contain zero or more replies. When replying to a parent message, this ts value is used for the thread_ts parameter.
thread_ts string A unique identifier of an originating message of a thread. When ingesting messages, this attribute indicates that the message is either the parent of a thread or a reply to it.
parent_user_id string The user ID belonging to the team member that authored the parent message in a thread. This field only appears attached to replies. Only included in replies transmitted via Web API methods.
reply_count integer When present, indicates `0` or more replies to the parent message.
replies array

A collection of zero or more references to a parent message's threaded children messages.

Each reply in the array will be a JSON hash containing two attributes.

  • user — The user ID belonging to the team member authoring this reply.
  • ts — The unique identifier for this specific reply message.

See Retrieving message replies to learn how to obtain further messages.

Related documentation

Web API methods

Event types