Showing a shimmering status next to your bot's name in a Slack agent
When you use Claude's Slack integration, or run Nous Research's Hermes Agent in Slack, the area next to the bot name shimmers while it is preparing a reply, and text like "is thinking..." is shown. This post explains how to show the same thing in your own agent bot.
It covers everything from configuring the Slack app as an "Agent" instead of an "App" to implementing the status display. The code assumes Bolt for JavaScript.
This is how it looks when it runs. The status line next to the bot name changes from "Thinking…" to "Looking things up…", and disappears when the reply comes back.
TL;DR
- That display is the status line produced by
assistant.threads.setStatus, drawn with an animation by the Slack client. What you specify through the API is the text to show. - To show it, configure the Slack app as Agents & AI Apps (agent mode) and add the
assistant:writeorchat:writescope. - In the new UI (
agent_view), DMs arrive asmessage.imevents, so callclient.assistant.threads.setStatus()directly. - The status disappears automatically about 2 minutes after the last update, so re-send it periodically during a run to keep it.
- Serialize the API calls so that the clear is always the last write.
- If you swap the text according to the name of the tool being run, it becomes a live report such as "Running a command…".
1. How the status line works
When you set a status string with assistant.threads.setStatus, Slack draws a line of the form <app name> <status text> in the conversation. This line is animated, so the app name part looks like it is shimmering. The API controls the text, and the Slack client handles the visual effect.
The Slack adapter of the open-source Hermes Agent (plugins/platforms/slack/adapter.py) also states that it uses the same API.
async def send_typing(self, chat_id: str, metadata=None) -> None:
"""Show a typing/status indicator using assistant.threads.setStatus.
Displays "is thinking..." next to the bot name in a thread, ...
Note that the message streaming API added in October 2025 (chat.startStream / chat.appendStream / chat.stopStream) is a separate feature for delivering the message body little by little, and is independent of the status line.
2. Configuring the app as an "Agent" instead of an "App"
To show the status line, the app has to be configured as an agent. Open the app at https://api.slack.com/apps and set the following.
2-1. Enable Socket Mode (optional)
Socket Mode communicates over an outbound WebSocket to wss://wss-primary.slack.com. You do not have to open an inbound port, so it works as is inside a home LAN or on a local machine.
- Settings → Socket Mode → Enable Socket Mode
- Create an App-Level Token (scope:
connections:write) → note thexapp-...value
When it is enabled, events are delivered to the WebSocket instead of the Request URL (HTTP). You do not need it if your environment can expose an HTTP endpoint.
2-2. Enable agent mode
The setting that makes it "an Agent instead of an App" is done here.
- Features → Agents & AI Apps (previously named Assistant) → Enable
- App Home → enable the Messages Tab (Chat Tab)
- App Home → turn ON "Allow users to send Slash commands and messages from the messages tab"
Turning the third one ON lets users send DMs from the Messages tab.
2-3. Bot Token Scopes
Grant these in OAuth & Permissions.
| Scope | Purpose |
|---|---|
assistant:write |
Agent mode (showing the status line) |
chat:write |
Posting messages. Can also be used for the status line |
app_mentions:read |
Receiving mentions in channels |
im:history |
Receiving DMs |
channels:history / groups:history |
Continuing to respond to thread replies |
reactions:write |
Showing a reaction while processing (optional) |
With the change of March 5, 2026, the scope required by assistant.threads.setStatus became either assistant:write or chat:write. Slack has announced that it plans to move toward chat:write from now on.
After adding scopes, reinstall the app to the workspace.
2-4. Event Subscriptions
Even when you use Socket Mode, you still need to set the events to subscribe to.
message.im: DMsapp_mention: mentions in channelsmessage.channels/message.groups: continuing thread repliesassistant_thread_started/assistant_thread_context_changed: forassistant_view
2-5. assistant_view and agent_view
There are two kinds of agent UI, and DMs arrive differently in each.
assistant_view |
agent_view |
|
|---|---|---|
| UI | A dedicated UI with separate Chat / History tabs | Everything happens in the Messages tab, the same as a normal DM |
| Position | The one that has existed until now | The default for new apps from June 30, 2026 |
| How DMs arrive | Dedicated events such as assistant_thread_started |
Normal message.im events |
You can check which one your app is with features.assistant_view / features.agent_view in the App Manifest. A newly created app will be agent_view.
The Assistant class of Bolt for JS is designed around assistant_view-only events such as assistant_thread_started, and agent_view DMs arrive at app.message() as message.im. If you want to show the status in both UIs, call the API directly from client as in the next chapter.
3. Implementation — call setStatus directly on client
If you call it directly from client, it is shown both in agent_view DMs and in channel threads. Hermes Agent takes the same approach.
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: "Thinking…",
});
To clear it, send an empty string.
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: "",
});
3-1. Specifying thread_ts
thread_ts is a required parameter. In agent_view, the user's message arrives as a top-level message, so pass the ts of that message itself. Slack opens a thread starting from that message. Hermes Agent handles it the same way (thread_ts = event.thread_ts or ts).
const threadTs = message.thread_ts ?? message.ts;
3-2. Re-send periodically to keep it visible
The status is cleared automatically about 2 minutes after the last update. An agent's work can take longer than that, so re-send the same text at a fixed interval during a run. At an 8-second interval that is 7.5 requests per minute per thread, which leaves plenty of room under the default rate limit (600 req/min/app/team).
3-3. Serialize the API calls
If you send setStatus calls in parallel, the order of the network responses can cause old text to arrive after the clear. Serialize them with a Promise chain so that the clear is always the last write.
let chain: Promise<void> = Promise.resolve();
const enqueuePost = (text: string): void => {
chain = chain.then(async () => {
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: text,
});
});
};
3-4. Always send the clear
If setStatus fails because of a missing scope or a network error, you can stop the periodic re-send, but make sure you send the clear once. Then the status line is reliably removed when the work ends.
3-5. Rotate text with loading_messages
assistant.threads.setStatus has an optional parameter called loading_messages. If you pass up to 10 strings, they are shown in rotation.
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: "Thinking…",
loading_messages: ["Thinking…", "Looking things up…", "Organizing…", "Almost there…"],
});
It is a good fit for the display until the agent starts using tools.
4. Change the text based on the running tool
Showing what the agent is doing right now changes how the wait feels, compared with showing "Thinking…" the whole time. Hermes Agent also shows a live report such as "is running pytest…".
Pick up the start of a tool run on the agent SDK side and reflect it in the status text. With the Claude Agent SDK, pick up the tool_use blocks from the content of the assistant messages that come through the stream.
for await (const message of query(options)) {
// Don't surface tool calls made inside a sub-agent
if (message.type === "assistant" && !message.parent_tool_use_id) {
for (const block of message.message.content) {
if (block.type === "tool_use") {
onToolUse(block.name);
}
}
}
}
Messages that have parent_tool_use_id set are runs inside a sub-agent, so excluding them keeps the display calm.
After that, map tool names to text.
function statusTextForTool(toolName: string): string {
if (toolName === "Bash") return "Running a command…";
if (toolName === "Task") return "Asking a sub-agent to investigate…";
if (["Read", "Glob", "Grep"].includes(toolName)) return "Looking through files…";
if (["Write", "Edit"].includes(toolName)) return "Editing files…";
if (["WebSearch", "WebFetch"].includes(toolName)) return "Searching the web…";
if (toolName.startsWith("mcp__")) {
const server = toolName.split("__")[1];
return server ? `Using ${server}…` : "Using an external tool…";
}
return "Working…";
}
MCP tools are named mcp__<server name>__<tool name>, so extracting the server name and showing it makes the text easy to understand.
5. Threads and sessions
When you call setStatus in agent_view, a thread opens starting from that message. If you also post the reply to the same thread, the status line and the answer appear in the same place.
await client.chat.postMessage({
channel: message.channel,
thread_ts: threadTs,
text: reply,
});
Also, Bolt's Assistant middleware receives messages with channel_type=im and a thread_ts before the message.im handler does. Once the DM is threaded, the follow-up messages go to the Assistant handler.
If you want to treat a DM as "one continuation for the whole conversation", do not split the session key by thread. On the other hand, a thread that the user started themselves in assistant_view is a separate topic, so you want to split those per thread. If you decide by whether a session already exists, it works as intended in both UIs.
const threadKey = sessionKeyOf(teamId, channel, threadTs);
const dmKey = dmSessionKeyOf(teamId, channel);
const sessionKey = hasSession(threadKey) || !hasSession(dmKey) ? threadKey : dmKey;
This hasSession() looks at running sessions in addition to saved session IDs. Session IDs are usually persisted after a run completes, so if you also treat what is in the run queue as "has a session", the conversation continues even when another message comes to the same thread during the first run.
References
- assistant.threads.setStatus | Slack Developer Docs
- Developing an agent | Slack Developer Docs
- Introducing the Agent messaging experience (changelog)
- Scope update for assistant.threads.setStatus (changelog)
send_typing()inplugins/platforms/slack/adapter.pyof NousResearch/hermes-agent
Related posts
We look forward to discussing your development needs.