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 }) => {
  await 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];

  await 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', async ({ message, say }) => {
  await 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', async ({ event, say }) => {
  if (event.reaction === 'calendar') {
    await 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, and menu selects, 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.

Note: Since v2, message shortcuts (previously message actions) now use the shortcut() method instead of the action() method. View the migration guide for V2 to learn about the changes.

// 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 }) => {
  await 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 }) => {
    await 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', async ({ ack, say }) => {
  // Acknowledge action request
  await ack();
  await 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', async ({ action, ack, respond }) => {
  await ack();
  await 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' }, async ({ action, ack }) => {
  // it’s a valid email, accept the submission
  if (isEmail.test(action.submission.email)) {
    await ack();
  } else {
    // if it isn’t a valid email, acknowledge with an error
    await ack({
      errors: [{
        "name": "email_address",
        "error": "Sorry, this isn’t a valid email"
      }]
    });
  }
});

Listening and responding to shortcuts

The shortcut() method supports both global shortcuts and message shortcuts.

Shortcuts are invokable UI elements within Slack clients. For global shortcuts, they are available in the composer and search menus. For message shortcuts, they are available in the context menus of messages. Your app can use the shortcut() method to listen to incoming shortcut events. The method requires a callback_id parameter of type string or RegExp.

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

Shortcuts include a trigger_id which an app can use to open a modal that confirms the action the user is taking.

When configuring shortcuts within your app configuration, you’ll continue to append /slack/events to your request URL.

⚠️ Note that global shortcuts do not include a channel ID. If your app needs access to a channel ID, you may use a conversations_select element within a modal. Message shortcuts do include channel ID.

// The open_modal shortcut opens a plain old modal
app.shortcut('open_modal', async ({ shortcut, ack, context, client }) => {

  try {
    // Acknowledge shortcut request
    await ack();

    // Call the views.open method using one of the built-in WebClients
    const result = await client.views.open({
      // The token you used to initialize your app is stored in the `context` object
      token: context.botToken,
      trigger_id: shortcut.trigger_id,
      view: {
        type: "modal",
        title: {
          type: "plain_text",
          text: "My App"
        },
        close: {
          type: "plain_text",
          text: "Close"
        },
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
            }
          },
          {
            type: "context",
            elements: [
              {
                type: "mrkdwn",
                text: "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
              }
            ]
          }
        ]
      }
    });

    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

Listening to shortcuts using a constraint object

You can use a constraints object to listen to callback_ids, and types. Constraints in the object can be of type string or RegExp object.

  // Your middleware will only be called when the callback_id matches 'open_modal' AND the type matches 'message_action'
  app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ shortcut, ack, context, client }) => {
    try {
      // Acknowledge shortcut request
      await ack();

      // Call the views.open method using one of the built-in WebClients
      const result = await client.views.open({
        // The token you used to initialize your app is stored in the `context` object
        token: context.botToken,
        trigger_id: shortcut.trigger_id,
        view: {
          type: "modal",
          title: {
            type: "plain_text",
            text: "My App"
          },
          close: {
            type: "plain_text",
            text: "Close"
          },
          blocks: [
            {
              type: "section",
              text: {
                type: "mrkdwn",
                text: "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
              }
            },
            {
              type: "context",
              elements: [
                {
                  type: "mrkdwn",
                  text: "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
                }
              ]
            }
          ]
        }
      });

      console.log(result);
    }
    catch (error) {
      console.error(error);
    }
  });

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.

When configuring commands within your app configuration, you’ll continue to append /slack/events to your request URL.

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

  await say(`${command.text}`);
});

Opening modals

Modals are focused surfaces that allow you to collect user data and display dynamic information. You can open a modal by passing a valid trigger_id and a view payload to the built-in client’s views.open method.

Your app receives trigger_ids in payloads sent to your Request URL triggered user invocation like a slash command, button press, or interaction with a select menu.

Read more about modal composition in the API documentation.

