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.

1
2
3
4
5
6
7
8
9
10
// This will match any message that contains 👋
app.message(':wave:', async ({ message, say }) => {
  // Handle only newly posted messages here
  if (message.subtype === undefined
    || message.subtype === 'bot_message'
    || message.subtype === 'file_share'
    || message.subtype === 'thread_broadcast') {
    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.

1
2
3
4
5
6
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.

1
2
3
4
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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, client, logger }) => {
  try {
    // Call chat.postMessage with the built-in client
    const result = await client.chat.postMessage({
      channel: welcomeChannelId,
      text: `Welcome to the team, <@${event.user.id}>! 🎉 You can introduce yourself in this channel.`
    });
    logger.info(result);
  }
  catch (error) {
    logger.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 subtype() middleware. Common message subtypes like message_changed and message_replied can be found on the message event page.

1
2
3
4
5
6
7
8
9
10
11
12
// Import subtype from the package
const { App, subtype } = require('@slack/bolt');

// Matches all message changes from users
app.message(subtype('message_changed'), ({ event, logger }) => {
  // This if statement is required in TypeScript code
  if (event.subtype === 'message_changed'
    && !event.message.subtype
    && !event.previous_message.subtype) {
    logger.info(`The user ${event.message.user} changed their message from ${event.previous_message.text} to ${event.message.text}`);
  }
});

Using the Web API

You can call any Web API method using the WebClient provided to your app’s listeners as client. This uses either the token that initialized your app or the token that is returned from the authorize function for the incoming event. The built-in OAuth support handles the second case by default.

Your Bolt app also has a top-level app.client which you can manually pass the token parameter. If the incoming request is not authorized or you’re calling a method from outside of a listener, use the top-level app.client.

Calling one of the WebClient’s methods will return a Promise containing the response from Slack, regardless of whether you use the top-level or listener’s client.

Since the introduction of org wide app installations, some web-api methods now require team_id to indicate which workspace to act on. Bolt for JavaScript will attempt to infer the team_id based on incoming payloads and pass it along to client. This is handy for existing applications looking to add support for org wide installations and not spend time updating all of these web-api calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Unix Epoch time for September 30, 2019 11:59:59 PM
const whenSeptemberEnds = 1569887999;

app.message('wake me up', async ({ message, client, logger }) => {
  try {
    // Call chat.scheduleMessage with the built-in client
    const result = await client.chat.scheduleMessage({
      channel: message.channel,
      post_at: whenSeptemberEnds,
      text: 'Summer has come and passed'
    });
  }
  catch (error) {
    logger.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 request was received from Slack. This is discussed in the acknowledging requests 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.

Learn more about the block_actions payload, here. To access the full payload of a view from within a listener, reference the body argument within your callback function.

1
2
3
4
5
// Your listener function will be called every time an interactive component with the action_id "approve_button" is triggered
app.action('approve_button', async ({ ack }) => {
  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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Your listener function will only be called when the action_id matches 'select_user' AND the block_id matches 'assign_ticket'
app.action({ action_id: 'select_user', block_id: 'assign_ticket' },
  async ({ body, client, ack, logger }) => {
    await ack();
    try {
      // Make sure the action isn't from a view (modal or app home)
      if (body.message) {
        const result = await client.reactions.add({
          name: 'white_check_mark',
          timestamp: body.message.ts,
          channel: body.channel.id
        });

        logger.info(result);
      }
    }
    catch (error) {
      logger.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 request 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.

1
2
3
4
5
6
// 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.

1
2
3
4
5
6
7
// Listens to actions triggered with action_id of “user_select”
app.action('user_select', async ({ action, ack, respond }) => {
  await ack();
  if (action.type === 'users_select') {
    await respond(`You selected <@${action.selected_user}>`);
  }
});

Acknowledging requests

Actions, commands, and options requests must always be acknowledged using the ack() function. This lets Slack know that the request was received and updates the Slack user interface accordingly. Depending on the type of request, your acknowledgement may be different. For example, when acknowledging a modal 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Regex to determine if this is a valid email
let isEmail = /^[\w\-\.]+@([\w\-]+\.)+[\w\-]+$/;
// This uses a constraint object to listen for modal submissions with a callback_id of ticket_submit 
app.view('ticket_submit', async ({ ack, view }) => {
  // get the email value from the input block with `email_address` as the block_id
  const email = view.state.values['email_address']['input_a'].value;

  // if it’s a valid email, accept the submission
  if (email && isEmail.test(email)) {
    await ack();
  } else {
    // if it isn’t a valid email, acknowledge with an error
    await ack({
      "response_action": "errors",
      errors: {
        "email_address": "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 entry points to apps. Global shortcuts are available from within search in Slack. Message shortcuts are available in the context menus of messages. Your app can use the shortcut() method to listen to incoming shortcut requests. The method requires a callback_id parameter of type string or RegExp.

⚠️ Note that if you use shortcut() multiple times with overlapping RegExp matches, all matching listeners will run. Design your regular expressions to avoid this possibility.

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

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// The open_modal shortcut opens a plain old modal
app.shortcut('open_modal', async ({ shortcut, ack, client, logger }) => {

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

    // Call the views.open method using one of the built-in WebClients
    const result = await client.views.open({
      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*>"
              }
            ]
          }
        ]
      }
    });

    logger.info(result);
  }
  catch (error) {
    logger.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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  // 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, client, logger }) => {
    try {
      // Acknowledge shortcut request
      await ack();

      // Call the views.open method using one of the built-in WebClients
      const result = await client.views.open({
        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*>"
                }
              ]
            }
          ]
        }
      });

      logger.info(result);
    }
    catch (error) {
      logger.error(error);
    }
  });

Listening and responding to commands

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

⚠️ Note that if you use command() multiple times with overlapping RegExp matches, all matching listeners will run. Design your regular expressions to avoid this possibility.

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

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.

1
2
3
4
5
6
7
// The echo command simply echoes on command
app.command('/echo', async ({ command, ack, respond }) => {
  // Acknowledge command request
  await ack();

  await respond(`${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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Listen for a slash command invocation
app.command('/ticket', async ({ ack, body, client, logger }) => {
  // Acknowledge the command request
  await ack();

  try {
    // Call views.open with the built-in client
    const result = await client.views.open({
      // 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'
        }
      }
    });
    logger.info(result);
  }
  catch (error) {
    logger.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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Listen for a button invocation with action_id `button_abc` (assume it's inside of a modal)
app.action('button_abc', async ({ ack, body, client, logger }) => {
  // Acknowledge the button request
  await ack();

  try {
    if (body.type !== 'block_actions' || !body.view) {
      return;
    }
    // Call views.update with the built-in client
    const result = await client.views.update({
      // Pass the view_id
      view_id: body.view.id,
      // Pass the current hash to avoid race conditions
      hash: body.view.hash,
      // 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'
          }
        ]
      }
    });
    logger.info(result);
  }
  catch (error) {
    logger.error(error);
  }
});

Listening to views

You may listen for user interactions with views using the view method.

Slack will send a view_submission request when a user submits a view. To receive the values submitted in view input blocks, you can access the state object. state contains a values object that uses the block_id and unique action_id to store the input values. If the notify_on_close field of a view has been set to true, Slack will also send a view_closed request if a user clicks the close button. See the section on Handling views on close for more detail. To listen to either a view_submission request or view_closed request, you can use the built-in view() method.

view() requires a callback_id of type string or RegExp or a constraint object with properties type and callback_id.


Update views on submission

To update a view in response to a view_submission request, you may pass a response_action of type update with a newly composed view to display in your acknowledgement.

1
2
3
4
5
6
7
// Update the view on submission 
app.view('modal-callback-id', async ({ ack, body }) => {
  await ack({
    response_action: 'update',
    view: buildNewModalView(body),
  });
});

Similarly, there are options for displaying errors in response to view submissions.

Read more about view submissions in our API documentation.


Handling views on close

💡 When listening for view_closed requests, you must pass an object containing type: 'view_closed' and the view callback_id. See below for an example of this:

See the API documentation for more information about view_closed.

1
2
3
4
5
6
// Handle a view_closed request
app.view({ callback_id: 'view_b', type: 'view_closed' }, async ({ ack, body, view, client }) => {
  // Acknowledge the view_closed request
  await ack();
  // react on close request
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Handle a view_submission request
app.view('view_b', async ({ ack, body, view, client, logger }) => {
  // Acknowledge the view_submission request
  await ack();

  // Do whatever you want with the input data - here we're saving it to a DB then sending the user a verification 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 client.chat.postMessage({
      channel: user,
      text: msg
    });
  }
  catch (error) {
    logger.error(error);
  }

});

Publishing views to App Home

Home tabs are customizable surfaces accessible via the sidebar and search that allow apps to display views on a per-user basis. After enabling App Home within your app configuration, home tabs can be published and updated by passing a user_id and view payload to the views.publish method.

You can subscribe to the app_home_opened event to listen for when users open your App Home.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Listen for users opening your App Home
app.event('app_home_opened', async ({ event, client, logger }) => {
  try {
    // Call views.publish with the built-in client
    const result = await client.views.publish({
      // Use the user ID associated with the event
      user_id: event.user,
      view: {
        // Home tabs must be enabled in your app configuration page under "App Home"
        type: "home",
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "*Welcome home, <@" + event.user + "> :house:*"
            }
          },
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "Learn how home tabs can be more useful and interactive <https://api.slack.com/surfaces/tabs/using|*in the documentation*>."
            }
          }
        ]
      }
    });

    logger.info(result);
  }
  catch (error) {
    logger.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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 of results) {
      options.push({
        text: {
          type: "plain_text",
          text: result.label
        },
        value: result.value
      });
    }

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

Authenticating with OAuth

To prepare your Slack app for distribution, you will need to enable Bolt OAuth and store installation information securely. Bolt supports OAuth and will handle the rest of the work; this includes setting up OAuth routes, state verification, and passing your app an installation object which you must store.

To enable OAuth, you must provide:

  • clientId, clientSecret, stateSecret and scopes (required)
  • An installationStore option with handlers that store and fetch installations to your database (optional, strongly recommended in production)

Development and Testing

We’ve provided a default implementation of the installationStore FileInstallationStore which you can use during app development and testing.

1
2
3
4
5
6
7
8
9
10
const { App } = require('@slack/bolt');
const { FileInstallationStore } = require('@slack/oauth');
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:history', 'chat:write', 'commands'],
  installationStore: new FileInstallationStore(),
});

:warning: This is not recommended for use in production - you should implement your own production store. Please see the example code to the right and our other examples.


Installing your App
  • Initiating an installation: Bolt for JavaScript provides an Install Path /slack/install out-of-the-box. This endpoint returns a simple page with an Add to Slack button which initiates a direct install of your app (with a valid state parameter). An app hosted at www.example.com would serve the install page at www.example.com/slack/install.
    • 💡 You can skip rendering the provided default webpage and navigate users directly to Slack authorize URL by settinginstallerOptions.directInstall: true in the App constructor (example).
  • Add to Slack: The Add to Slack button initiates the OAuth process with Slack. After users have clicked Allow to grant your app permissions, Slack will call your app’s Redirect URI (provided out-of-the-box), and prompt users to Open Slack. See the Redirect URI section below for customization options.

  • Open Slack: After users Open Slack, and here after as your app processes events from Slack, your provided installationStore’s fetchInstallation and storeInstallation handlers will execute. See the Installation Object section below for more detail on arguments passed to those handlers.

  • If you need additional authorizations (user tokens) from users inside a team when your app is already installed, or have 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.

  • 💡 Bolt for JavaScript does not support OAuth for custom receivers. If you’re implementing a custom receiver, you can use our Slack OAuth library, which is what Bolt for JavaScript uses under the hood.

Redirect URI

Bolt for JavaScript provides a Redirect URI Path /slack/oauth_redirect. Slack uses the Redirect URI to redirect users after they complete an app’s installation flow.

💡 You will need to add the full Redirect URI including your app domain in your Slack app configuration settings under OAuth and Permissions, e.g. https://example.com/slack/oauth_redirect.

To supply your own custom Redirect URI, you can set redirectUri in the App options and installerOptions.redirectUriPath. You must supply both, and the path must be consistent with the full URI.

1
2
3
4
5
6
7
8
9
10
11
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: ['chat:write'],
  redirectUri: 'https://example.com/slack/redirect', // here
  installerOptions: {
    redirectUriPath: '/slack/redirect', // and here!
  },
});

Installation object

Bolt will pass your installationStore’s storeInstallation handler an installation. This can be a source of confusion for developers who aren’t sure what shape of object to expect. The installation object should resemble:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  team: { id: 'T012345678', name: 'example-team-name' },
  enterprise: undefined,
  user: { token: undefined, scopes: undefined, id: 'U01234567' },
  tokenType: 'bot',
  isEnterpriseInstall: false,
  appId: 'A01234567',
  authVersion: 'v2',
  bot: {
    scopes: [
      'chat:write',
    ],
    token: 'xoxb-244493-28*********-********************',
    userId: 'U012345678',
    id: 'B01234567'
  }
}

Bolt will pass your fetchInstallation and deleteInstallation handlers an installQuery object:

1
2
3
4
5
6
7
{
  userId: 'U012345678',
  isEnterpriseInstall: false,
  teamId: 'T012345678',
  enterpriseId: undefined,
  conversationId: 'D02345678'
}

Org-wide installation

To add support for org-wide installations, you will need Bolt for JavaScript version 3.0.0 or later. Make sure you have enabled org-wide installation in your app configuration settings under Org Level Apps.

Installing an org-wide app from admin pages requires additional configuration to work with Bolt. In that scenario, the recommended state parameter is not supplied. Bolt will try to verify state and stop the installation from progressing.

You may disable state verification in Bolt by setting the stateVerification option to false. See the example setup below:

1
2
3
4
5
6
7
8
9
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  scopes: ['chat:write'],
  installerOptions: {
    stateVerification: false,
  },
});

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const database = {
  async get(key) {},
  async delete(key) {},
  async set(key, value) {}
};

const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  stateSecret: 'my-secret',
  scopes: ['chat:write', 'commands'],
  installationStore: {
    storeInstallation: async (installation) => {
      // Bolt will pass your handler an installation object
      // Change the lines below so they save to your database
      if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
        // handle storing org-wide app installation
        return await database.set(installation.enterprise.id, installation);
      }
      if (installation.team !== undefined) {
        // single team app installation
        return await database.set(installation.team.id, installation);
      }
      throw new Error('Failed saving installation data to installationStore');
    },
    fetchInstallation: async (installQuery) => {
      // Bolt will pass your handler an installQuery object
      // Change the lines below so they fetch from your database
      if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
        // handle org wide app installation lookup
        return await database.get(installQuery.enterpriseId);
      }
      if (installQuery.teamId !== undefined) {
        // single team app installation lookup
        return await database.get(installQuery.teamId);
      }
      throw new Error('Failed fetching installation');
    },
    deleteInstallation: async (installQuery) => {
      // Bolt will pass your handler  an installQuery object
      // Change the lines below so they delete from your database
      if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
        // org wide app installation deletion
        return await database.delete(installQuery.enterpriseId);
      }
      if (installQuery.teamId !== undefined) {
        // single team app installation deletion
        return await database.delete(installQuery.teamId);
      }
      throw new Error('Failed to delete installation');
    },
  },
});

Customizing OAuth defaults

We provide several options for customizing 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: This relative path must match the redirectUri provided in the App options
  • 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
  • userScopes: Array of user scopes needed when the user installs the app, similar to scopes attribute at the parent level.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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',
      userScopes: ['chat:write'],
      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;
        }
      },
  }
});

Using Socket Mode

Socket Mode allows your app to connect and receive data from Slack via a WebSocket connection. To handle the connection, Bolt for JavaScript includes a SocketModeReceiver (in @slack/bolt@3.0.0 and higher). Before using Socket Mode, be sure to enable it within your app configuration.

To use the SocketModeReceiver, just pass in socketMode:true and appToken:YOUR_APP_TOKEN when initializing App. You can get your App Level Token in your app configuration under the Basic Information section.

1
2
3
4
5
6
7
8
9
10
11
12
const { App } = require('@slack/bolt');

const app = new App({
  token: process.env.BOT_TOKEN,
  socketMode: true,
  appToken: process.env.APP_TOKEN,
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();

Custom SocketMode Receiver

You can define a custom SocketModeReceiver by importing it from @slack/bolt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { App, SocketModeReceiver } = require('@slack/bolt');

const socketModeReceiver = new SocketModeReceiver({
  appToken: process.env.APP_TOKEN,

  // enable the following if you want to use OAuth
  // clientId: process.env.CLIENT_ID,
  // clientSecret: process.env.CLIENT_SECRET,
  // stateSecret: 'my-state-secret',
  // scopes: ['channels:read', 'chat:write', 'app_mentions:read', 'channels:manage', 'commands'],
});

const app = new App({
  receiver: socketModeReceiver,
  // disable token line below if using OAuth
  token: process.env.BOT_TOKEN
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();

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.

You can also define more focussed and specific error handlers for a variety of error paths directly on the HTTPReceiver:

  • dispatchErrorHandler: triggered if an incoming request is to an unexpected path.
  • processEventErrorHandler: triggered when processing a request (i.e. middleware, authorization) throws an exception.
  • unhandledRequestHandler: triggered when a request from Slack goes unacknowledged.
  • unhandledRequestTimeoutMillis: the amount of time in milliseconds to wait for request acknowledgement from the application before triggering the unhandledRequestHandler. Default is 3001.

NOTE: It is imperative that any custom Error Handlers defined in your app respond to the underlying Slack request that led to the error, using response.writeHead() to set the HTTP status code of the response and response.end() to dispatch the response back to Slack. See the example for details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { App, HTTPReceiver } from '@slack/bolt';

const app = new App({
  receiver: new HTTPReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
    // more specific, focussed error handlers
    dispatchErrorHandler: async ({ error, logger, response }) => {
      logger.error(`dispatch error: ${error}`);
      response.writeHead(404);
      response.write("Something is wrong!");
      response.end();
    },
    processEventErrorHandler: async ({ error, logger, response }) => {
      logger.error(`processEvent error: ${error}`);
      // acknowledge it anyway!
      response.writeHead(200);
      response.end();
      return true;
    },
    unhandledRequestHandler: async ({ logger, response }) => {
      logger.info('Acknowledging this incoming request because 2 seconds already passed...');
      // acknowledge it anyway!
      response.writeHead(200);
      response.end();
    },
    unhandledRequestTimeoutMillis: 2000, // the default is 3001
  }),
});

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

Accessing more data in the error handler

There may be cases where you need to log additional data from a request in the global error handler. Or you may simply wish to have access to the logger you’ve passed into Bolt.

Starting with version 3.8.0, when passing extendedErrorHandler: true to the constructor, the error handler will receive an object with error, logger, context, and the body of the request.

It is recommended to check whether a property exists on the context or body objects before accessing its value, as the data available in the body object differs from event to event, and because errors can happen at any point in a request’s lifecycle (i.e. before a certain property of context has been set).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { App } = require('@slack/bolt');

const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
  extendedErrorHandler: true,
});

app.error(async ({ error, logger, context, body }) => {
  // Log the error using the logger passed into Bolt
  logger.error(error);

  if (context.teamId) {
    // Do something with the team's ID for debugging purposes
  }
});

Authorization

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

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. If you’re using the built-in OAuth support authorization is handled by default, so you do not need to pass in an authorize option.

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 request 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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');
}

Token rotation

Supported in Bolt for JavaScript as of v3.5.0, token rotation provides an extra layer of security for your access tokens and is defined by the OAuth V2 RFC.

Instead of an access token representing an existing installation of your Slack app indefinitely, with token rotation enabled, access tokens expire. A refresh token acts as a long-lived way to refresh your access tokens.

Bolt for JavaScript supports and will handle token rotation automatically so long as the built-in OAuth functionality is used.

For more information about token rotation, please see the documentation.


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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 requests 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Authentication middleware that associates incoming request with user in Acme identity provider
async function authWithAcme({ payload, client, 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 request
    if (error.message === 'Not Found') {
        await client.chat.postEphemeral({
          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 at the start of a message.

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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, logger }) => logger.info(
  // Handle only newly posted messages
  if (message.subtype === undefined
    // || message.subtype === 'bot_message'
    || message.subtype === 'file_share'
    || message.subtype === 'thread_broadcast') {
    logger.info(`(MSG) User: ${message.user} Message: ${message.text}`)
  }
));

Adding context

All listeners have access to a context object, which can be used to enrich requests 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
async function addTimezoneContext({ payload, client, context, next }) {
  const user = await client.users.info({
    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, client, context, logger }) => {
  // Acknowledge command request
  await ack();
  // Get local hour of request
  const localHour = (Date.UTC(2020, 3, 31) + 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 (localHour > 17 || localHour < 9) {
    // Assume function exists to get local tomorrow 9AM from offset
    const localTomorrow = getLocalTomorrow(context.tz_offset);

    try {
      // Schedule message
      const result = await client.chat.scheduleMessage({
        channel: requestChannel,
        text: requestText,
        post_at: localTomorrow
      });
    }
    catch (error) {
      logger.error(error);
    }
  } else {
    try {
      // Post now
      const result = await client.chat.postMessage({
        channel: requestChannel,
        text: requestText
      });
    } catch (error) {
      logger.error(error);
    }
  }
});

Deferring App initialization

Bolt offers a way to defer full initialization via the deferInitialization option and to call the equivalent App#init() in your code, putting more control over asynchronous execution required for initialization into your hands as the developer.

Note: If you call start() before init(), Bolt will raise an exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { App } = require('@slack/bolt');

// deferInitialization is one of the options you can set in the constructor
const app = new App({
  token,
  signingSecret,
  deferInitialization: true,
});

(async () => {
  try {
    // Must call init() before start() within an async function
    await app.init();
    // Now safe to call start()
    await app.start(process.env.PORT || 3000);
  } catch (e) {
    console.log(e);
    process.exit(1);
  }
})()

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.

1
2
3
4
5
6
7
8
9
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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: (level) => { },
    getLevel: () => { },
    setName: (name) => { },
  },
});

Customizing a receiver

Writing a custom receiver

A receiver is responsible for handling and parsing any incoming requests from Slack then sending it to the app, so that the app can add context and pass the request 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 a request 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


Customizing built-in receivers

The built-in HTTPReceiver, ExpressReceiver, AwsLambdaReceiver and SocketModeReceiver accept several configuration options. For a full list of options, see the Receiver options reference.

Extracting custom properties

Use the customPropertiesExtractor option to extract custom properties from incoming events. The event type depends on the type of receiver you are using, e.g. HTTP requests for HTTPReceivers, websocket messages for SocketModeReceivers.

This is particularly useful for extracting HTTP headers that you want to propagate to other services, for example, if you need to propagate a header for distributed tracing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { App, HTTPReceiver } = require('@slack/bolt');

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: new HTTPReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
    customPropertiesExtractor: (req) => {
      return {
        "headers": req.headers,
        "foo": "bar",
      };
    }
  }),
});

app.use(async ({ logger, context, next }) => {
  logger.info(context);
  await next();
});

(async () => {
  // Start your app
  await app.start(process.env.PORT || 3000);

  console.log('⚡️ Bolt app is running!');
})();

You can find more examples of extracting custom properties from different types of receivers here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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

As of v3.7.0, custom HTTP routes can be easily added by passing in an array of routes as customRoutes when initializing App.

Each CustomRoute object must contain three properties: path, method, and handler. method, which corresponds to the HTTP verb, can be either a string or an array of strings.

Since v3.13.0, the default built-in receivers (HTTPReceiver and SocketModeReceiver) support dynamic route parameters like Express.js does. With this, you can capture positional values in the URL for use in your route’s handler via req.params.

To determine what port the custom HTTP route will be available on locally, you can specify an installerOptions.port property in the App constructor. Otherwise, it will default to port 3000.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { App } = require('@slack/bolt');

// Initialize Bolt app, using the default HTTPReceiver
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  customRoutes: [
    {
      path: '/health-check',
      method: ['GET'],
      handler: (req, res) => {
        res.writeHead(200);
        res.end(`Things are going just fine at ${req.headers.host}!`);
      },
    },
    {
      path: '/music/:genre',
      method: ['GET'],
      handler: (req, res) => {
        res.writeHead(200);
        res.end(`Oh? ${req.params.genre}? That slaps!`);
      },
    },
  ],
  installerOptions: {
    port: 3001,
  },
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();

Custom ExpressReceiver 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 additional routes and middleware can be added.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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(...);
});

// Middleware methods execute on every web request
receiver.router.use((req, res, next) => {
  console.log(`Request time: ${Date.now()}`);
  next();
});

// 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();
  console.log('⚡️ Bolt app started');
})();

Overview of Workflow Steps for apps

⚠️ Workflow Steps from Apps are a legacy feature, not to be confused with workflows that are part of the next generation Slack platform. They are not interchangeable features. We encourage those who are currently publishing Workflow Steps from apps to consider the new automation features.

Workflow Steps from apps allow your app to create and process custom workflow steps that users can add using Workflow Builder.

A workflow step is made up of three distinct user events:

  • Adding or editing the step in a Workflow
  • Saving or updating the step’s configuration
  • The end user’s execution of the step

All three events must be handled for a workflow step to function.

Read more about workflow steps from apps in the API documentation.


Creating workflow steps

To create a workflow step, Bolt provides the WorkflowStep class.

When instantiating a new WorkflowStep, pass in the step’s callback_id and a configuration object.

The configuration object contains three properties: edit, save, and execute. Each of these properties must be a single callback or an array of callbacks. All callbacks have access to a step object that contains information about the workflow step event.

After instantiating a WorkflowStep, you can pass it into app.step(). Behind the scenes, your app will listen and respond to the workflow step’s events using the callbacks provided in the configuration object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { App, WorkflowStep } = require('@slack/bolt');

// Initiate the Bolt app as you normally would
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
});

// Create a new WorkflowStep instance
const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {},
  save: async ({ ack, step, update }) => {},
  execute: async ({ step, complete, fail }) => {},
});

app.step(ws);

Adding or editing workflow steps

When a builder adds (or later edits) your step in their workflow, your app will receive a workflow_step_edit event. The edit callback in your WorkflowStep configuration will be run when this event is received.

Whether a builder is adding or editing a step, you need to send them a workflow step configuration modal. This modal is where step-specific settings are chosen, and it has more restrictions than typical modals—most notably, it cannot include title​, submit​, or close​ properties. By default, the configuration modal’s callback_id will be the same as the workflow step.

Within the edit callback, the configure() utility can be used to easily open your step’s configuration modal by passing in an object with your view’s blocks. To disable saving the configuration before certain conditions are met, pass in submit_disabled with a value of true.

To learn more about opening configuration modals, read the documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {
    await ack();

    const blocks = [
      {
        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',
        },
      },
    ];

    await configure({ blocks });
  },
  save: async ({ ack, step, update }) => {},
  execute: async ({ step, complete, fail }) => {},
});

Saving step configurations

After the configuration modal is opened, your app will listen for the view_submission event. The save callback in your WorkflowStep configuration will be run when this event is received.

Within the save callback, the update() method can be used to save the builder’s step configuration by passing in the following arguments:

  • inputs is an object representing the data your app expects to receive from the user upon workflow step execution.
  • outputs is an array of objects containing data that your app will provide upon the workflow step’s completion. Outputs can then be used in subsequent steps of the workflow.
  • step_name overrides the default Step name
  • step_image_url overrides the default Step image

To learn more about how to structure these parameters, read the documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {},
  save: async ({ ack, step, view, update }) => {
    await ack();

    const { values } = view.state;
    const taskName = values.task_name_input.name;
    const taskDescription = values.task_description_input.description;
                
    const inputs = {
      taskName: { value: taskName.value },
      taskDescription: { value: taskDescription.value }
    };

    const outputs = [
      {
        type: 'text',
        name: 'taskName',
        label: 'Task name',
      },
      {
        type: 'text',
        name: 'taskDescription',
        label: 'Task description',
      }
    ];

    await update({ inputs, outputs });
  },
  execute: async ({ step, complete, fail }) => {},
});

Executing workflow steps

When your workflow step is executed by an end user, your app will receive a workflow_step_execute event. The execute callback in your WorkflowStep configuration will be run when this event is received.

Using the inputs from the save callback, this is where you can make third-party API calls, save information to a database, update the user’s Home tab, or decide the outputs that will be available to subsequent workflow steps by mapping values to the outputs object.

Within the execute callback, your app must either call complete() to indicate that the step’s execution was successful, or fail() to indicate that the step’s execution failed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {},
  save: async ({ ack, step, update }) => {},
  execute: async ({ step, complete, fail }) => {
    const { inputs } = step;

    const outputs = {
      taskName: inputs.taskName.value,
      taskDescription: inputs.taskDescription.value,
    };

    // signal back to Slack that everything was successful
    await complete({ outputs });
    // NOTE: If you run your app with processBeforeResponse: true option,
    // `await complete()` is not recommended because of the slow response of the API endpoint
    // which could result in not responding to the Slack Events API within the required 3 seconds
    // instead, use:
    // complete({ outputs }).then(() => { console.log('workflow step execution complete registered'); });

    // let Slack know if something went wrong
    // await fail({ error: { message: "Just testing step failure!" } });
    // NOTE: If you run your app with processBeforeResponse: true, use this instead:
    // fail({ error: { message: "Just testing step failure!" } }).then(() => { console.log('workflow step execution failure registered'); });
  },
});