Listening to messages

To listen to messages that your app has access to receive, you can use the message() method which filters out events that aren’t of type message.

message() accepts an optional pattern parameter of type string or RegExp object which filters out any messages that don’t match the pattern.

// This will match any message that contains 👋
app.message(':wave:', async ({ message, say}) => {
  say(`Hello, <@${message.user}>`);
});

Using a RegExp pattern

A RegExp pattern can be used instead of a string for more granular matching.

All of the results of the RegExp match will be in context.matches.

app.message(/^(hi|hello|hey).*/, async ({ context, say }) => {
  // RegExp matches are inside of context.matches
  const greeting = context.matches[0];
  
  say(`${greeting}, how are you?`);
});

Sending messages

Within your listener function, say() is available whenever there is an associated conversation (for example, a conversation where the event or action which triggered the listener occurred). say() accepts a string to post simple messages and JSON payloads to send more complex messages. The message payload you pass in will be sent to the associated conversation.

In the case that you’d like to send a message outside of a listener or you want to do something more advanced (like handle specific errors), you can call chat.postMessage using the client attached to your Bolt instance.

// Listens for messages containing "knock knock" and responds with an italicized "who's there?"
app.message('knock knock', ({ message, say }) => {
  say(`_Who's there?_`);
});

Sending a message with blocks

say() accepts more complex message payloads to make it easy to add functionality and structure to your messages.

To explore adding rich message layouts to your app, read through the guide on our API site and look through templates of common app flows in the Block Kit Builder.

// Sends a section block with datepicker when someone reacts with a 📅 emoji
app.event('reaction_added', ({ event, say }) => {
  if (event.reaction === 'calendar') {
    say({
      blocks: [{
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": "Pick a date for me to remind you"
          },
          "accessory": {
            "type": "datepicker",
            "action_id": "datepicker_remind",
            "initial_date": "2019-04-28",
            "placeholder": {
              "type": "plain_text",
              "text": "Select a date"
             }
          }
        }]});
  }
});

Listening to events

You can listen to any Events API event using the event() method after subscribing to it in your app configuration. This allows your app to take action when something happens in Slack, like a user reacting to a message or joining a channel.

The event() method requires an eventType of type string.

const welcomeChannelId = 'C12345';