// Listen for a slash command invocation
app.command('/ticket', async ({ ack, body, context }) => {
  // Acknowledge the command request
  await ack();

  try {
    const result = await app.client.views.open({
      token: context.botToken,
      // Pass a valid trigger_id within 3 seconds of receiving it
      trigger_id: body.trigger_id,
      // View payload
      view: {
        type: 'modal',
        // View identifier
        callback_id: 'view_1',
        title: {
          type: 'plain_text',
          text: 'Modal title'
        },
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: 'Welcome to a modal with _blocks_'
            },
            accessory: {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'Click me!'
              },
              action_id: 'button_abc'
            }
          },
          {
            type: 'input',
            block_id: 'input_c',
            label: {
              type: 'plain_text',
              text: 'What are your hopes and dreams?'
            },
            element: {
              type: 'plain_text_input',
              action_id: 'dreamy_input',
              multiline: true
            }
          }
        ],
        submit: {
          type: 'plain_text',
          text: 'Submit'
        }
      }
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

Updating and pushing views

Modals contain a stack of views. When you call views.open, you add the root view to the modal. After the initial call, you can dynamically update a view by calling views.update, or stack a new view on top of the root view by calling views.push.

views.update
To update a view, you can use the built-in client to call views.update with the view_id that was generated when you opened the view, and a new view including the updated blocks array. If you’re updating the view when a user interacts with an element inside of an existing view, the view_id will be available in the body of the request.

views.push
To push a new view onto the view stack, you can use the built-in client to call views.push with a valid trigger_id a new view payload. The arguments for views.push is the same as opening modals. After you open a modal, you may only push two additional views onto the view stack.

Learn more about updating and pushing views in our API documentation.

// Listen for a button invocation with action_id `button_abc` (assume it's inside of a modal)
app.action('button_abc', async ({ ack, body, context }) => {
  // Acknowledge the button request
  await ack();

  try {
    const result = await app.client.views.update({
      token: context.botToken,
      // Pass the view_id
      view_id: body.view.id,
      // View payload with updated blocks
      view: {
        type: 'modal',
        // View identifier
        callback_id: 'view_1',
        title: {
          type: 'plain_text',
          text: 'Updated modal'
        },
        blocks: [
          {
            type: 'section',
            text: {
              type: 'plain_text',
              text: 'You updated the modal!'
            }
          },
          {
            type: 'image',
            image_url: 'https://media.giphy.com/media/SVZGEcYt7brkFUyU90/giphy.gif',
            alt_text: 'Yay! The modal was updated'
          }
        ]
      }
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

Listening for view submissions

If a view payload contains any input blocks, you must listen to view_submission events to receive their values. To listen to view_submission events, you can use the built-in view() method.

view() requires a callback_id of type string or RegExp.

You can access the value of the input blocks by accessing the state object. state contains a values object that uses the block_id and unique action_id to store the input values.

Read more about view submissions in our API documentation.

// Handle a view_submission event
app.view('view_b', async ({ ack, body, view, context }) => {
  // Acknowledge the view_submission event
  await ack();

  // Do whatever you want with the input data - here we're saving it to a DB then sending the user a verifcation of their submission

  // Assume there's an input block with `block_1` as the block_id and `input_a`
  const val = view['state']['values']['block_1']['input_a'];
  const user = body['user']['id'];

  // Message to send user
  let msg = '';
  // Save to DB
  const results = await db.set(user.input, val);

  if (results) {
    // DB save was successful
    msg = 'Your submission was successful';
  } else {
    msg = 'There was an error with your submission';
  }

  // Message the user
  try {
    await app.client.chat.postMessage({
      token: context.botToken,
      channel: user,
      text: msg
    });
  }
  catch (error) {
    console.error(error);
  }

});

Listening and responding to options

The options() method listens for incoming option request payloads from Slack. Similar to action(), 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
      });
    }

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

Authenticating with OAuth

Slack apps installed on multiple workspaces will need to implement OAuth, then store installation information (like access tokens) securely. By providing clientId, clientSecret, stateSecret and scopes when initializing App, Bolt for JavaScript will handle the work of setting up OAuth routes and verifying state. Your app only has built-in OAuth support when using the built-in ExpressReceiver. If you’re implementing a custom receiver, you can make use of our OAuth library, which is what Bolt for JavaScript uses under the hood.

Bolt for JavaScript will create a Redirect URL slack/oauth_redirect, which Slack uses to redirect users after they complete your app’s installation flow. You will need to add this Redirect URL in your app configuration settings under OAuth and Permissions. This path can be configured in the installerOptions argument described below.

Bolt for JavaScript will also create a slack/install route, where you can find an Add to Slack button for your app to perform direct installs of your app. If you need any additional authorizations (user tokens) from users inside a team when your app is already installed or a reason to dynamically generate an install URL, manually instantiate an ExpressReceiver, assign the instance to a variable named receiver, and then call receiver.installer.generateInstallUrl(). Read more about generateInstallUrl() in the OAuth docs.

To learn more about the OAuth installation flow with Slack, read the API documentation.

const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET
  stateSecret: 'my-state-secret',
  scopes: ['channels:read', 'groups:read', 'channels:manage', 'chat:write', 'incoming-webhook'],
  installationStore: {
    storeInstallation: async (installation) => {
      // change the line below so it saves to your database
      return await database.set(installation.team.id, installation);
    },
    fetchInstallation: async (InstallQuery) => {
      // change the line below so it fetches from your database
      return await database.get(InstallQuery.teamId);
    },
  },
});

Customizing OAuth defaults

You can override the default OAuth using the installerOptions object, which can be passed in during the initialization of App. You can override the following:

  • authVersion: Used to toggle between new Slack Apps and Classic Slack Apps
  • metadata: Used to pass around session related information
  • installPath: Override default path for “Add to Slack” button
  • redirectUriPath: Override default redirect url path
  • callbackOptions: Provide custom success and failure pages at the end of the OAuth flow
  • stateStore: Provide a custom state store instead of using the built in ClearStateStore
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET
  scopes: ['channels:read', 'groups:read', 'channels:manage', 'chat:write', 'incoming-webhook'],
  installerOptions: {
      authVersion: 'v1', // default  is 'v2', 'v1' is used for classic slack apps
      metadata: 'some session data',
      installPath: '/slack/installApp',
      redirectUriPath: '/slack/redirect',
      callbackOptions: {
        success: (installation, installOptions, req, res) => {
          // Do custom success logic here
          res.send('successful!');
        }, 
        failure: (error, installOptions , req, res) => {
          // Do custom failure logic here
          res.send('failure');
        }
      },
      stateStore: {
        // Do not need to provide a `stateSecret` when passing in a stateStore
        // generateStateParam's first argument is the entire InstallUrlOptions object which was passed into generateInstallUrl method
        // the second argument is a date object
        // the method is expected to return a string representing the state
        generateStateParam: async (installUrlOptions, date) => {
          // generate a random string to use as state in the URL
          const randomState = randomStringGenerator();
          // save installOptions to cache/db
          await myDB.set(randomState, installUrlOptions);
          // return a state string that references saved options in DB
          return randomState;
        },
        // verifyStateParam's first argument is a date object and the second argument is a string representing the state
        // verifyStateParam is expected to return an object representing installUrlOptions
        verifyStateParam:  async (date, state) => {
          // fetch saved installOptions from DB using state reference
          const installUrlOptions = await myDB.get(randomState);
          return installUrlOptions;
        }
      },
  }
});

