Block Kit Interactivity

Both Slack functions and custom functions can be configured to handle Block Kit interactive components.

Block Kit interactive components are a subset of Block Kit elements, which can include buttons, checkboxes, text input fields, date and time pickers, radio buttons, images, and more. Each of these Block Kit elements can be used inside a layout block, which is used to create the layout of your app.

To learn more about Block Kit, refer to Building with Block Kit and Interactivity in Block Kit.

➡️ To add Block Kit interactivity to your app, read on!

Create a function that sends interactable Block Kit components

Let's say we want an approval workflow where one step is sending a message with two button options: "Approve" and "Deny". When someone clicks either button, our app will handle these button interactions (which are composed in Block Kit Actions) and update the original message with either an "Approved!" or "Denied!" text response.

Step 1. Define the function

First, we'll create a function definition defining the inputs that will appear in the message, and the outputs from the approver's interaction with the message:

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";

export const ApprovalFunction = DefineFunction({
  callback_id: "approval",
  title: "Approval",
  description: "Get approval for a request",
  source_file: "functions/approval.ts",
  input_parameters: {
    properties: {
      dtest: {
        type: "slack#/types/date",
      },
      requester_id: {
        type: Schema.slack.types.user_id,
        description: "Requester",
      },
      approval_channel_id: {
        type: Schema.slack.types.channel_id,
        description: "Approval channel",
      },
      subject: {
        type: Schema.types.string,
        description: "Subject",
      },
      details: {
        type: Schema.types.string,
        description: "Details Updated",
      },
    },
    required: [
      "requester_id",
      "approval_channel_id",
      "subject",
      "details",
    ],
  },
  output_parameters: {
    properties: {
      approved: {
        type: Schema.types.boolean,
        description: "Approved",
      },
      comments: {
        type: Schema.types.string,
        description: "Comments",
      },
      reviewer: {
        type: Schema.slack.types.user_id,
        description: "Reviewer",
      },
    },
    required: ["approved", "reviewer"],
  },
});
  // To be continued in the next step ...

Step 2. Call the Slack client in the function handler

We can then use the provided Slack client in the function handler to call the chat.postMessage method directly to post our message. The message will contain two buttons the user can interact with: one for "Approve" and one for "Deny".

There are two Block Kit interactive components that your Block Kit element will use for interactivity with other aspects of your workflow:

  • The action_id property. This uniquely identifies a particular interactive component. This will be used to route the interactive callback to the correct handler when an interaction happens on that element.
  • The block_id property. This uniquely identifies the entire Block Kit element.

The response_url property is no longer included in view_submission payloads or block_actions payloads that are routed to function handlers — the Slack API client provided to these handlers can call Slack’s messaging APIs directly.

Below is the part of our function that posts a message containing our two buttons, "Approve" and "Deny".

// ... continued from the step above
// Slack client in the function handler
export default SlackFunction(ApprovalFunction, async ({ inputs, client }) => {
  console.log("Incoming approval!");
  // Call the messaging API
  const response = await client.chat.postMessage({
    channel: inputs.approval_channel_id,
    blocks: [{
      "type": "actions",
      "block_id": "my-buttons",
      "elements": [
        {
          type: "button",
          text: { type: "plain_text", text: "Approve" },
          action_id: "approve_request",
          style: "primary",
        },
        {
          type: "button",
          text: { type: "plain_text", text: "Deny" },
          action_id: "deny_request",
          style: "danger",
        },
      ],
    }],
  });
  if (response.error) {
    const error = `Failed to post a message with buttons! - ${response.error}`;
    return { error };
  }
  // Important to set 'completed' to false. We'll update the status later 
  // in our action handler
  return { completed: false };

  // To be continued in the next step ...

We return completed: false here to ensure the function execution does not complete until the interactivity is complete. The function execution will be completed later in the action handler.

Step 3. Add a handler to respond to Block Kit element interactions

Now that we have some interactive components to listen for, let's define a handler to react to interactions with these components.

In the same function source file (and "chaining" off our function implementation), we'll define a handler that will listen for actions performed on one of the two interactive components (approve_request and deny_request) that we'll attach to the message using the addBlockActionsHandler() helper method.

// ... continued from the step above
}).addBlockActionsHandler(
  ["approve_request", "deny_request"], // First arg can accept an array of action_id strings
  async ({ body, action, inputs, client }) => { // Second arg is the handler function itself
    console.log("Incoming action handler invocation", action);
    const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
    console.log("waiting");
    await sleep(3000);
    console.log("done waiting");
    const outputs = {
      reviewer: inputs.requester_id,
      // Based on which button was pressed (determined via action_id), we can
      // determine whether the request was approved or not
      approved: action.action_id === "approve_request",
    };

    // Remove the button from the original message using the chat.update API,
    // and replace its contents with the result of the approval
    const messageUpdate = await client.chat.update({
       channel: inputs.approval_channel_id,
       ts: outputs.message_ts,
       blocks: [{
         type: "context",
         elements: [
           {
             type: "mrkdwn",
             text: `${
               outputs.approved ? " :white_check_mark: Approved" : ":x: Denied"
             } by <@${outputs.reviewer}>`,
           },
         ],
       }],
     });
     if (messageUpdate.error) {
       const error = `Failed to update the message - ${messageUpdate.error}`;
       return { error };
     }

    // Now, we can mark the function as 'completed' - which is required as
    // we explicitly marked it as incomplete in the main function handler earlier
    const completion = await client.functions.completeSuccess({
      function_execution_id: body.function_data.execution_id,
      outputs,
    });
    if (completion.error) {
      const error = `Failed to complete a function - ${completion.error}`;
      return { error };
    }
  },
);

Remember to can mark the function as completed. This is required since we explicitly marked it as incomplete in the main function handler previously.

Input validation

It's important to validate the input data you receive from the user.

  1. First, validate that the user is authorized to pass the input.
  2. Second, validate that the user is passing a value you expect to receive, and nothing more.

Onward

Now you have some interactivity weaved within your app, hooray!

💻 For an expanded version of the sample code provided above, check out our Request Time Off sample app.

To learn more about leveraging built-in powers or defining your own, check out Slack functions and custom functions.

Have 2 minutes to provide some feedback?

We'd love to hear about your experience building modular Slack apps. Please complete our short survey so we can use your feedback to improve.