// When a user joins the team, send a message in a predefined channel asking them to introduce themselves
app.event('team_join', async ({ event, context }) => {
  try {
    const result = await app.client.chat.postMessage({
      token: context.botToken,
      channel: welcomeChannelId,
      text: `Welcome to the team, <@${event.user.id}>! 🎉 You can introduce yourself in this channel.`
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

Filtering on message subtypes

A message() listener is equivalent to event('message')

You can filter on subtypes of events by using the built-in matchEventSubtype() middleware. Common message subtypes like bot_message and message_replied can be found on the message event page.

// Matches all messages from bot users
app.message(subtype('bot_message'), ({ message }) => {
  console.log(`The bot user ${message.user} said ${message.text}`);
});

Using the Web API

You can call any Web API method using the WebClient provided to your Bolt app as app.client (given that your app has the appropriate scopes). When you call one the client’s methods, it returns a Promise containing the response from Slack.

The token used to initialize Bolt can be found in the context object, which is required for most Web API methods.

// Unix Epoch time for September 30, 2019 11:59:59 PM
const whenSeptemberEnds = 1569887999;

app.message('wake me up', async ({ message, context }) => {
  try {
    // Call the chat.scheduleMessage method with a token
    const result = await app.client.chat.scheduleMessage({
      // The token you used to initialize your app is stored in the `context` object
      token: context.botToken,
      channel: message.channel.id,
      post_at: whenSeptemberEnds,
      text: 'Summer has come and passed'
    });
  }
  catch (error) {
    console.error(error);
  }
});

Listening to actions

Your app can listen to user actions like button clicks, menu selects, and message actions using the action method.

Actions can be filtered on an action_id of type string or RegExp object. action_ids act as unique identifiers for interactive components on the Slack platform.

You’ll notice in all action() examples, ack() is used. It is required to call the ack() function within an action listener to acknowledge that the event was received from Slack. This is discussed in the acknowledging events section.

// Your middleware will be called every time an interactive component with the action_id "approve_button" is triggered
app.action('approve_button', async ({ ack, say }) => {
  ack();
  // Update the message to reflect the action
});

Listening to actions using a constraint object

You can use a constraints object to listen to callback_ids, block_ids, and action_ids (or any combination of them). Constraints in the object can be of type string or RegExp object.

// Your middleware 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 ({ action, ack, context }) => {
    ack();
    try {
      const result = await app.client.reactions.add({
        token: context.botToken,
        name: 'white_check_mark',
        timestamp: action.ts,
        channel: action.channel.id
      });
    }
    catch (error) {
      console.error(error);
    }
});

Responding to actions

There are two main ways to respond to actions. The first (and most common) way is to use the say function. The say function sends a message back to the conversation where the incoming event took place.

The second way to respond to actions is using respond(), which is a simple utility to use the response_url associated with an action.

// Your middleware will be called every time an interactive component with the action_id “approve_button” is triggered
app.action('approve_button', ({ ack, say }) => {
  // Acknowledge action request
  ack();
  say('Request approved 👍');
});

Using respond()

Since respond() is a utility for calling the response_url, it behaves in the same way. You can pass a JSON object with a new message payload that will be published back to the source of the original interaction with optional properties like response_type (which has a value of in_channel or ephemeral), replace_original, and delete_original.

// Listens to actions triggered with action_id of “user_select”
app.action('user_choice', ({ action, ack, respond }) => {
	ack();
	respond(`You selected <@${action.selected_user}>`);
});

Acknowledging events

Actions, commands, and options events must always be acknowledged using the ack() function. This lets Slack know that the event was received and updates the Slack user interface accordingly. Depending on the type of event, your acknowledgement may be different. For example, when acknowledging a dialog submission you will call ack() with validation errors if the submission contains errors, or with no parameters if the submission is valid.

We recommend calling ack() right away before sending a new message or fetching information from your database since you only have 3 seconds to respond.

// Regex to determine if this is a valid email
let isEmail = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
// This uses a constraint object to listen for dialog submissions with a callback_id of ticket_submit 
app.action({ callback_id: 'ticket_submit' }, ({ action, ack }) => {
	// it’s a valid email, accept the submission
  if (isEmail.test(action.submission.email)) {
	  ack();
  } else {
		// if it isn’t a valid email, acknowledge with an error
	  ack({
		  errors: [{
			  "name": "email_address",
			  "error": "Sorry, this isn’t a valid email"
      }]
    });
  }
});

Listening and responding to commands

Your app can use the command() method to listen to incoming slash command events. The method requires a commandName of type string.

Commands must be acknowledged with ack() to inform Slack your app has received the event.

There are two ways to respond to slash commands. The first way is to use say(), which accepts a string or JSON payload. The second is respond() which is a utility for the response_url. These are explained in more depth in the responding to actions section.

// The echo command simply echoes on command
app.command('/echo', async ({ command, ack, say }) => {
  // Acknowledge command request
  ack();
  
  say(`${command.text}`);
});

Listening and responding to options

The option() method listens for incoming option request payloads from Slack. Similar to actions(), an action_id or constraints object is required.

While it’s recommended to use action_id for external_select menus, dialogs do not yet support Block Kit so you’ll have to use the constraints object to filter on a callback_id.

To respond to options requests, you’ll need to ack() with valid options. Both external select response examples and dialog response examples can be found on our API site.

// Example of responding to an external_select options request
app.options('external_action', async ({ options, ack }) => {
  // Get information specific to a team or channel
  const results = await db.get(options.team.id);
  
  if (results) {
    let options = [];
    // Collect information in options array to send in Slack ack response
    for (const result in results) {
      options.push({
        "text": {
          "type": "plain_text",
          "text": result.label
        },
        "value": result.value
      });
    }

    ack({
      "options": options
    });
  } else {
    ack();
  }
});

Handling errors

If an error occurs in a listener, it’s recommended you handle it directly. However, there are cases where errors may be triggered after your listener has already returned (such as in say(), respond(), or if you don’t call ack() when it’s required). By default, these errors will be logged to the console. To handle them yourself, you can attach a global error handler to your app with the error(fn) method.

If you want more control over errors, it’s advised to use the chat.postMessage method attached to your app under the client key (instead of say() or respond()). This returns a Promise that can be caught to handle the error.

app.error((error) => {
	// Check the details of the error to handle cases where you should retry sending a message or stop the app
	console.error(error);
});

Authorization

Authorization is the process of deciding which Slack credentials (such as a bot token) should be available while processing a specific incoming event.

Custom apps installed on a single workspace can simply use the token option at the time of App initialization. However, when your app needs to handle several tokens, such as cases where it will be installed on multiple workspaces or needs access to more than one user token, the authorize option should be used instead.

The authorize option can be set to a function that takes an event source as its input, and should return a Promise for an object containing the authorized credentials. The source contains information about who and where the event is coming from by using properties like teamId (always available), userId, conversationId, and enterpriseId.

The authorized credentials should also have a few specific properties: botToken, userToken, botId (required for an app to ignore messages from itself), and botUserId. You can also include any other properties you’d like to make available on the context object.

You should always provide either one or both of the botToken and userToken properties. At least one of them is necessary to make helpers like say() work. If they are both given, then botToken will take precedence.

const app = new App({ authorize: authorizeFn, signingSecret: process.env.SLACK_SIGNING_SECRET });

// NOTE: This is for demonstration purposes only.
// All sensitive data should be stored in a secure database
// Assuming this app only uses bot tokens, the following object represents a model for storing the credentials as the app is installed into multiple workspaces.

const installations = [
  {
    enterpriseId: 'E1234A12AB',
    teamId: 'T12345',
    botToken: 'xoxb-123abc',
    botId: 'B1251',
    botUserId: 'U12385',
  },
  {
    teamId: 'T77712',
    botToken: 'xoxb-102anc',
    botId: 'B5910',
    botUserId: 'U1239',
  },
];

const authorizeFn = async ({ teamId, enterpriseId }) => {
  // Fetch team info from database
  for (const team in installations) {
    // Check for matching teamId and enterpriseId in the installations array
    if ((team.teamId === teamId) && (team.enterpriseId === enterpriseId)) {
      // This is a match. Use these installation credentials.
      return {
        // You could also set userToken instead
        botToken: team.botToken,
        botId: team.botId,
        botUserId: team.botUserId
      };
    }
  }
  
  throw new Error('No matching authorizations');
}

Conversation stores

Bolt includes support for a store, which sets and retrieves state related to a conversation. Conversation stores have two methods:

  • set() modifies conversation state. set() requires a conversationId of type string, value of any type, and an optional expiresAt of type number. set() returns a Promise.
  • get() fetches conversation state from the store. get() requires a conversationId of type string and returns a Promise with the conversation’s state.

conversationContext() is a built-in global middleware that allows conversations to be updated by other middleware. When receiving an event, middleware functions can use context.updateConversation() to set state and context.conversation to retrieve it.

The built-in conversation store simply stores conversation state in memory. While this is sufficient for some situations, if there is more than one instance of your app running, the state will not be shared among the processes so you’ll want to implement a conversation store that fetches conversation state from a database.

const app = new App({
  token,
  signingSecret,
  // It's more likely that you'd create a class for a convo store
  convoStore: new simpleConvoStore()
});

// A simple implementation of a conversation store with a Firebase-like database
class simpleConvoStore {
  set(conversationId, value, expiresAt) {
    // Returns a Promise
    return db().ref('conversations/' + conversationId).set({ value, expiresAt });
  }

  get(conversationId) {
    // Returns a Promise
    return new Promise((resolve, reject) => {
      db().ref('conversations/' + conversationId).once('value').then((result) => {
        if (result !== undefined) {
          if (result.expiresAt !== undefined && Date.now() > result.expiresAt) {
            db().ref('conversations/' + conversationId).delete();

            reject(new Error('Conversation expired'));
          }
          resolve(result.value)
        } else {
          // Conversation not found
          reject(new Error('Conversation not found'));
        }
      });
    });
  }
}

Global middleware

Global middleware is run for all incoming events before any listener middleware. You can add any number of global middleware to your app by utilizing app.use(fn(payload,...,next)).

Both global and listener middleware must call next() to pass control of the execution chain to the next middleware, or call next(error) to pass an error back up the previously-executed middleware chain.

As an example, let’s say your app should only respond to users identified with a corresponding internal authentication service (an SSO provider or LDAP, for example). You may define a global middleware that looks up a user record in the authentication service and errors if the user is not found.

// Authentication middleware that associates incoming event with user in Acme identity provider
function authWithAcme({ payload, context, say, next }) {
  const slackUserId = payload.user;
  const helpChannelId = 'C12345';

  // Assume we have a function that accepts a Slack user ID to find user details from Acme
  acme.lookupBySlackId(slackUserId)
    .then((user) => {
      // When the lookup is successful, populate context with Acme user details
      context.user = user;

      // Pass control to the next middleware and any listener functions
      next();
    })
    .catch((error) => {
      // This user wasn't found in Acme. Send them an error and don't continue processing event
      if (error.message === 'Not Found') {
        app.client.chat.postEphemeral({
          token: context.botToken,
          channel: payload.channel,
          user: slackUserId,
          text: `Sorry <@${slackUserId}, you aren't registered in Acme. Please post in <#${helpChannelId} for assistance.`
        });
        return;
      }

      // Pass control to previous middleware (if any) or the global error handler
      next(error);
    });
}

Listener middleware

Listener middleware is used for logic across many listener functions (but usually not all of them). They are added as arguments before the listener function in one of the built-in methods. You can add any number of listener middleware before the listener function.

There’s a collection of built-in listener middleware that you can use like subtype() for filtering on a message subtype and directMention() which filters out any message that doesn’t directly @-mention your bot.

But of course, you can write your own middleware for more custom functionality. While writing your own middleware, your function must call next() to pass control to the next middleware, or call next(error) to pass an error back up the previously-executed middleware chain.

As an example, let’s say your listener should only deal with messages from humans. You can write a listener middleware that excludes any bot messages.

// Listener middleware that filters out messages with 'bot_message' subtype
function noBotMessages({ message, next }) {
  if (!message.subtype || message.subtype !== 'bot_message') {
     next();
  }
}

// The listener only receives messages from humans
app.message(noBotMessages, ({ message }) => console.log(
  `(MSG) User: ${message.user}
   Message: ${message.text}`
));

Adding context

All listeners have access to a context object, which can be used to enrich events with additional information. For example, perhaps you want to add user information from a third party system or add temporary state for the next middleware in the chain.

context is just an object, so you can add to it by setting it to a modified version of itself.

async function addTimezoneContext ({ payload, context, next }) {
  const user = await app.client.users.info({
    token: context.botToken,
    user: payload.user_id,
    include_locale: true
  });

  // Add user's timezone context
  context.tz_offset = user.tz_offset;
  
  // Pass control to the next middleware function
  next();
}

app.command('request', addTimezoneContext, async ({ command, ack, context }) => {
  // Acknowledge command request
  ack();
  // Get local hour of request
  const local_hour = (Date.UTC() + context.tz_offset).getHours();

  // Request channel ID
  const requestChannel = 'C12345';

  const requestText = `:large_blue_circle: *New request from <@${command.user_id}>*: ${command.text}`;

  // If request not inbetween 9AM and 5PM, send request tomorrow
  if (local_hour > 17 || local_hour < 9) {
    // Assume function exists to get local tomorrow 9AM from offset
    const local_tomorrow = getLocalTomorrow(context.tz_offset);

    try {
      // Schedule message
      const result = await app.client.chat.scheduleMessage({
        token: context.botToken,
        channel: requestChannel,
        text: requestText,
        post_at: local_tomorrow
      });
    }
    catch (error) {
      console.error(error);
    }
  } else {
    try {
      // Post now
      const result = app.client.chat.postMessage({
        token: context.botToken,
        channel: requestChannel,
        text: requestText
      });
    }
    catch (error) {
      console.error(error);
    }
  }
});

Logging

By default, Bolt will log information from your app to the console. You can customize how much logging occurs by passing a logLevel in the constructor. The available log levels in order of most to least logs are DEBUG, INFO, WARN, and ERROR.

// Import LogLevel from the package
const { App, LogLevel } = require('@slack/bolt');

// Log level is one of the options you can set in the constructor
const app = new App({
  token,
  signingSecret,
  logLevel: LogLevel.DEBUG,
});

Sending log output somewhere besides the console

If you want to send logs to somewhere besides the console or want more control over the logger, you can implement a custom logger. A custom logger must implement specific methods (known as the Logger interface):

Method Parameters Return type
setLevel() level: LogLevel void
setName() name: string void
debug() ...msgs: any[] void
info() ...msgs: any[] void
warn() ...msgs: any[] void
error() ...msgs: any[] void

A very simple custom logger might ignore the name and level, and write all messages to a file.

const { App } = require('@slack/bolt');
const { createWriteStream } = require('fs');
const logWritable = createWriteStream('/var/my_log_file'); // Not shown: close this stream

const app = new App({
  token,
  signingSecret,
  // Creating a logger as a literal object. It's more likely that you'd create a class.
  logger: {
    debug(...msgs): { logWritable.write('debug: ' + JSON.stringify(msgs)); },
    info(...msgs): { logWritable.write('info: ' + JSON.stringify(msgs)); },
    warn(...msgs): { logWritable.write('warn: ' + JSON.stringify(msgs)); },
    error(...msgs): { logWritable.write('error: ' + JSON.stringify(msgs)); },
    setLevel(): { },
    setName(): { },
  },
});

Customizing a receiver

A receiver is responsible for handling and parsing any incoming events from Slack, then emitting the event so the Bolt app can add context and pass it to your app’s listeners. Receivers must conform to the Receiver interface:

Method Parameters Return type
on() type: string, listener: fn() unknown
start() None Promise
stop() None Promise

on() is called two times in the Bolt app:

  • Receiver.on('message', listener) should route all incoming requests that have been parsed to onIncomingEvent(). It’s called in the Bolt app as this.receiver.on('message', message => this.onIncomingEvent(message)).
  • Receiver.on('error', listener) should route errors to the global error handler. It’s called in the Bolt app as this.receiver.on('error', error => this.onGlobalError(error)).

To use a custom receiver, you can pass it into the constructor when initializing your Bolt app. Here is what a basic custom receiver might look like.

For a more in-depth look at a receiver, read the source code for the built-in Express receiver

import { EventEmitter } from 'events';
import { createServer } from 'http';
import express from 'express';

// EventEmitter handles the on() function for us
// https://nodejs.org/api/events.html#events_emitter_on_eventname_listener
class simpleReceiver extends EventEmitter {
  constructor(signingSecret, endpoints) {
    super();
    this.app = express();
    this.server = createServer(this.app);

    for (const endpoint of endpoints) {
      this.app.post(endpoint, this.requestHandler.bind(this));
    }
  }

  start(port) {
    return new Promise((resolve, reject) => {
      try {
        this.server.listen(port, () => {
          resolve(this.server);
        });
      } catch(error) {
        reject(error);
      }
    });
  }

  stop() {
    return new Promise((resolve, reject) => {
      this.server.close((error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      })
    })
  }

  // This is a very simple implementation. Look at the ExpressReceiver source for more detail
  requestHandler(req, res) {
    // Assume parseBody function exists to parse incoming requests
    const parsedReq = parseBody(req);
    const event = {
      body: parsedReq.body,
      // Receivers are responsible for handling acknowledgements
      ack: (response) => {
        if (!response) {
          res.send('')
        } else {
          res.send(response);
        }
      }
    };
    this.emit('message', event);
  }
}