Published: November 22, 2019
Updated: March 3, 2023
Your Slack app's App Home is a focused, 1:1 space in Slack shared between individual users and your app. Within every App Home, users find multiple tabs: About, Messages, and the newly introduced Home tab. The Home tab is a dynamic and persistent visual interface allowing users to interact privately with your app. Your app can greet users, showcase customized content, or even be a little playful!
This new feature may sound familiar to you! There's been an event called app_home_opened
for some time, triggered when a user selects an app from the left-hand menu. It's a great opportunity, showcased in this tutorial, to welcome a user with a message with timely information.
With the Home tab, your app can go beyond messaging to display more organized, dynamic content with blocks in a persistent location independent of conversation.
This is what the new surface area looks like for the Google Calendar Slack app:
You can view your daily calendar from the app's Home tab, where you can modify your invitation response or join a conference call on Zoom. The Message
tab is where the app sends you direct messages. For example, in Google Calendar, the app notifies you by sending a message 1 minute before a meeting. The About
tab is where you can view information about the app.
To demonstrate what you can do with App Home, I am going to walk you through with an app called Stickies, which allows users to store short, private notes within the App Home.
app_home_opened
event gets triggered and sent to the app server.views.publish
method.views.open
method.view_submission
.views.publish
method.Now, let’s create the Stickies app for your workspace. The source code of the app is on Glitch, where you can "remix" and run without deploying code. Click the magnifying glass at the top of the page, type Stickies into the search box, and hit Enter. Click on the Stickies app when it appears in the results, and then click View Source to view the app's source code.
First of all, you need to set up your app on Slack. Go to Slack App Management to create an app or click the button:
Next, go to Features > OAuth & Permissions to specify the Bot Token Scopes. Select chat:write
. (Technically, our sample app does not send any messages, but just follow this instruction for now. To learn more about this new more granular bot permission model, read Installing with OAuth 2.0, Version 2!)
Navigate to Features > App Home and enable the Home Tab.
Navigate to Features > Event Subscriptions to enable events (See Step 1 in the screenshot below). Then, enter your Request URL (Step 2). If you remixed the example Glitch code, your Request URL should be https://your-project.glitch.me/slack/events
. Glitch generates a project name when you create a project, so you'll have a project name composed of two or three random words, such as obtainable-lovely-friday. (You can also customize the project name.) If you're running on your own server, append /slack/events
to the URL.
Then, scroll down to Subscribe to bot events to add app_home_opened
event (Step 3). Then, click Save Changes (Step 4).
Similarly, you will need to navigate to Features > Interactivity & Shortcuts to tell Slack where to send interactive payloads. Use your Request URL, https://your-project.glitch.me/slack/actions
, then save.
Let’s install the app. Go to Install App and click Install to Workspace
to install the app to your workspace, and follow the prompts. Once the installation process is finished, you will now have your OAuth access tokens on screen.
Now, get ready with your Glitch project window in your browser or IDE. Click on the .env
file on the left pane. This is where your environment variables are stored. Copy the bot token, which begins with xoxb
, and paste it into the variable SLACK_BOT_TOKEN
.
Also in the Slack app config page, get your Signing Secret key at Settings > Basic Information, then copy and paste it to the .env as well.
In this tutorial, I am using Node.js with Express as the web server. All API calls are done with straightforward HTTP requests and responses. Hopefully, the code is readily comprehensible for any language you use.
⚡️ If you prefer developing with Bolt for JavaScript framework, the source code is also available. But this tutorial uses “vanilla” JS code!
In your Node.js code, include dependencies and spin up your Express server. You'll need to evaluate the raw request payload to verify the signing secret from Slack. The index.js
file shows how to run a server with Express, and demonstrates checking the HTTP headers to verify request signature. (For more details about using Signing Secret with Express and Body Parser in Node.js, please read the Verifying the Requests section in this tutorial).
app_home_opened
event Next, use an HTTP POST method route to create an endpoint to receive the event payload. This is where Slack API server sends you a JSON payload when an event is fired. Once you receive the data, check if the event type is app_home_opened
, then prepare to display the App Home view.
Here is the code snippet from index.js
:
app.post('/slack/events', async(req, res) => {
switch (req.body.type) {
case 'url_verification': {
// verify Events API endpoint by returning challenge if present
res.send({ challenge: req.body.challenge });
break;
}
case 'event_callback': {
// Verify the signing secret
if (!signature.isVerified(req)) {
res.sendStatus(404);
return;
}
// Request is verified --
else {
const {type, user, channel, tab, text, subtype} = req.body.event;
// Triggered when the App Home is opened by a user
if(type === 'app_home_opened') {
// Display App Home
appHome.displayHome(user);
}
Now, let’s display a rich content in App Home view with rich message layout, Block Kit:
const displayHome = async(user, data) => {
const args = {
token: process.env.SLACK_BOT_TOKEN,
user_id: user,
view: await updateView(user)
};
const result = await axios.post('/views.publish', qs.stringify(args));
};
To display content in the App Home, call view.publish
method. In this example, I am using the axios
module to handle the API calls via HTTP POST.
In this code example, I am calling another function to create JSON to construct the view to be displayed. This function can be reused when you update the view when new content is added later.
This code snippet shows how to build and display the initial view:
const updateView = async(user) => {
let blocks = [
{
// Section with text and a button
type: "section",
text: {
type: "mrkdwn",
text: "*Welcome!* \nThis is a home for Stickers app. You can add small notes here!"
},
accessory: {
type: "button",
action_id: "add_note",
text: {
type: "plain_text",
text: "Add a Stickie"
}
}
},
// Horizontal divider line
{
type: "divider"
}
];
let view = {
type: 'home',
title: {
type: 'plain_text',
text: 'Keep notes!'
},
blocks: blocks
}
return JSON.stringify(view);
};
The blocks
array definied in the code snippet above is prototyped with Block Kit Builder.
In the actual source code, the function is dynamic where it takes additional content from the interactive button and modal. I’ll explain this in a later section.
When a user clicks the button, a modal pops open.
Notice the action_id
is specified in the message building block. Use the identifier to grab the data we need. Once a user clicks the button, the API server sends your Request URL a payload of the user action, containing a trigger_id
. You need this to open a modal.
app.post('/slack/actions', async(req, res) => {
const { token, trigger_id, user, actions, type } = JSON.parse(req.body.payload);
if(actions && actions[0].action_id.match(/add_/)) {
openModal(trigger_id);
}
});
This is how you create form elements (input box and a drop-down menu with a submit button) in a modal view. For this exercise, we'll make a form with a multi-line text input and pick-a-color dropdown.
To open the modal, call the views.open
method:
const openModal = async(trigger_id) => {
const modal = {
type: 'modal',
title: {
type: 'plain_text',
text: 'Create a stickie note'
},
submit: {
type: 'plain_text',
text: 'Create'
},
blocks: [
// Text input
{
"type": "input",
"block_id": "note01",
"label": {
"type": "plain_text",
"text": "Note"
},
"element": {
"action_id": "content",
"type": "plain_text_input",
"placeholder": {
"type": "plain_text",
"text": "Take a note... "
},
"multiline": true
}
},
// Drop-down menu
{
"type": "input",
"block_id": "note02",
"label": {
"type": "plain_text",
"text": "Color",
},
"element": {
"type": "static_select",
"action_id": "color",
"options": [
{
"text": {
"type": "plain_text",
"text": "yellow"
},
"value": "yellow"
},
{
"text": {
"type": "plain_text",
"text": "blue"
},
"value": "blue"
}
]
}
}
]
};
const args = {
token: process.env.SLACK_BOT_TOKEN,
trigger_id: trigger_id,
view: JSON.stringify(modal)
};
const result = await axios.post('https://slack.com/api/views.open', qs.stringify(args));
};
The code snippet seems long, but as you can see, the code is mostly just constructing a JSON for the form UI! See how it is built on Block Kit Builder.
The submission from a user is handled in the same way the button click from the Home tab was handled.
When the form in the modal is submitted, a payload is sent to the same endpoint of the action. You can differentiate the submission by checking the type
in the payload data. The full code snippet from the index.js
file is below:
app.post('/slack/actions', async(req, res) => {
//console.log(JSON.parse(req.body.payload));
const { token, trigger_id, user, actions, type } = JSON.parse(req.body.payload);
// Button with "add_" action_id clicked --
if(actions && actions[0].action_id.match(/add_/)) {
// Open a modal window with forms to be submitted by a user
appHome.openModal(trigger_id);
}
// Modal forms submitted --
else if(type === 'view_submission') {
res.send(''); // Make sure to respond to the server to avoid an error
const ts = new Date();
const { user, view } = JSON.parse(req.body.payload);
const data = {
timestamp: ts.toLocaleString(),
note: view.state.values.note01.content.value,
color: view.state.values.note02.color.selected_option.value
}
appHome.displayHome(user.id, data);
}
});
Append the newly acquired data from the user to the current view block, and rerender the Home tab view using views.publish
.
In this example app, we're using a persistent database with the node-json-db
module. Each time a user adds a new note, the data is pushed to the data array. I am creating a new data block in JSON then appending it to the existing JSON and then displaying the new view by calling views.publish
.
You can see the source code in the appHome.js
file below:
const updateView = async(user) => {
// Intro message -
let blocks = [
{
type: "section",
text: {
type: "mrkdwn",
text: "Hello! Make a note of things you don't want to forget."
},
accessory: {
type: "button",
action_id: "add_note",
text: {
type: "plain_text",
text: "Add sticky note",
emoji: true
}
}
},
{
type: "divider"
}
];
// Append new data blocks after the intro -
let newData = [];
try {
const rawData = db.getData(`/${user}/data/`);
newData = rawData.slice().reverse(); // Reverse to make the latest first
newData = newData.slice(0, 50); // Just display 20. Block Kit display has some limit.
} catch(error) {
//console.error(error);
};
if(newData) {
let noteBlocks = [];
for (const o of newData) {
const color = (o.color) ? o.color : 'yellow';
let note = o.note;
if (note.length > 3000) {
note = note.substr(0, 2980) + '... _(truncated)_'
console.log(note.length);
}
noteBlocks = [
{
type: "section",
text: {
type: "mrkdwn",
text: note
},
accessory: {
type: "image",
image_url: `https://cdn.glitch.com/0d5619da-dfb3-451b-9255-5560cd0da50b%2Fstickie_${color}.png`,
alt_text: "stickie note"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": o.timestamp
}
]
},
{
type: "divider"
}
];
blocks = blocks.concat(noteBlocks);
}
}
// The final view -
let view = {
type: 'home',
title: {
type: 'plain_text',
text: 'Keep notes!'
},
blocks: blocks
}
return JSON.stringify(view);
};
/* Display App Home */
const displayHome = async(user, data) => {
if(data) {
// Store in a local DB
db.push(`/${user}/data[]`, data, true);
}
const args = {
token: process.env.SLACK_BOT_TOKEN,
user_id: user,
view: await updateView(user)
};
const result = await axios.post(`${apiUrl}/views.publish`, qs.stringify(args));
try {
if(result.data.error) {
console.log(result.data.error);
}
} catch(e) {
console.log(e);
}
};
But, it's up to you how you want to achieve this flow.
Now your app should be working. To enable it, go to your Slack workspace, click Apps from the sidebar menu, where you should see a list of all installed apps, and click your app. See how App Home works by playing around with the app!
I hope this tutorial gives you some ideas on how you can use App Home for your app!
This tutorial only covered the fundamental parts of building an App Home experience using views
methods and modals.