Developing apps with AI features
Slack offers a unique messaging experience for apps using AI features, including a side-by-side window within the Slack client, an app launch point accessible from the top bar in Slack, special loading states, suggested prompts, and app threads. Slack does not provide an LLM; rather, it gives you the tools and interface to best integrate an LLM of your choice for use in Slack.
Don't have a paid plan? Join the Developer Program and provision a fully-featured sandbox for free.
Implementing AI features
Simply by toggling on the Agents & AI Apps feature in the app settings, your app will live in the top bar app entry point and be available for interaction in the split pane. The rest of the AI-related app features need to be implemented. Use this guide to do so both with and without the use of the Bolt frameworks.
Loading states
Use the assistant.threads.setStatus API method to set the loading state status of the app. We recommend using the Bolt framework to handle the details for you.
- API (no SDK)
- Bolt Python
- Bolt JS
Here is a sample request for the API without using the Bolt framework. Refer to the method documentation or app flow section below for more implementation details.
{
"status": "is working on your request...",
"channel_id": "D324567865",
"thread_ts": "1724264405.531769"
}
Use the Bolt for Python setStatus utility to cycle through strings passed into a loading_messages array. Refer to the Bolt for Python documentation for more details.
# This listener is invoked when the human user sends a reply in the assistant thread
@assistant.user_message
def respond_in_assistant_thread(
client: WebClient,
context: BoltContext,
get_thread_context: GetThreadContext,
logger: logging.Logger,
payload: dict,
say: Say,
set_status: SetStatus,
):
try:
channel_id = payload["channel"]
team_id = payload["team"]
thread_ts = payload["thread_ts"]
user_id = payload["user"]
user_message = payload["text"]
# Set your desired statuses here
set_status(
status="thinking...",
loading_messages=[
"Untangling the internet cables…",
"Consulting the office goldfish…",
"Convincing the AI to stop overthinking…",
],
)
...
Use the Bolt for JavaScript setStatus utility to cycle through strings passed into a loading_messages array. Refer to the Bolt for JavaScript documentation for more details.
...
const assistant = new Assistant({
...
userMessage: async ({ client, context, logger, message, getThreadContext, say, setTitle, setStatus }) => {
if (!('text' in message) || !('thread_ts' in message) || !message.text || !message.thread_ts) {
return;
}
const { channel, thread_ts } = message;
const { userId, teamId } = context;
try {
await setTitle(message.text);
/**
* Set the status of the Assistant to give the appearance of active processing.
*/
await setStatus({
status: 'thinking...',
loading_messages: [
'Teaching the hamsters to type faster…',
'Untangling the internet cables…',
'Consulting the office goldfish…',
'Polishing up the response just for you…',
'Convincing the AI to stop overthinking…',
],
});
...
Set suggested prompts
Use the assistant.threads.setSuggestedPrompts API method to set suggested prompts for your user to choose from. We recommend using the Bolt framework to handle the details for you.
- API (no SDK)
- Bolt Python
- Bolt JS
Here is a sample request for the API without using the Bolt framework. Refer to the method documentation or app flow section below for more implementation details.
{
"channel_id": "D2345SFDG",
"thread_ts": "1724264405.531769",
"title": "Welcome. What can I do for you?",
"prompts": [
{
"title": "Generate ideas",
"message": "Pretend you are a marketing associate and you need new ideas for an enterprise productivity feature. Generate 10 ideas for a new feature launch.",
},
{
"title": "Explain what SLACK stands for",
"message": "What does SLACK stand for?",
},
{
"title": "Describe how AI works",
"message": "How does artificial intelligence work?",
},
]
}
Use the Bolt for Python utility to set predetermined suggested prompts for the user to choose from. Refer to the Bolt for Python documentation for more details.
assistant = Assistant()
@assistant.thread_started
def start_assistant_thread(
say: Say,
get_thread_context: GetThreadContext,
set_suggested_prompts: SetSuggestedPrompts,
logger: logging.Logger,
):
try:
say("How can I help you?")
prompts: List[Dict[str, str]] = [
{
"title": "Suggest names for my Slack app",
"message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.",
},
]
thread_context = get_thread_context()
if thread_context is not None and thread_context.channel_id is not None:
summarize_channel = {
"title": "Summarize the referred channel",
"message": "Can you generate a brief summary of the referred channel?",
}
prompts.append(summarize_channel)
set_suggested_prompts(prompts=prompts)
except Exception as e:
logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e)
say(f":warning: Something went wrong! ({e})")
Use the Bolt for JavaScript utility to set predetermined suggested prompts for the user to choose from. Refer to the Bolt for JavaScript documentation for more details.
...
threadStarted: async ({ event, logger, say, setSuggestedPrompts, saveThreadContext }) => {
const { context } = event.assistant_thread;
try {
await say('Hi, how can I help?');
await saveThreadContext();
/**
* Provide the user up to 4 optional, preset prompts to choose from.
*
* The first `title` prop is an optional label above the prompts that
* defaults to 'Try these prompts:' if not provided.
*/
if (!context.channel_id) {
await setSuggestedPrompts({
title: 'Start with this suggested prompt:',
prompts: [
{
title: 'This is a suggested prompt',
message:
'When a user clicks a prompt, the resulting prompt message text ' +
'can be passed directly to your LLM for processing.\n\n' +
'Assistant, please create some helpful prompts I can provide to ' +
'my users.',
},
],
});
}
if (context.channel_id) {
await setSuggestedPrompts({
title: 'Perform an action based on the channel',
prompts: [
{
title: 'Summarize channel',
message: 'Assistant, please summarize the activity in this channel!',
},
],
});
}
} catch (e) {
logger.error(e);
}
},
...
App threads
By enabling the Agents & AI Apps feature in the app settings, Slack will automatically group your app conversations into threads. You can set the title of these threads using the assistant.threads.setTitle API method, or use the Bolt framework utility to handle the details.
- API (no SDK)
- Bolt Python
- Bolt JS
Here is a sample request for the API without using the Bolt framework. Refer to the method documentation for more implementation details.
{
"title": "Holidays this year",
"channel_id": "D324567865",
"thread_ts": "1786543.345678"
}
Use the Bolt for Python setTitle utility to set the title of the app thread. Refer to the Bolt for Python documentation for more details.
assistant = Assistant()
@assistant.thread_started
def start_assistant_thread(
say: Say,
get_thread_context: GetThreadContext,
set_suggested_prompts: SetSuggestedPrompts,
logger: logging.Logger,
):
try:
say("How can I help you?")
prompts: List[Dict[str, str]] = [
{
# Set the thread title here
"title": "Suggest names for my Slack app",
"message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.",
},
]
thread_context = get_thread_context()
if thread_context is not None and thread_context.channel_id is not None:
summarize_channel = {
# Set the thread title here
"title": "Summarize the referred channel",
"message": "Can you generate a brief summary of the referred channel?",
}
prompts.append(summarize_channel)
set_suggested_prompts(prompts=prompts)
except Exception as e:
logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e)
say(f":warning: Something went wrong! ({e})")
Use the Bolt for JavaScript setTitle utility to set the title of the app thread. Refer to the Bolt for JavaScript documentation for more details.
...
threadStarted: async ({ event, logger, say, setSuggestedPrompts, saveThreadContext }) => {
const { context } = event.assistant_thread;
try {
await say('Hi, how can I help?');
await saveThreadContext();
if (!context.channel_id) {
await setSuggestedPrompts({
// Set the thread title here
title: 'Start with this suggested prompt:',
prompts: [
{
title: 'This is a suggested prompt',
message:
'When a user clicks a prompt, the resulting prompt message text ' +
'can be passed directly to your LLM for processing.\n\n' +
'Assistant, please create some helpful prompts I can provide to ' +
'my users.',
},
],
});
}
if (context.channel_id) {
await setSuggestedPrompts({
// Set the thread title here
title: 'Perform an action based on the channel',
prompts: [
{
title: 'Summarize channel',
message: 'Assistant, please summarize the activity in this channel!',
},
],
});
}
} catch (e) {
logger.error(e);
}
},
...
Text streaming
Text streaming is handled by three different API methods: chat.startStream, chat.appendStream, and chat.stopStream. We recommend using the Bolt framework to handle the details of implementing these three methods.
- API (no SDK)
- Bolt Python
- Bolt JS
Here is a sample of these method requests for the API methods without using the Bolt framework. Refer to the method documentation linked above for more implementation details.
Initiate a new streaming method with the chat.startStream API method:
{
"channel": "D12345678",
"markdown_text": "Hello there",
"markdown": true,
"thread_ts": "1503435956.000248"
}
Append chunks progressively to an existing streaming message with the chat.appendStream API method:
{
"channel": "D12345678",
"message_ts": "1503435956.000247",
"thread_ts": "1503435956.000248",
"markdown_text": "Here's the next part of the response..."
}
Close the stream (optionally with final text) with the chat.stopStream API method:
{
"channel": "D12345678",
"message_ts": "1503435956.000247",
"thread_ts": "1503435956.000248",
"markdown_text": ""
}
Use the Bolt for Python chat_stream() utility to streamline (pun intended) all three API methods for streaming your app's messages. Refer to the Bolt for Python documentation for more details.
...
@assistant.user_message
def respond_in_assistant_thread(
...
):
try:
...
replies = client.conversations_replies(
channel=context.channel_id,
ts=context.thread_ts,
oldest=context.thread_ts,
limit=10,
)
messages_in_thread: List[Dict[str, str]] = []
for message in replies["messages"]:
role = "user" if message.get("bot_id") is None else "assistant"
messages_in_thread.append({"role": role, "content": message["text"]})
returned_message = call_llm(messages_in_thread)
# Define the text streamer
streamer = client.chat_stream(
channel=channel_id,
recipient_team_id=team_id,
recipient_user_id=user_id,
thread_ts=thread_ts,
)
for event in returned_message:
if event.type == "response.output_text.delta":
# Append the text stream with new text
streamer.append(markdown_text=f"{event.delta}")
else:
continue
# Stop the text stream
streamer.stop()
...
Use the Bolt for JavaScript chat_stream() utility to streamline all three API methods for streaming your app's messages. Refer to the Bolt for JavaScript documentation for more details.
...
// Define the streamer
const streamer = client.chatStream({
channel: channel,
recipient_team_id: teamId,
recipient_user_id: userId,
thread_ts: thread_ts,
});
for await (const chunk of llmResponse) {
if (chunk.type === 'response.output_text.delta') {
// Append the stream with new text
await streamer.append({
markdown_text: chunk.delta,
});
}
}
// Stop the text stream
await streamer.stop();
} catch (e) {
logger.error(e);
// Send message to advise user and clear processing status if a failure occurs
await say({ text: `Sorry, something went wrong! ${e}` });
}
},
});
...
Feedback block
Block Kit offers three blocks with which you can build feedback blocks to include in your app's responses. Use the following blocks to create this: context actions block, icon button block, and feedback button block.
- API (no SDK)
- Bolt Python
- Bolt JS
Here is an example of using the context_actions and feedback_buttons blocks to create a thumbs up/thumbs down section you can include in your app messages. To take action on the feedback, you will have to define what you'd like to happen when the button is clicked using a block_actions payload.
{
"blocks": [
{
"type": "context_actions",
"elements": [
{
"type": "feedback_buttons",
"action_id": "feedback_buttons_1",
"positive_button": {
"text": {
"type": "plain_text",
"text": "👍"
},
"value": "positive_feedback"
},
"negative_button": {
"text": {
"type": "plain_text",
"text": "👎"
},
"value": "negative_feedback"
}
},
]
}
]
}
View this in Block Kit Builder here.
Additionally, you could include the icon button in messages to allow for deleting them. That block looks like this:
{
"blocks": [
{
"type": "context_actions",
"elements": [
{
"type": "icon_button",
"icon": "trash",
"text": {
"type": "plain_text",
"text": "Delete"
},
"action_id": "delete_button",
"value": "delete_item"
}
]
}
]
}
View this in Block Kit Builder here.
Use the Bolt for Python blocks utility to handle feedback interactions. Refer to the Bolt for Python documentation for more details.
from typing import List
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject
def create_feedback_block() -> List[Block]:
"""
Create feedback block with thumbs up/down buttons
Returns:
Block Kit context_actions block
"""
blocks: List[Block] = [
ContextActionsBlock(
elements=[
FeedbackButtonsElement(
action_id="feedback",
positive_button=FeedbackButtonObject(
text="Good Response",
accessibility_label="Submit positive feedback on this response",
value="good-feedback",
),
negative_button=FeedbackButtonObject(
text="Bad Response",
accessibility_label="Submit negative feedback on this response",
value="bad-feedback",
),
)
]
)
]
return blocks
Refer to the full documentation to see the chat_stream and handle_feedback utilities carry out the full feedback flow.
Use the Bolt for JavaScript blocks utility to handle feedback interactions. Refer to the Bolt for JavaScript documentation for more details.
const feedbackBlock = {
type: 'context_actions',
elements: [
{
type: 'feedback_buttons',
action_id: 'feedback',
positive_button: {
text: { type: 'plain_text', text: 'Good Response' },
accessibility_label: 'Submit positive feedback on this response',
value: 'good-feedback',
},
negative_button: {
text: { type: 'plain_text', text: 'Bad Response' },
accessibility_label: 'Submit negative feedback on this response',
value: 'bad-feedback',
},
},
],
};
Refer to the full documentation to see the chatStream and feedback utilities carry out the full feedback flow.
App flow
To set up your app for the AI capabilities discussed above, first create an app, then find the Agents & AI Apps feature in the sidebar, and enable it. You will also want to subscribe to the assistant_thread_started, assistant_thread_context_changed, and message.im events.
The following is a sample flow for an app with AI features, including the methods to use to create the AI app experience your users expect.
The instructions here show the app flow without the use of the Bolt framework. However, using Bolt for Python or Bolt for JavaScript simplifies listening for and responding to these events.
1. Listen for the assistant_thread_started event
When the user first opens the container, Slack sends the assistant_thread_started event along with the context object. The context object is shown below:
"context": {
"channel_id": "C123ABC456",
"team_id": "T123ABC456",
"enterprise_id": "E123ABC456"
}
Some applications have no use for the context object. Perhaps your app does, though! If so, first call the conversations.info method to see if your app has access to the channel, then proceed from there.
The assistant_thread_context_changed event is sent when a user opens a new channel while the container is open. This can be used to track the active context of a user in Slack.
2. Respond to the assistant_thread_started event
A user's first impression is a crucial moment. Capitalizing on this first interaction is an opportunity to create a delightful experience.
Your app can call the assistant.threads.setStatus method if it needs time to generate prompts.
If your app is sending hardcoded prompts (this can be configured in the app settings), it would skip this step and instead call the assistant.threads.setSuggestedPrompts method to send one or more suggested prompts.
3. Listen for the message.im event
The user then will type a message or click on a prompt which triggers a message.im event. The event is the same whether the user clicked the suggested prompt or typed it manually. Users can message your app via the container or through your app's Chat tab.
Your app can respond to the user directly or it can pass back the thread_ts parameter to continue in the same thread. In most situations, you will want to call the chat.postMessage method with the thread_ts parameter.
When your app receives the thread_ts parameter, you can retrieve the conversation by using thread_ts as the unique identifier. This is useful if your app stores the long-lived context or the state of a thread.
You can also fetch previous thread messages using the conversations.replies method and choose which other messages from the conversation to include in the LLM prompt or your app logic.
Once you receive the response from the LLM, use the chat.startStream method to start a text stream, the chat.appendStream method to append it, and the chat.stopStream method to stop it. These allow the user to see the response from the LLM as a text stream, rather than a single block of text sent all at once. This provides closer alignment with expected behavior from other major LLM tools. The Bolt framework, available for Python and JavaScript, makes this process simpler.
Note: @-mentions in channels can happen like they do today — whether you support this or not is up to you. You can engage with the user or ask them to use the container to converse with your app.
4. Respond to the message.im event
Your app should then call the assistant.threads.setStatus method to display the status indicator in the container. We recommend doing so immediately for the user's benefit.
Your app can send a message back to the user via the chat.postMessage method, automatically clearing the status indicator in the Slack client. The status can also be cleared by again calling the assistant.threads.setStatus method with an empty string in the status field.
Additional ways to add AI to your app
AI can be infused in your Slack app with any feature—the world is your oyster. Beyond the side-by-side chat experience, any user input can be used as input for an LLM query, and the answer can be posted back in Slack. Take these three examples:
- Ask an LLM a question that was reacted to with a reacji. Learn how to do this in the Events API documentation
- Initiate asking an LLM a question posed in a message from a message shortcut
- Start off an LLM query with a slash command
- Collect structured data in a modal, then use it in a query to an LLM
Remember to enable the Agents & AI Apps feature toggle to gain access to the assistant:write scope, which is required for calling assistant-related methods like assistant.threads.setStatus.
Integrating with AI carries an inherent risk of prompt injection. Read more about the risk of data exfiltration and how to prevent it in the security documentation.
Tips and tricks
Block Kit and interactivity
Provide interactive Block Kit elements, such as drop-down menus and buttons, to allow your app to interact with the user. Block Kit is not required, however; you can forgo interactivity and message the user via plain text and Slack markdown.
When updating longer messages sent to a user, only call the chat.update method once every 3 seconds with new content, otherwise your calls may hit the rate limit.
You can also set a section block element's expand property to true to allow your app to post long messages without the user needing to click 'see more' to see the full text of the message.
Slash commands
Slash commands are not supported in the split view container because all messages in the container or in conversation with the app take place in message threads. Slash commands do not work in threads in general. They do work in messages.
Sending notifications
When your app has the Agents & AI Apps feature toggled on, every DM with the user is a thread.
When sending a notification to a user outside of an existing thread:
- Use the
chat.postMessagemethod as normal, but look for thetsparameter in the response. - Call the
assistant.threads.setTitlemethod, sending the newtsparameter as thethread_tsparameter to set the title of the thread. This allows a user to see the new notification with a titled thread when they view the app's DM.
When a user navigates to your app, two tabs are available. The History tab is all of the past threads (in which the user has sent a message) — this is where they will see new notifications. The Chat tab is the last active thread between the user and your app.
Slack has the Activity side rail tab to show new activity in a workspace. This area is optimized for users to quickly see and respond to notifications from your app.
Data retention
Do not store any Slack data you obtain. Instead, store metadata and pull in data in real time if needed.
Members only
Workspace guests are not permitted to access apps with the Agents & AI Apps feature enabled.
Next steps
✨ Get started with an agent template in Bolt for Python or Bolt for JavaScript.
✨ Build an AI-fueled Code Assistant app using Bolt for JavaScript with this tutorial.
✨ Make your app the best it can be with our Best practices for AI-enabled apps.
✨ For details on the end user experience of apps using platform AI features, check out these Help Center articles: Understand AI apps in Slack and Manage app agents and assistants.