Set up a Slack bot from scratch
In this article I will show you how I have set up a Slack bot that can handle a command, send a personalized message, open a modal, and get events in the Slack bot channel. I also shared through the article, some useful tools for developing a Slack application/bot.
Then, this article is based on the actual Skillz bot developed for the Zenika’s collaborators. It was a discovery for me to develop this bot which has some different functionalities. So I found it interesting to share the main methods that are offered by the Slack API and the Bolt JS library, and then how to use the new App Manifest method of Slack API.
SkillZ is an application for managing the skills and competences of Zenika’s collaborators. The aim is to gather the feedback in order to match the different profiles with missions or training that correspond the best. The bot allows the user to keep updated on their Skillz profile.
I had hesitated a lot between several languages. But my discovery of the Bolt library made me decide, and I was more interested in developing in Javascript. The Bolt library and the Slack API are very complete, and easy to use for the development of a Slack bot. It exists for JS, Python and Java languages. I have more skills in Javascript than with the others, so I will show you how to create a Slack bot with Bolt JS.
Setup
Prerequies
In order to successfully create a Slack bot, you will need:
- Basic knowledge of JavaScript and Node.js
- Node.js v12 or higher (https://nodejs.org/en/download/)
- npm (https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
Set up the repository
First I have to initialize my repository and add an “app.js” file.
touch app.js |
npm init |
Dependencies
Dotenv
Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env.
npm install dotenv |
Ngrok
Ngrok is a tool that allowed me to make a local web development workstation accessible from the outside, via a secure tunnel, from a url such as https://azerty.ngrok.io. … In fact, Ngrok provides you with an externally accessible url. Is it available thanks to this link : https://ngrok.com/.
💡 For a “simple” bot, we can use Ngrok for free. It’s efficient.
Bolt
Bolt is a library that is easy to use for sending messages, listening to events from the users, getting information about the user etc. Get more information here: https://slack.dev/bolt-js/tutorial/getting-started. Then, to install bolt in my project, I can simply use:
npm install @slack/bolt |
Create the application
Create the workspace
To get started, I decided to install my bot in an isolated Slack workspace. It simplified and reassured me on the development of the bot, because I could test a lot of things on a private server, and invite the testers of my choice to test my bot. Everyone in an organization can belong to one workspace, which is then subdivided into multiple channels.
Next, Slack will prompt me to enter the name of the project I’m working on.
Create an application from app manifest
Slack apps are small applications that provide specific functionalities within a workspace.
For this demonstration, I’ve to create it from an app manifest. It is the fastest way to create a complete bot.

💡I decided to write my article around Slack applications developed with a Manifest because for a bot that has several environments, we have to create several bots for each environment (testing/production), otherwise it creates conflict between the commands. So with a Manifest file, we can reuse the configuration of an existing bot with a few clicks. Understanding then how they work seemed interesting to me.
Then install the bot on the workspace that you would like to.
Listen events of the aplication
Launch Ngrok
./ngrok http 3000 |
💡 To listen to the events, I have to run it until the end of all my development. Because it generates a random https:// link at each launching.
Then the terminal should looks like this :
Now come back to the Slack API, in the menu of the application, and there is a sidebar with a lot of functionalities. Click on “App Manifest” like below :
There is a manifest that looks like this :
💡Note that all the configurations of the bot are considered by the App Manifest. So on the screenshot, this is the basic configuration.
Set the app manifest
The Manifest app allows to set and create bot features. What is possible with the manifest app can be done graphically in Slack API. But Slack API tends to change a lot graphically, and to set up the features, you have to set up in several different places each time. This means a lot of clicks, whereas in a few lines, in the same place, you can set everything with the Manifest app. Moreover, when there are errors (in syntax or in connections to the Ngrok listening tunnels), the App Manifest feature warns us. Get more information about the manifests here : https://api.slack.com/reference/manifests.
So we can have this kind of manifest :
display_information: name: Bot tutorial description: Keep updated about Skillz app background_color: “#0000AA” features: app_home: home_tab_enabled: true messages_tab_enabled: true messages_tab_read_only_enabled: false bot_user: display_name: Name viewed by users always_online: true oauth_config: scopes: bot: – commands – chat:write – chat:write.customize settings: event_subscriptions: request_url: https://<IP address generated by ngrok>/slack/events bot_events: – app_home_opened interactivity: is_enabled: true request_url: https://<IP address generated by ngrok>/slack/events org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false |
💡 This manifest allows to have: an app home page, listen command, write in chat, and get user’s information. Furthermore, the first lines can set a description, color to our bot interface.
Don’t forget to copy and paste the “https://[…]” given by Ngrok at each “request_url” and “url”. And then, click on “Save changes”.
App manifest schema
To understand the fields of the App Manifest that I’ve mentioned in this article, I can take a look at the differents used fields:
display_information: A group of settings that describe parts of an app’s appearance within Slack. If you’re distributing the app via the App Directory, read the listing guidelines to pick the best values for these settings. Also, display_information has different children that it can handle, like the name configuration, the color of the background etc.
features: A group of settings corresponding to the Features section of the app config pages.
So this field allows us to get or not these different tabs and also to configure these. As example, features.app_home.messages_tab_read_only_enabled is a boolean that specifies whether or not the users can send messages to your app in the Messages tab of your App Home.
oauth_config: A group of settings describing OAuth configuration for the app. It allows setting the permissions scopes of the bot and the user.
settings: Globally, it allows settings that describe interactivity and describe Events API configuration of the app.
Set the permissions
Create and get .env variables
At the root of the repository, I have to create a .env file to set the credentials :
SLACK_BOT_TOKEN= SLACK_SIGNING_SECRET= PORT=3000 |
Firstly, we will get the SLACK_BOT_TOKEN variable. Go to the Slack API dashboard, and click on “Oauth and Permissions” on the side bar. You can find the “SLACK_BOT_TOKEN” value starting with “xoxb-[…]” copy and paste this value in the .env file.
Secondly, in the same dashboard and side bar we can find the tab “Basic Information”, we can find the “SIGNING_SECRET” value in the “SLACK_SIGNING_SECRET” variable.
Set up app.js
Now, I can fill the App credentials to use all the functions that App object offers me. Then I can start it in an async function.
const { App } = require(“@slack/bolt”); const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET }); (async () => { await app.start({ port: process.env.PORT }); console.log(“⚡️ Bot-tutorial started”); })(); |
Set up the built script
“scripts”: { “dev”: “node -r dotenv/config app.js”, “test”: “echo \”Error: no test specified\” && exit 1″ }, |
💡 The “dev” line allows us to use dotenv, then our environment variables.
So I can test my code thanks to this command :
npm run dev |
My bot is running now 🚀
Post a message
Firstly I’ve to create a “postMessages.js” file that use postMessage method :
async function postSingleLineMessage( channelID, message, app, notificationMessage ) { try { await app.client.chat.postMessage({ token: app.token, channel: channelID, blocks: [ { type: “section”, text: { type: “mrkdwn”, text: message, }, }, ], text: `${notificationMessage}`, }); } catch (e) { console.error(“error”, e); } }; module.exports.postSingleLineMessage = postSingleLineMessage; |
This function will post a message in a channelID specified, with a message specified. I also have to set a notification message.
To test this function, I can go through the “Messages” tab of the bot.
Then, on the URL I can see my user ID (the pink square) and the channel ID of my bot (the green square) :
I have to copy the channel ID (the green square, we can also call it “the last part of the URL).
So I can call my function on my App.js.
const { App } = require(“@slack/bolt”); const { postSingleLineMessage } = require(“./src/postMesages”); const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET }); (async () => { await app.start({ port: process.env.PORT }); postSingleLineMessage(“D031QRK3X1Q”, “Hello !”, app, “First message”); console.log(“⚡️ Bot-tutorial started”); })(); |
Then I have to add my channel ID (found in the green square above) as the first parameter.
⚡️ I’ve received a notification!
Create a command
Now I’ll create a “Hello World” command that will answer me thanks to the function “postSingleLineMessage” that I’ve created previously. It will be available by writing /helloWorld in the chat
Set command informations
Then, I will add in the “App Manifest” yaml this part of code :
slash_commands: – command: /helloWorld url: https://<IP address generated by ngrok>/slack/events description: I am polite 🙂 should_escape: false |
So the yaml should looks like this :
display_information: name: Bot tutorial description: Keep updated about Skillz app background_color: “#0000AA” features: app_home: home_tab_enabled: true messages_tab_enabled: true messages_tab_read_only_enabled: false bot_user: display_name: Name viewed by users always_online: true slash_commands: – command: /helloWorld url: https://<IP address generated by ngrok>/slack/events description: I am polite 🙂 should_escape: false oauth_config: scopes: bot: – commands – chat:write – users:read – users.profile:read – channels:read – mpim:write – im:write – chat:write.customize settings: event_subscriptions: request_url: https://<IP address generated by ngrok>/slack/events bot_events: – app_home_opened interactivity: is_enabled: true request_url: https://<IP address generated by ngrok>/slack/events org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false |
💡 It’s important to keep a specific order of each field.
Now I have to click on “Save changes” at the top right of the yaml file.
Now on Slack, I can see in the “About” section on Slack the command that I have added in the App Manifest and its description.
Let’s code
💡Slack has implemented the block kit builder which allows you to create any Slack component in a few clicks. I recommend using this application to create basic components. I will develop below how to understand and create each section of Slack components.
Start by creating 2 folders and 2 files in the “src” folder created previously. Create a “commands” folder which has a “helloWorld” folder and a “commandsHandler.js” file. Then add the “helloWorldCommand.js” file in the “helloWorld” folder. So my architecture looks like below :
💡I will continue to use this type of architecture for each type of method (actions, views, commands etc.) during each demonstration of this article. Because in my opinion, this is the best type of architecture to navigate through that I have found until now. Especially when we have to handle many commands, views and actions.
Set helloWorldCommand.js file
const { postSingleLineMessage } = require(“../../postMesages”); module.exports = { helloWorldCommand(app) { app.command(“/helloWorld”, async ({ ack, body }) => { await ack(); try { await postSingleLineMessage( body[“channel_id”], “Hello world command”, app, “Response from /helloWorld” ); } catch (e) { console.error(e); } }); }, }; |
Previously I had needed to use the url to get the channel ID. But thanks to the using of the “body” to get the channel ID, I can get the right channel in which the event has been detected.
The Slack app can use the command() method to listen to incoming slash command requests. This method requires a commandName of type string or RegExp. So in this case, the commandName is “/helloWorld”.
Furthermore, Commands must be acknowledged with ack() to inform Slack your app has received the request.
💡The body contains a lot of useful informations
Set commandsHandler.js file
const { helloWorldCommand } = require(“./helloWorld/helloWorldCommand”); module.exports = { commandsHandler(app) { helloWorldCommand(app); }, }; |
Now I can call the commandHandler in the “App.js” file to handle the commandHandler.js.
Slack render
Now I can test the command to send /helloWorld to the bot like this :
🎉 Now I have firstly the message sent in a raw way (the simple “Hello”) and sent at the starting of the bot, and then the “Hello world command” as response of the command.
Get events and open modal
The Slack API offers several methods to create different components (like pop-ups, and modals. See more here). I found it interesting to present how the modal component works globally because Modals: focused spaces for user interaction | Slack Slack API offers to catch the events that can happen in this modal and to model the components in a flexible way.
Catch action
async function postMessageWithButton( channelID, message, app, notificationMessage ) { try { await app.client.chat.postMessage({ token: app.token, channel: channelID, blocks: [ { type: “section”, text: { type: “mrkdwn”, text: `${message}`, }, accessory: { type: “button”, text: { type: “plain_text”, text: “Share with bot !”, }, action_id: “modalTrigger”, }, }, ], // Text in the notification text: `${notificationMessage}`, }); } catch (e) { console.error(“error”, e); } } |
In this example, I’ve firstly posted a message given by parameter and generated an accessory of type “button”, and filled it with the text “Share with bot !” and then added an action_id that I’ve named “modalTrigger”.
You can see all the components that can be used with an action_id.
Create a modal
This action_id can be retrieved in a Slack “action()” method that can be handled like below :
module.exports = { modalActions(app) { app.action(“modalTrigger”, async ({ ack, body }) => { await ack(); try { await app.client.views.open({ trigger_id: body.trigger_id, token: app.token, view: { type: “modal”, // View identifier callback_id: “modalTrigger”, title: { type: “plain_text”, text: “Daily message”, }, blocks: [], submit: { type: “plain_text”, text: “Submit”, }, }, }); } catch (error) { console.error(error); } }); app.action(“informationModal”, async ({ ack }) => { await ack(); }); }, }; |
So when the app detects the “modalTrigger” as trigger_id, the bot will create a modal thanks to views.open() method. Note that the action “informationModal” is mandatory to catch the events to close or submit in the modal.
The content of the modals can be updated easily thanks to the view() methods.
Accessories : Radio buttons
Then I add the initial statement of the modal and add some components that can handle the “trigger_id”.
await app.client.views.open({ trigger_id: body.trigger_id, token: app.token, view: { type: “modal”, // View identifier callback_id: “modalTrigger”, title: { type: “plain_text”, text: “Daily message”, }, blocks: [ { type: “section”, block_id: “first_block”, text: { type: “mrkdwn”, text: “How are you today ?”, }, accessory: { type: “radio_buttons”, action_id: “informationModal”, initial_option: { value: “fine”, text: { type: “plain_text”, text: “Yes I’m feeling good”, }, }, options: [ { value: “fine”, text: { type: “plain_text”, text: “Yes I’m feeling good”, }, }, { value: “bad”, text: { type: “plain_text”, text: “I don’t feeling good”, }, }, ], }, }, ], submit: { type: “plain_text”, text: “Submit”, }, }, }); |
I added this block just after the action called “modalTrigger”.
I’ve to set the initial options, and then add the radio buttons and fill it. See all about this element here.
Final render
So if I call the “postMessageWithButton()” function declared here in the “App.js” file, I can see my button with a personalized message :
And then, if I click on the button my modal is open, and I can click on the several buttons displayed.
Get information
Once a modal is created I can update the content, and get the events of the modal thanks to the view method.
module.exports = { modalView(app) { app.view(“modalTrigger”, async ({ ack, body, view }) => { await ack(); const howAreYouValue = view[“state”][“values”][“first_block”][“informationModal”][ “selected_option” ][“value”]; console.log(howAreYouValue); }); }, }; |
For this demonstration, I’ll simply use the view object delivered by the view method.
So now, when I click on “submit” from the modal, I can see the value of my radio button picked.
⚡️ I can send a Slack message or update the modal in relation to my response right now.
Conclusion
I had no experience in developing a bot, and I had never done a Javascript project from scratch before I started the Skillz bot project. But the bolt library and the Slack API are easy to use and very well documented so I really enjoyed developing it. It was a great exercise to understand how a bot could work, and the range of features it can offer. I hope this article will help understand how a Slack bot in JS can work.
The Skillz bot is available in production on the Zenika Slack, I hope you will enjoy using it!