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, client }) => {
  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.`
    });
    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 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 event 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.

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

app.message('wake me up', async ({ message, client }) => {
  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) {
    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 ({ body, action, ack, context }) => {
    await ack();
    try {
      const result = await app.client.reactions.add({
        token: context.botToken,
        name: 'white_check_mark',
        timestamp: action.ts,
        channel: body.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 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 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, 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({
      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, 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({
        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, client }) => {
  // 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'
        }
      }
    });
    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, client }) => {
  // Acknowledge the button request
  await ack();

  try {
    // 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'
          }
        ]
      }
    });
    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);
  }

});

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.

// Listen for users opening your App Home
app.event('app_home_opened', async ({ event, client }) => {
  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*>."
            }
          }
        ]
      }
    });

    console.log(result);
  }
  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;
        }
      },
  }
});

Overview of Workflow Steps for apps

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.

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.

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.

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.

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,
    };

    // if everything was successful
    await complete({ outputs });

    // if something went wrong
    // fail({ error: { message: "Just testing step failure!" } });
  },
});

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. 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 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');
})();