In this tutorial, you'll see how you can build an app that can publish notification messages that contain interactive elements. We'll show you how apps can respond to the use of those interactive elements and use them to trigger useful flows.
Quickly create an app with the correct configuration of scopes and features for this tutorial by clicking below.
Bolt is the swiftest way to start building on the Slack Platform. Before you proceed with this tutorial, follow our Getting Started with Bolt for JavaScript, Python, and Java guides to get setup and ready to build.
Your Slack app can publish text messages to public channels or private conversations just as users can. These messages can be used as notifications for your app.
Apps can also include special visual components in their messages, using a framework we call Block Kit. Block Kit contains a subset of interactive components that allow users to take actions within your app, directly from a conversation.
Composing a notification message involves choosing blocks, and laying them out to present important data. In doing so, you’ll create an array of Block Kit objects that will be used when publishing a message.
After you've finished with this section, you will have created an interactive message payload to publish in the next section.
All of the Slack APIs that publish messages use a common structure, called a message payload.
This payload is a JSON object that is used to define the content of the message and metadata about the message. The metadata can include required information such as the conversation the message should be published to, and optional parameters which determine the visual composition of the message.
Here's a very basic app-published message payload:
{
"channel": "CONVERSATION_ID",
"text": "Hello, world.",
}
You can go further to customize published messages by using special text formatting, or by composing a blocks
parameter to define a rich, and potentially interactive, message layout.
View our message payload reference for a list of potential payload parameters.
Text within message payloads can be formatted using a special markup syntax called mrkdwn
. Consult this reference guide to learn everything you can do with mrkdwn
.
Slack provides a range of visual components, called Block Kit, that can be used in messages. These blocks can be used to lay out complex information in an understandable way.
Each block is represented in our APIs as a JSON object. Here's an example of a section
block:
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "New Paid Time Off request from <example.com|Fred Enriquez>\n\n<https://example.com|View request>"
}
}
Every block contains a type
field — specifying which of the available blocks to use — along with other fields that describe the content of the block.
Block Kit Builder is a visual prototyping sandbox that will let you choose from, configure, and preview all the available blocks.
If you want to skip the builder, the block reference guide contains the specifications of every block, and the JSON fields required for each of them.
Individual blocks can be stacked together to create complex visual layouts.
When you've chosen each of the blocks you want in your layout, place each of them in an array, in visual order, like this:
[
{
"type": "header",
"text": {
"type": "plain_text",
"text": "New request"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Type:*\nPaid Time Off"
},
{
"type": "mrkdwn",
"text": "*Created by:*\n<example.com|Fred Enriquez>"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*When:*\nAug 10 - Aug 13"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "<https://example.com|View request>"
}
}
]
Block Kit Builder will allow you to drag, drop, and rearrange blocks to design and preview Block Kit layouts.
Alternatively you can use the block reference guide to manually generate a complete blocks
array, like the one shown above.
Blocks are added to messages by adding a blocks
array to the message payload, like this:
{
"channel": "C123ABC456",
"text": "New Paid Time Off request from Fred Enriquez",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "New request"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Type:*\nPaid Time Off"
},
{
"type": "mrkdwn",
"text": "*Created by:*\n<example.com|Fred Enriquez>"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*When:*\nAug 10 - Aug 13"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "<https://example.com|View request>"
}
}
]
}
When you're using blocks
in your message payload, the top-level text
field becomes a fallback message displayed in notifications. Blocks should define everything else about the desired visible message.
You can take Block Kit layouts further by adding interactive components like buttons, select menus, date pickers, and more.
Interactive components allow your app to retrieve, manipulate, update, and return data to external services in response to user action. All from the comfort of Slack.
Adding interactive components is no different from adding any other block. Let's add a couple of buttons to an actions block in a message payload:
{
"channel": "C123ABC456",
"text": "New Paid Time Off request from Fred Enriquez",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "New request",
"emoji": true
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Type:*\nPaid Time Off"
},
{
"type": "mrkdwn",
"text": "*Created by:*\n<example.com|Fred Enriquez>"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*When:*\nAug 10 - Aug 13"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"emoji": true,
"text": "Approve"
},
"style": "primary",
"value": "click_me_123"
},
{
"type": "button",
"text": {
"type": "plain_text",
"emoji": true,
"text": "Reject"
},
"style": "danger",
"value": "click_me_123"
}
]
}
]
}
You could replace these buttons with any of the available interactive components. You can also add a button as a section
block's accessory
element, rather than the actions
block used above.
Browse the interactive components to see a full list of what's available, or try the Block Kit Builder tool to visually prototype a layout with interactive components.
When the message payload you've constructed here is published to Slack, people will be able to click on the buttons included in the layout. When that happens, your app will be sent interactive payloads for processing and response.
We'll show you how these interactive payloads work, and how to configure your app to receive them, in a later step.
The Web API is the core of every Slack app's functionality. In the next section, we'll use it to access the API for publishing messages.
Before we do that, familiarize yourself with the process for actually using the API. We recommend using Bolt to save yourself a lot of time and effort.
After finishing this section, you should be familiar with the basics of using the Web API in an app.
All flavors of Bolt use a similar client interface, with API parameters passed as arguments.
Here's a basic Bolt example that calls chat.postMessage
:
import com.slack.api.bolt.App; import com.slack.api.bolt.AppConfig; import com.slack.api.bolt.jetty.SlackAppServer; import com.slack.api.methods.SlackApiException; import java.io.IOException; public class ChatPostMessage { public static void main(String[] args) throws Exception { var config = new AppConfig(); config.setSingleTeamBotToken(System.getenv("SLACK_BOT_TOKEN")); config.setSigningSecret(System.getenv("SLACK_SIGNING_SECRET")); var app = new App(config); // `new App()` does the same app.message("hello", (req, ctx) -> { var logger = ctx.logger; try { var event = req.getEvent(); // Call the chat.postMessage method using the built-in WebClient var result = ctx.client().chatPostMessage(r -> r // The token you used to initialize your app is stored in the `context` object .token(ctx.getBotToken()) // Payload message should be posted in the channel where original message was heard .channel(event.getChannel()) .text("world") ); logger.info("result: {}", result); } catch (IOException | SlackApiException e) { logger.error("error: {}", e.getMessage(), e); } return ctx.ack(); }); var server = new SlackAppServer(app); server.start(); } }
Code to initialize Bolt app// Require the Node Slack SDK package (github.com/slackapi/node-slack-sdk) const { WebClient, LogLevel } = require("@slack/web-api"); // WebClient instantiates a client that can call API methods // When using Bolt, you can use either `app.client` or the `client` passed to listeners. const client = new WebClient("xoxb-your-token", { // LogLevel can be imported and used to make debugging simpler logLevel: LogLevel.DEBUG });
// ID of the channel you want to send the message to const channelId = "C12345"; try { // Call the chat.postMessage method using the WebClient const result = await client.chat.postMessage({ channel: channelId, text: "Hello world" }); console.log(result); } catch (error) { console.error(error); }
Code to initialize Bolt appimport logging import os # Import WebClient from Python SDK (github.com/slackapi/python-slack-sdk) from slack_sdk import WebClient from slack_sdk.errors import SlackApiError # WebClient instantiates a client that can call API methods # When using Bolt, you can use either `app.client` or the `client` passed to listeners. client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) logger = logging.getLogger(__name__)
# ID of the channel you want to send the message to channel_id = "C12345" try: # Call the chat.postMessage method using the WebClient result = client.chat_postMessage( channel=channel_id, text="Hello world" ) logger.info(result) except SlackApiError as e: logger.error(f"Error posting message: {e}")
If you're curious, you can read more about how Bolt can be used to call the Web API—just consult the relevant Bolt for JavaScript, Bolt for Python, or Bolt for Java docs.
Not using Bolt? Read how to manually build an app that can interact with the Web API.
Putting everything you've learned so far together, you're ready to make the API calls that will publish a notification as a message.
Because you requested chat:write
and chat:write.public
permissions earlier, your app will be able to publish to any Slack conversation it is a member of or any public channel, at any time. Notification messages should typically be sent in reaction to some event on a service outside of Slack.
To publish your message, we'll use the chat.postMessage
Web API method.
After finishing this step, your app will have published an interactive notification to a Slack conversation of your choice.
When an app publishes a message, the app needs to know which Slack conversation the message should be added to.
In order to find a valid Slack conversation ID, we'll use the conversations.list
API method. This API will return a list of all public channels in the workspace your app is installed to. You'll need the channels:read
permission granted to your app.
Within that list, we'll be able to find a specific id
of the conversation that we want to access. Here's an example API call:
import com.slack.api.Slack; import com.slack.api.methods.SlackApiException; import com.slack.api.model.Conversation; import org.slf4j.LoggerFactory; import java.io.IOException; public class FindingConversation { /** * Find conversation ID using the conversations.list method */ static void findConversation(String name) { // you can get this instance via ctx.client() in a Bolt app var client = Slack.getInstance().methods(); var logger = LoggerFactory.getLogger("my-awesome-slack-app"); try { // Call the conversations.list method using the built-in WebClient var result = client.conversationsList(r -> r // The token you used to initialize your app .token(System.getenv("SLACK_BOT_TOKEN")) ); for (Conversation channel : result.getChannels()) { if (channel.getName().equals(name)) { var conversationId = channel.getId(); // Print result logger.info("Found conversation ID: {}", conversationId); // Break from for loop break; } } } catch (IOException | SlackApiException e) { logger.error("error: {}", e.getMessage(), e); } } public static void main(String[] args) throws Exception { // Find conversation with a specified channel `name` findConversation("tester-channel"); } }
Code to initialize Bolt app// Require the Node Slack SDK package (github.com/slackapi/node-slack-sdk) const { WebClient, LogLevel } = require("@slack/web-api"); // WebClient instantiates a client that can call API methods // When using Bolt, you can use either `app.client` or the `client` passed to listeners. const client = new WebClient("xoxb-your-token", { // LogLevel can be imported and used to make debugging simpler logLevel: LogLevel.DEBUG });
// Find conversation ID using the conversations.list method async function findConversation(name) { try { // Call the conversations.list method using the built-in WebClient const result = await app.client.conversations.list({ // The token you used to initialize your app token: "xoxb-your-token" }); for (const channel of result.channels) { if (channel.name === name) { conversationId = channel.id; // Print result console.log("Found conversation ID: " + conversationId); // Break from for loop break; } } } catch (error) { console.error(error); } } // Find conversation with a specified channel `name` findConversation("tester-channel");
Code to initialize Bolt appimport logging import os # Import WebClient from Python SDK (github.com/slackapi/python-slack-sdk) from slack_sdk import WebClient from slack_sdk.errors import SlackApiError # WebClient instantiates a client that can call API methods # When using Bolt, you can use either `app.client` or the `client` passed to listeners. client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) logger = logging.getLogger(__name__)
channel_name = "needle" conversation_id = None try: # Call the conversations.list method using the WebClient for result in client.conversations_list(): if conversation_id is not None: break for channel in result["channels"]: if channel["name"] == channel_name: conversation_id = channel["id"] #Print result print(f"Found conversation ID: {conversation_id}") break except SlackApiError as e: print(f"Error: {e}")
GET https://slack.com/api/conversations.list Authorization: Bearer xoxb-your-token
You'll get back a JSON object, with a channels
array containing all the public channels that your app can see. You can find your channel by looking for the name
in each object.
When you've found the matching channel, make note of the id
value, as you'll need it for certain API calls.
If your app implements shortcuts, slash commands, or uses the Events API, your app will see conversation id
s in request payloads sent by those features.
In those cases, your app can dynamically respond using the payload data to identify the relevant conversation, rather than needing to use the conversations.list
method described above.
If you're using Bolt, call chat.postMessage
as below, making sure to include your blocks
array in the message payload:
import com.slack.api.bolt.App; import com.slack.api.bolt.AppConfig; import com.slack.api.bolt.jetty.SlackAppServer; import com.slack.api.methods.SlackApiException; import java.io.IOException; public class ChatPostMessage { public static void main(String[] args) throws Exception { var config = new AppConfig(); config.setSingleTeamBotToken(System.getenv("SLACK_BOT_TOKEN")); config.setSigningSecret(System.getenv("SLACK_SIGNING_SECRET")); var app = new App(config); // `new App()` does the same app.message("hello", (req, ctx) -> { var logger = ctx.logger; try { var event = req.getEvent(); // Call the chat.postMessage method using the built-in WebClient var result = ctx.client().chatPostMessage(r -> r // The token you used to initialize your app is stored in the `context` object .token(ctx.getBotToken()) // Payload message should be posted in the channel where original message was heard .channel(event.getChannel()) .text("world") ); logger.info("result: {}", result); } catch (IOException | SlackApiException e) { logger.error("error: {}", e.getMessage(), e); } return ctx.ack(); }); var server = new SlackAppServer(app); server.start(); } }
Code to initialize Bolt app// Require the Node Slack SDK package (github.com/slackapi/node-slack-sdk) const { WebClient, LogLevel } = require("@slack/web-api"); // WebClient instantiates a client that can call API methods // When using Bolt, you can use either `app.client` or the `client` passed to listeners. const client = new WebClient("xoxb-your-token", { // LogLevel can be imported and used to make debugging simpler logLevel: LogLevel.DEBUG });
// ID of the channel you want to send the message to const channelId = "C12345"; try { // Call the chat.postMessage method using the WebClient const result = await client.chat.postMessage({ channel: channelId, text: "Hello world" }); console.log(result); } catch (error) { console.error(error); }
Code to initialize Bolt appimport logging import os # Import WebClient from Python SDK (github.com/slackapi/python-slack-sdk) from slack_sdk import WebClient from slack_sdk.errors import SlackApiError # WebClient instantiates a client that can call API methods # When using Bolt, you can use either `app.client` or the `client` passed to listeners. client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) logger = logging.getLogger(__name__)
# ID of the channel you want to send the message to channel_id = "C12345" try: # Call the chat.postMessage method using the WebClient result = client.chat_postMessage( channel=channel_id, text="Hello world" ) logger.info(result) except SlackApiError as e: logger.error(f"Error posting message: {e}")
If you're not using Bolt, you'll need to manually create an API call to chat.postMessage
.
Read our detailed guide to sending messages if you want to learn more.
Your interactive notification message should now be sitting in the chosen conversation, with some buttons awaiting interaction.
When one of those buttons are used, Slack will send an interaction payload to your app. You'll use the information in that payload to spur some relevant action in your app.
After finishing this step, your app will be able to handle interactions with published notifications.
Every interaction between a user and an app exists as part of a flow, a series of events that gets something done:
In the next few steps, we'll get your app ready to handle this flow.
Interaction payloads are sent to your app's configured Request URL. To add a Request URL to your app:
If you're using Socket Mode, you won't need to configure a Request URL. Interaction payloads will instead be sent to your WebSocket URL, and will be wrapped in some Socket Mode specific metadata.
If you're using the Bolt framework, the process of receiving and processing interaction payloads is handled for you. All you need to do is create a listener for each block_id
or action_id
created by your app:
// This listener will be called every time an interactive component with the `action_id` "approve_button" is triggered // `block_id` is disregarded in this case app.blockAction("approve_button", (req, ctx) -> { String value = req.getPayload().getActions().get(0).getValue(); // "button's value" // Do something in response return ctx.ack(); });
// This listener will be called every time an interactive component with the `action_id` "approve_button" is triggered // `block_id` is disregarded in this case app.action('approve_button', async ({ ack, say }) => { await ack(); // Do something in response }); // This listener will only be called when the `action_id` matches 'select_user' AND the `block_id` matches 'assign_ticket' app.action({ action_id: 'select_user', block_id: 'assign_ticket' }, async ({ body, action, ack, client }) => { await ack(); // Do something in response });
# This listener will be called every time an interactive component with the `action_id` "approve_button" is triggered # `block_id` is disregarded in this case @app.action("approve_button") def some_action_response(ack): ack() # Do something in response # This listener will only be called when the `action_id` matches 'select_user' AND the `block_id` matches 'assign_ticket' @app.action({ "block_id": "assign_ticket", "action_id": "select_user" }) def some_other_action_response(ack, body, client): ack() # Do something in response
Your app has access to all the contextual info about the interaction that's included in the payload. Your app can use as much or as little of the info as needed to generate a response.
You can view a full list of the fields included in our block_actions
payload reference.
All apps must acknowledge the receipt of an interaction payload within 3 seconds. To enable this acknowledgment response, Bolt action listeners are passed a callable ack()
function (in Bolt for Java this function is ctx.ack()
).
This ack()
function requires no arguments. We recommend calling ack()
immediately, before any other processing, since you only have 3 seconds to respond.
Not using Bolt? Read how to manually build an app that can receive and process interaction payloads.
If you've reached this step, your app should be able to successfully handle user interactions. Now you need to decide what your app will do in response. Send a message to the user, pop a modal, update data on an external service, or lots of other possibilities.
Read our guide to responding to user interactions to help you decide what to do.