[QUERY] How to unify thread_ts vs message_ts from a `MessagePosted` event?
Question
What I want to do is get any images from the exact message that my bot was tagged in. I have this trigger:
import type { Trigger } from "deno-slack-sdk/types.ts";
import { TriggerContextData, TriggerTypes, TriggerEventTypes } from "deno-slack-api/mod.ts";
import { TagCapnWorkflow } from "../workflows/RespondToTag.ts";
const trigger: Trigger = {
type: TriggerTypes.Event,
name: "Ask Capn",
description: "use @capn to tag Capn in a channel message",
workflow: `#/workflows/${TagCapnWorkflow.id}`,
event: {
event_type: TriggerEventTypes.AppMentioned,
all_resources: true,
},
inputs: {
user_id: {
value: TriggerContextData.Event.MessagePosted.user_id,
},
text: {
value: TriggerContextData.Event.MessagePosted.text,
},
channel_id: {
value: TriggerContextData.Event.MessagePosted.channel_id,
},
message_ts: {
// In the case this is a threaded message, this property becomes the message_ts of the parent message.
// Thus, if we are were tagged in a thread, this is the ts of the original message, not where we were tagged.
// TODO: should also pass in TriggerContextData.Event.MessagePosted.thread_ts,
// but this is sometimes null and therefore causes schema validation errors???
value: TriggerContextData.Event.MessagePosted.message_ts,
},
thread_ts: {
value: TriggerContextData.Event.MessagePosted.thread_ts,
},
},
};
export default trigger;
From reading the docs (well, intellisense on TriggerContextData.Event.MessagePosted.thread_ts) l, my expectation was
- if the bot is tagged in the a thread, then
- message_ts is the top-level message
- thread_ts is the specific message that the bot was tagged in. This is the mesage I want to look for files in.
- if the bot is tagged in top-level of the channel, then
- message_ts is a value (and is the message I want to look for files in)
- thread_ts is NULL.
OK, so then I have this workflow:
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { RespondToCapnTagFunction } from "../functions/respond_to_tag_capn/definition.ts";
export const TagCapnWorkflow = DefineWorkflow({
callback_id: "tag_capn",
title: "Tag Capn",
description:
"use @capn to tag Capn in a channel message",
input_parameters: {
properties: {
user_id: {
type: Schema.slack.types.user_id,
description: "The ID of the user who tagged Capn",
},
channel_id: {
type: Schema.slack.types.channel_id,
description: "The ID of the channel where Capn was tagged",
},
text: {
type: Schema.types.string,
description: "The text of the message where Capn was tagged",
},
message_ts: {
type: Schema.slack.types.message_ts,
description: "The timestamp of the message where Capn was tagged",
},
thread_ts: {
type: Schema.slack.types.message_ts,
description: "The timestamp of the thread where Capn was tagged",
}
},
required: ["user_id", "channel_id", "text", "message_ts"],
},
});
TagCapnWorkflow.addStep(RespondToCapnTagFunction, {
text: TagCapnWorkflow.inputs.text,
user_id: TagCapnWorkflow.inputs.user_id,
channel_id: TagCapnWorkflow.inputs.channel_id,
message_ts: TagCapnWorkflow.inputs.message_ts,
thread_ts: TagCapnWorkflow.inputs.thread_ts,
});
But, when I tag the bot in either the top level of a channel, OR in a thread, I get this error:
2025-10-23 14:03:14 [error] [Wf09MXNHK1K5] (Trace=Tr09NDTDF28L) Trigger for workflow 'Tag Capn' failed: parameter_validation_failed
2025-10-23 14:03:14 [error] [Wf09MXNHK1K5] (Trace=Tr09NDTDF28L) - Null value for non-nullable parameter `thread_ts`
Can you help me understand this? Can you give me a solution to my goal?
Environment
Paste the output of cat import_map.json | grep deno-slack
NA
Paste the output of deno --version
deno 2.3.3 (stable, release, aarch64-apple-darwin) v8 13.7.152.6-rusty typescript 5.8.3
Paste the output of sw_vers && uname -v on macOS/Linux or ver on Windows OS
ProductName: macOS ProductVersion: 15.6.1 BuildVersion: 24G90 Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000
Requirements
Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.
Oh, am I being silly, is it because I am triggering on AppMentioned, but I am trying to reference MessagePosted data fields?
I just found https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-event-triggers#response-object
It looks like the AppMentioned event contains:
{
"team_id": "T0123ABC",
"enterprise_id": "E0123ABC",
"event_id": "Ev0123ABC",
"event_timestamp": 1643810217.088700,
"type": "event",
"data": {
"app_id": "A1234ABC",
"channel_id": "C0123ABC",
"channel_name": "cool-channel",
"channel_type": "public/private/dm/mpdm",
"event_type": "slack#/events/app_mentioned",
"message_ts": "164432432542.2353",
"text": "<@U0LAN0Z89> is it everything a river should be?",
"user_id:": "U0123ABC",
}
}
and MessagePosted contains
{
"team_id": "T0123ABC",
"enterprise_id": "E0123ABC",
"event_id": "Ev0123ABC",
"event_timestamp": 1630623713,
"type": "event",
"data": {
"channel_id": "C0123ABC",
"channel_type": "public/private/dm/mpdm",
"event_type": "slack#/events/message_posted",
"message_ts": "1355517523.000005",
"text": "Hello world",
"thread_ts": "1355517523.000006", // Nullable
"user_id": "U0123ABC",
}
}
so they are ALMOST the same, except AppMentioned doesn't have thread_ts. Is this an oversight in the API? Can you adjust the API so that the AppMentioned ALSO has the thread_ts property?
I'm pretty sure this is wrong:
if the bot is tagged in the a thread, then message_ts is the top-level message thread_ts is the specific message that the bot was tagged in. This is the mesage I want to look for files in.
-
message_tsis always the timestamp of the message. -
thread_tsis the timestamp of the thread (if the message is in a thread,NULLotherwise).
So you should be able to deal with messages without having to worry about whether or not they're in a thread (unless you want to reply in that same thread).
~If you really need the thread_ts you could call the conversation.history API endpoint in order to retrieve a single message (based on the message_ts), which will return the thread_ts (if there is one). See documentation here.~
~Ah, nvm.... we need to call conversations.replies in order to retrieve messages from a thread, which requires the thread_id ahead of time. 🤦♂️~
~According to this, the only way to spot if a message is in a thread is via the thread_ts parameter. If that parameter is not provided, then we're out of luck. 😩~
Detect a threaded message by looking for a
thread_tsvalue in the message object. The existence of such a value indicates that the message is part of a thread.
Ok, actually, conversations.replies is the way to go!
It's very simple, using the ts of any message:
https://slack.com/api/conversations.replies?channel=C1C1C1C1C1&ts=1234567890.123456
- If the message is a regular message without thread/replies, the response will only contain
tsand nothread_ts. - If the message is a regular message with a thread/replies, the response will contain
tsas well asthread_ts, and both values will be the same (along with other things likereply_count,reply_users_count, andlatest_reply). - If the message is a reply within a thread, the response will contain
tsas well asthread_ts, and the values will be different (and there won't be any other things likereply_count,reply_users_count, andlatest_reply).
I posted the answer on StackOverflow (here), because I remember struggling with this in the past.
- message_ts is always the timestamp of the message.
- thread_ts is the timestamp of the thread (if the message is in a thread, NULL otherwise).
You are right, and that suggestion of conversations.replies works great! Thank you!
Going forward, some free ™️ docs suggestions from me:
What do you think about adding a big fat warning to https://docs.slack.dev/reference/methods/conversations.history that this does NOT include messages that are replies? I was using this because I found this section, that looked like a bullseye to me. But when I tried it, I kept getting 0 messages back when I queried a message that was a reply.
And maybe augmenting with conversations.replies docs with something along the lines of "use this instead of conversations.replies when..."
Also, this doc keeps talking about "Individual messages" which I think NOW are adding the "individual" to contrast against "threaded", but that is super not clear from just that article. I thought it was contrasting vs "multiple". Perhaps rename to "standalone" instead of "individual"? or otherwise be more clear here.
Also this doc correctly links to https://docs.slack.dev/messaging/retrieving-messages#finding_threads, (note the #finding_threads) anchor, BUT when I actually open that URL, the docs site (on a recent chrome) misleadingly scrolls to center the section on "individual messages". Note how the table of contents is corrently highlighted, but the viewport hasn't scrolled down enough.
This made me think I was supposed to read this section, which was a total red herring. So tell your docs team to fix this #anchor-section issue
I 💯% agree with your doc suggestions.
This has always been so confusing to me, and the doc has never been helpful at all. I had to figure it out via trial & error.
We shouldn't need a StackOverflow answer to sort out the confusion caused by the documentation. Even LLMs are confused by this and are hallucinating wrong answers.
conversations.replies is a much better way to retrieve individual messages (whether or not they are in a thread).
This is a pretty foundational feature of a messaging system, so you'd think the documentation would be solid, but it really needs some love/work.... 😖
I hope the Slack team sees this. 🤞
What do you think about adding a big fat warning to https://docs.slack.dev/reference/methods/conversations.history that this does NOT include messages that are replies? I was using this because I found this section, that looked like a bullseye to me. But when I tried it, I kept getting 0 messages back when I queried a message that was a reply.
YES! That is the most confusing part. And it's even worse: you will only get 0 messages back if the message/reply is the most recent in the entire channel. Otherwise, you might get an entirely different message (i.e. the most recent channel message after that timestamp).
Also, this doc keeps talking about "Individual messages" which I think NOW are adding the "individual" to contrast against "threaded", but that is super not clear from just that article. I thought it was contrasting vs "multiple". Perhaps rename to "standalone" instead of "individual"? or otherwise be more clear here.
The API example is also clearly broken, mentions "additional parameters" which are not in the example, and is using conversations.history, which will not work for messages within a thread:
You can read the conversations.history reference for a more in-depth explanation of what these additional parameters do, but essentially we're telling the API to return one result from the conversation's history, using the message ts as a starting point.
Also this doc correctly links to https://docs.slack.dev/messaging/retrieving-messages#finding_threads, (note the #finding_threads) anchor, BUT when I actually open that URL, the docs site (on a recent chrome) misleadingly scrolls to center the section on "individual messages". Note how the table of contents is correctly highlighted, but the viewport hasn't scrolled down enough.
More problems:
- The example provided is only valid is the message is the parent message of a thread.
- For messages without replies, it will not look like that at all.
- For replies within a thread, it will not look like that at all either.
- It is talking about receiving or retrieving messages without much additional guidance on how to do so (especially the retrieval part).
@NickCrews: Thank you for posting this issue by the way. You gave me the motivation to finally do something about all my internal frustration about this and finally figure it out for good. 🙏💙
It's hard to swim against the current when the documentation tells you to go one way, but we actually have to just ignore it and go in a different direction. 😖