Handling errors

Note: Since v2, error handling has improved! View the migration guide for V2 to learn about the changes.

If an error occurs in a listener, it’s recommended you handle it directly with a try/catch. However, there still may be cases where errors slip through the cracks. 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 app.error(fn) method.

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 of 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 for JavaScript 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). The middleware function fn is called with the same arguments as listeners and an additional next function.

Both global and listener middleware must call await next() to pass control of the execution chain to the next middleware, or call throw 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.

Note: Since v2, global middleware was updated to support async functions! View the migration guide for V2 to learn about the changes.

// Authentication middleware that associates incoming event with user in Acme identity provider
async function authWithAcme({ payload, context, 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
  try {
    // Assume we have a function that can take a Slack user ID as input to find user details from the provider
    const user = await acme.lookupBySlackId(slackUserId);
      
    // When the user lookup is successful, add the user details to the context
    context.user = user;
  } catch (error) {
    // This user wasn't found in Acme. Send them an error and don't continue processing event
    if (error.message === 'Not Found') {
        await 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
    throw error;
  }
  
  // Pass control to the next middleware (if there are any) and the listener functions
  // Note: You probably don't want to call this inside a `try` block, or any middleware
  //       after this one that throws will be caught by it. 
  await next();
}

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 await next() to pass control to the next middleware, or throw 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.

Note: Since v2, listener middleware was updated to support async functions! View the migration guide for V2 to learn about the changes.

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

// The listener only receives messages from humans
app.message(noBotMessages, async ({ 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
  await next();
}

app.command('request', addTimezoneContext, async ({ command, ack, context }) => {
  // Acknowledge command request
  await 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 for JavaScript 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
getLevel() None string with value error, warn, info, or debug
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 sending it to the app, so that the app can add context and pass the event to your listeners. Receivers must conform to the Receiver interface:

Method Parameters Return type
init() app: App unknown
start() None Promise
stop() None Promise

init() is called after Bolt for JavaScript app is created. This method gives the receiver a reference to an App to store so that it can call:

  • await app.processEvent(event) whenever your app receives an event from Slack. It will throw if there is an unhandled error.

To use a custom receiver, you can pass it into the constructor when initializing your Bolt for JavaScript 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 ExpressReceiver

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

class SimpleReceiver  {
  constructor(signingSecret, endpoints) {
    this.app = express();
    this.server = createServer(this.app);

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

  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
  async requestHandler(req, res) {
    let ackCalled = false;
    // Assume parseBody function exists to parse incoming requests
    const parsedReq = parseBody(req);
    const event = {
      body: parsedReq.body,
      // Receivers are responsible for handling acknowledgements
      // `ack` should be prepared to be called multiple times and 
      // possibly with `response` as an error
      ack: (response) => {
        if (ackCalled) {
          return;
        }
        
        if (response instanceof Error) {
          res.status(500).send();
        } else if (!response) {
          res.send('')
        } else {
          res.send(response);
        }
        
        ackCalled = true;
      }
    };
    await this.bolt.processEvent(event);
  }
}

Adding Custom HTTP routes

Adding custom HTTP routes is quite straightforward when using Bolt’s built-in ExpressReceiver. Since v2.1.0, ExpressReceiver added a router property, which exposes the Express Router on which more routes can be added.

const { App, ExpressReceiver } = require('@slack/bolt');

// Create a Bolt Receiver
const receiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET });

// Create the Bolt App, using the receiver
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver
});

// Slack interactions are methods on app
app.event('message', async ({ event, client }) => {
  // Do some slack-specific stuff here
  await client.chat.postMessage(...);
});

// Other web requests are methods on receiver.router
receiver.router.post('/secret-page', (req, res) => {
  // You're working with an express req and res now.
  res.send('yay!');
});

(async () => {
  await app.start(8080);
  console.log('app is running');
})();

Beta Overview of Workflow Steps for apps

⚠️ Workflow steps from apps is a beta feature. As the feature is developed, Bolt for JavaScript’s API will change to add better native support. To develop with workflow steps in Bolt, use the @slack/bolt@feat-workflow-steps version of the package rather than the standard @slack/bolt.

The API documentation includes more information on setting up a beta app.


Beta Configuring and updating workflow steps

When a builder is adding your step to a new or existing workflow, your app will need to configure and update that step:

1. Listening for workflow_step_edit action

When a builder initially adds (or later edits) your step in their workflow, your app will receive a workflow_step_edit action. Your app can listen to workflow_step_edit using action() and the callback_id in your app’s configuration.

2. Opening and listening to configuration modal

The workflow_step_edit action will contain a trigger_id which your app will use to call views.open to open a modal of type workflow_step. This configuration modal has more restrictions than typical modals—most notably you cannot include title, submit, or close properties in the payload.

To learn more about configuration modals, read the documentation.

Similar to other modals, your app can listen to this view_submission payload with the built-in views() method.

3. Updating the builder’s workflow

After your app listens to the view_submission, you’ll call workflows.updateStep with the unique workflow_step_id (found in the body’s workflow_step object) to save the configuration for that builder’s specific workflow. Two important parameters:

  • inputs is an object with keyed child objects representing the data your app expects to receive from the user upon workflow step execution. You can include handlebar-style syntax ({{ variable }}) for variables that are collected earlier in a workflow.
  • outputs is an array of objects indicating the data your app will provide upon workflow step completion.

Read the documentation for input objects and output objects to learn more about how to structure these parameters.

// Your app will be called when user adds your step to their workflow
app.action({ type: 'workflow_step_edit', callback_id: 'add_task' }, async ({ body, ack, client }) => {
  // Acknowledge the event
  ack();
  // Open the configuration modal using `views.open`
  client.views.open({
    trigger_id: body.trigger_id,
    view: {
      type: 'workflow_step',
      // callback_id to listen to view_submission
      callback_id: 'add_task_config',
      blocks: [
        // Input blocks will allow users to pass variables from a previous step to your's
        { 'type': 'input',
          'block_id': 'task_name_input',
          'element': {
            'type': 'plain_text_input',
            'action_id': 'name',
            'placeholder': {
              'type': 'plain_text',
              'text': 'Add a task name'
            }
          },
          'label': {
            'type': 'plain_text',
            'text': 'Task name'
          }
        },
        { 'type': 'input',
          'block_id': 'task_description_input',
          'element': {
            'type': 'plain_text_input',
            'action_id': 'description',
            'placeholder': {
              'type': 'plain_text',
              'text': 'Add a task description'
            }
          },
          'label': {
            'type': 'plain_text',
            'text': 'Task description'
          }
        }
      ]
    }
  });
});

app.views('add_task_config', async ({ ack, view, body, client }) => {
  // Acknowledge the submission
  ack();
  // Unique workflow edit ID
  let workflowEditId = body.workflow_step.workflow_step_edit_id;
  // Input values found in the view's state object
  let taskName = view.state.values.task_name_input.name;
  let taskDescription = view.state.values.task_description_input.description;

  client.workflows.updateStep({
    workflow_step_edit_id: workflowEditId,
    inputs: {
      taskName: { value: (taskName || '') },
      taskDescription: { value: (taskDescription || '') }
    },
    outputs: [
      {
        name: 'taskName',
        type: 'text',
        label: 'Task name',
      },
      {
        name: 'taskDescription',
        type: 'text',
        label: 'Task description',
      }
    ]
  });

Beta Executing workflow steps

When your workflow is executed by an end user, your app will receive a workflow_step_execute event. This event includes the user’s inputs and a unique workflow execution ID. Your app must either call workflows.stepCompleted with the outputs you specified in workflows.updateStep, or workflows.stepFailed to indicate the step failed.

app.event('workflow_step_execute', async ({ event, client }) => {
  // Unique workflow edit ID
  let workflowExecuteId = event.workflow_step.workflow_step_execute_id;
  let inputs = event.workflow_step.inputs;

  client.workflows.stepCompleted({
    workflow_step_execute_id: workflowExecuteId,
    outputs: {
      taskName: inputs.taskName.value,
      taskDescription: inputs.taskDescription.value
    }
  });
  
  // You can do anything else you want here. Some ideas:
  // Display results on the user's home tab, update your database, or send a message into a channel
});