Slack is fully awesome. At Wehkamp we use it for our internal communication and as a tool for our DevOps. The Slack API allows us to build even more advanced integrations. In this blog I'll explore how to use the API to create stuff like a powerful progress indicator, just by updating a Slack message:
Note from 2022
The first version of this article was written in 2018. We continue to use Slack bots in Node.js, but we've decided to use TypeScript now. We used to send text messages, but nowadays we richer messages using the Slack Block Kit. When you do, it no longer shows the (edited)
in your messages.
I've update the article with the new links and code to keep it fresh.
Promise the update
The Slack Web API has two methods we can use to either create or update messages:
- chat.postMessage - this will send a new message to the channel and give us a message identifier
ts
. We will store and reuse it to update the message. - chat.update - this will update the message that corresponds to the
ts
.
Let's create a function that does the creation or update:
import { ChatPostMessageArguments, ChatUpdateArguments, WebClient } from "@slack/web-api"
import { ChatPostMessageWebAPICallResult, ChatUpdateMessageWebAPICallResult, Message } from "./types"
async function sendMessage(webClient: WebClient, msg: ChatPostMessageArguments | ChatUpdateArguments) {
if (msg.ts) {
let args = msg as ChatUpdateArguments
let result = await webClient.chat.update(args)
return result as ChatUpdateMessageWebAPICallResult
}
msg.thread_ts = msg.thread_ts || ""
let args = msg as ChatPostMessageArguments
let result = await webClient.chat.postMessage(args)
return result as ChatPostMessageWebAPICallResult
}
The function returns the message identifier as a promise. The only thing we need to do is save the message identifier and reuse it when calling the function.
Asynchronous problems
The first problem I ran into had to do with messages not arriving in the same order that I expected. Sometimes I ended up with an earlier message as end-result. And sometimes I wanted to send an update for a message that had not even been created yet. If we want to work asynchronously we need to synchronize the way we send messages.
We need to build a class that will handle the process of sending messages. It will store the message identifier so we can update the message. The class should also make sure that only a single message is being send at the same time.
If a message is being send and a new message comes in, the class should store that new message and send it after the first message has been sent.
But what if a 3rd message comes in? We are only interested in sending the latest message, so we should only store the last message. A pattern like this prevents useless updates / excessive use of the Slack API. Be aware: rate limiting applies to the Slack API, so you might want to consider only updating the message once a second.
Class me!
So let's look at the implementation:
import { ChatPostMessageArguments, ChatUpdateArguments, WebClient } from "@slack/web-api"
import removeMarkDown from "remove-markdown"
import { ChatPostMessageWebAPICallResult, ChatUpdateMessageWebAPICallResult, Message } from "./types"
export class UpdatableMessage {
private message?: Message
private nextMessage?: Message
private sending?: Promise<string>
private isSending = false
constructor(
private readonly webClient: WebClient,
private channel: string,
private ts: string,
private readonly threadTs: string
) {}
async waitForAllToBenSent(): Promise<string | null> {
if (this.sending) {
await this.sending
await delay(500)
}
return this.ts
}
async send(msg: Message): Promise<string> {
// don't send empty or the same message
if (!msg || msg === this.message) {
return this.getTs()
}
// when sending, add to later
if (this.isSending) {
this.nextMessage = msg
await Promise.resolve(this.sending)
return this.getTs()
}
// save original message for comparison
this.message = msg
if (isString(msg)) {
if (msg.length >= 3000) {
msg = {
text: msg,
}
} else {
msg = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: msg,
},
},
],
text: removeMarkDown(msg),
}
}
}
// combine with fields for the message update
msg = {
...msg,
...{
ts: this.ts,
channel: this.channel,
as_user: true,
thread_ts: this.threadTs,
},
}
// clear blocks from previous message is we have to
if (!msg.blocks) {
msg.blocks = []
}
this.isSending = true
this.sending = sendMessage(this.webClient, <any>msg).then(x => {
this.ts = this.ts || x.ts
this.channel = this.channel || x.channel
this.isSending = false
const msg = this.nextMessage
this.nextMessage = null
return this.send(msg)
})
return await this.sending
}
getTs() {
return this.ts
}
getChannel() {
return this.channel
}
async delete() {
// wait for last message to complete
let ts = await this.waitForAllToBenSent()
// reset any updates
this.ts = null
// delete this message
await this.webClient.chat.delete({
ts: ts,
channel: this.channel,
})
}
}
By implementing a simple Boolean that checks if a message is being send, we solve most - if not all - of our asynchronous problems.
Types
We have a types.d
file with the following types to harmonize messages:
import {
Block,
ChatPostMessageArguments,
ChatUpdateArguments,
KnownBlock,
WebAPICallResult
} from "@slack/web-api"
type ChatPostMessageWebAPICallResult = WebAPICallResult & {
channel: string
ts: string
message: {
test: string
username: string
bot_id?: string
attachments: [
{
text: string
id: number
fallback: string
}
]
type: string
subtype: string
ts: string
}
}
type ChatUpdateMessageWebAPICallResult = WebAPICallResult & {
channel: string
ts: string
text: string
}
type BlockMessage = {
text?: string
blocks?: (KnownBlock | Block)[]
}
type Message = BlockMessage | ChatPostMessageArguments | ChatUpdateArguments | string
type ChannelWebAPICallResult = WebAPICallResult & {
channel: {
name: string
}
}
Note on Slack Block Kit
We're using the Slack Block Kit to send a text message, but turning it into a text section. The text
field value is used by Slack to display a simple notification (on your PC or phone). We used the remove-markdown NPM package to strip markdown from the text.
Bot Zero
A more advanced version of the class is used by the progress script of the bot-zero project to show an example of a progress indicator. Go check it out and let us know what you think.

Changelog
- 2023-06-06 Updated the code to clear blocks from previous updates.
- 2023-06-06 Provided types to make sending complex (block kit) messages easier.
- 2022-08-18 Changed to code from JavaScript into TypeScript. The code now uses the Slack Block Kit and the Slack Web API.