The new Slack platform is in beta. Your feedback is most welcome.

Block Kit Interactivity

Your Custom functions can be configured to handle Block Kit interactive components—buttons, multi-select fields, text input fields, date & time pickers, etc.

Interactivity in your app flows from a trigger into a workflow. For example, in the case of a Shortcut trigger, the trigger's interactivity input property flows into the underlying workflow's interactivity input property, allowing the workflow to pass contextual information about that interactivity (who interacted, what channel they interacted in, when they interacted, etc) into one or more functions.

To add interactivity to your app:

  1. Create a function that sends interactable Block elements
  2. Create a router to handle the Block element interaction

Create a function that sends interactable elements

These components include an action_id property that you can use to uniquely identify a particular element that can be interacted with. The action_id will be used to route the interactive callback to the correct handler when an interaction happens on that element.

We will go through an example of an Approval Workflow that sends a message with two button options: "Approve" & "Deny". Once 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.

Build a message with buttons

The first thing we'll do is post a message that includes some interactive components. Below is a function that posts a message containing two buttons, "Approve" and "Deny". Notice the action_id and block_id; these fields are important to differentiate between the different interactive components later on in your app.

// /functions/approval_function.ts

import type { SlackFunctionHandler } from "deno-slack-sdk/types.ts";
import { ApprovalFunction } from "../manifest.ts";
import { SlackAPI } from "deno-slack-api/mod.ts";

const approval: SlackFunctionHandler<typeof ApprovalFunction.definition> =
  async ({ inputs, token }) => {
    console.log('Incoming approval!');
    // A Slack API client, so that we can make API calls to Slack
    const client = SlackAPI(token);

    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",
        }],
      }],
    });
    // Important to set completed: false! We will set the function's complete
    // status later - in our action handler
    return {
      completed: false,
    };
  };
export default approval;

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

Create a router to handle the Block element interaction

Now that we have some interactive components to listen for, let's define a BlockActionsRouter. This is a helpful utility to "route" different Block Kit interactions to specific action handlers inside your application.

In the same function source file, and just after our function implementation, we'll define a BlockActionsRouter that will listen for actions on one of the two interactive components (approve_request and deny_request) we attached to the message:

    // /functions/approval_function.ts
    // ... 
export default approval; 

import { BlockActionsRouter } from "deno-slack-sdk/mod.ts";
// We must pass in the function definition for our main function (we imported this in the earlier example code)
// when creating a new `BlockActionsRouter`
const ActionsRouter = BlockActionsRouter(ApprovalFunction);
// Now can use the router's addHandler method to register different handlers based on action properties like
// action_id or block_id
export const blockActions = ActionsRouter.addHandler(
  ['approve_request', 'deny_request'], // The first argument to addHandler can accept an array of action_id strings.
  async ({ action, inputs, token }) => { // The second argument is the handler function itself
    console.log('Incoming action handler invocation', action);
    const client = SlackAPI(token);

    const outputs = {
      reviewer: inputs.user.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",
      message_ts: inputs.message.ts,
    };

    // Remove the button from the original message using the chat.update API
    // and replace its contents with the result of the approval.
    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}>`,
          },
        ],
      }],
    });

    // And now we can mark the function as 'completed' - which is required as
    // we explicitly marked it as incomplete in the main function handler.
    await client.functions.completeSuccess({
      function_execution_id: body.function_data.execution_id,
      outputs,
    });
});

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

We're working to add support for the following known limitations of handling interactivity:

  • Using the response_url from an interactive event doesn't support interactive blocks

  • Sending interactive blocks in an ephemeral message isn't supported

  • Sending interactive blocks in a message sent using chat.scheduleMessage isn't supported

Onward

Hooray! You now have some interactivity weaved within your app. You are on a path to providing a keen user experience within your app. Keep on reading to learn how to trigger your workflows and include even more Built-in Slack functions.

Have 2 minutes to provide some feedback?

We'd love to hear about your experience with the new Slack platform. Please complete our short survey so we can use your feedback to improve.