Building approval workflows using Typescript and Deno

Beginner

This track will guide you on how to create, run and deploy an app using the next generation Slack platform. The Request Time Off App models how to collect user inputs and send those inputs to other users in Slack. More specifically this app showcases one way user interactivity is implemented within a Slack CLI app. By the end you will have a working app that can post Block Kit messages, handle user interactions, and even update messages in real time.

We can break this app into 3 major parts that work together to create a symphonic harmony.

  1. Functions
  2. Workflows
  3. Triggers

Each segment will give an explanation of the components along with some tips & tricks for executing a successful path forward.

Step 1Complete the Prerequisites

Every Slack CLI app journey begins with a few common steps. Complete the prerequisites below to get started on your trek up the CLI mountain.

  • Prework for the future platform.

    Before we create a harmonious collection we must warm up the instruments, in this case your local machine and terminal. Follow along with the pre-work steps to set up your machine and Slack workspace. From there you'll have the pieces to follow along.

    After you've installed the command-line interface you have two ways to dive in.

    Create a super fresh project

    You can create a fresh new project:

    slack create deno-approval-app
    

    But you'll still be presented with the paradox of choice: a totally clean project or something with some suggested structure to it. We'll walk you through the suggested structure regardless so it's best to go ahead and choose the blank project when following along directly.

    Or, use a template instead

    Of course, you could just jump right to the sample project on GitHub if you want. You can even just jumpstart your project using the CLI and skip all this copy and pasting.

    slack create --template https://github.com/slack-samples/deno-request-time-off
    
Step complete!

Step 2Explore the Manifest

At the root of every Slack CLI app there exists an app manifest that configures how an app presents itself.

  • Explore the App Manifest

    The App Manifest is where we define the meat and potatoes of an app. Below is the manifest that powers the Request Time Off app:

    import { Manifest } from "deno-slack-sdk/mod.ts";
    import { CreateTimeOffRequestWorkflow } from "./workflows/CreateTimeOffRequestWorkflow.ts";
    import { SendTimeOffRequestToManagerFunction } from "./functions/send_time_off_request_to_manager/definition.ts";
    
    export default Manifest({
      name: "Request Time Off",
      description: "Ask your manager for some time off",
      icon: "assets/icon.png",
      workflows: [CreateTimeOffRequestWorkflow],
      functions: [SendTimeOffRequestToManagerFunction],
      outgoingDomains: [],
      botScopes: [
        "commands",
        "chat:write",
        "chat:write.public",
        "datastore:read",
        "datastore:write",
      ],
    });
    
    

    The Manifest of a CLI Slack app describes the most important application information, such as its name, description, icon, the list of Workflows and Functions (among many other things). Read through the full Manifest documentation to learn more.

    The following steps will guide you through how to expand the manifest file into a functional app.

Step complete!

Step 3Create a Function

Functions are where you define inputs and outputs of your app, and implement how your app transforms the inputs into outputs.

  • Define a Function

    First we will define and implement our Function. Functions are reusable building blocks that accept inputs, perform calculations and provide outputs.

    The code behind the app's function is stored under the ./functions/send_time_off_request_to_manager/ directory. We're working with five files inside:

    1. definition.ts: Function definition
    2. mod.ts: Function implementation
    3. block_actions.ts: Action handler for our interactive blocks.
    4. blocks.ts: A layout of visual blocks that is easy on the eyes.
    5. constants.ts: Constant variables referenced throughout the app.

    definition.ts houses the function's input_parameters, output_parameters, title, description and implementation source file. This is a custom function as opposed to Built-In function, meaning the function implementation is up to you!

    Notice the interactivity parameter of type Schema.slack.types.interactivity -- one of the many built-in Slack types available to allow your function to utilize user interaction.

    
    import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";
    
    /**
     * Custom function that sends a message to the user's manager asking for approval
     * for the time off request. The message includes some Block Kit with two interactive
     * buttons: one to approve, and one to deny.
     */
    export const SendTimeOffRequestToManagerFunction = DefineFunction({
      callback_id: "send_time_off_request_to_manager",
      title: "Request Time Off",
      description: "Sends your manager a time off request to approve or deny",
      source_file: "functions/send_time_off_request_to_manager/mod.ts",
      input_parameters: {
        properties: {
          interactivity: {
            type: Schema.slack.types.interactivity,
          },
          employee: {
            type: Schema.slack.types.user_id,
            description: "The user requesting the time off",
          },
          manager: {
            type: Schema.slack.types.user_id,
            description: "The manager approving the time off request",
          },
          start_date: {
            type: "slack#/types/date",
            description: "Time off start date",
          },
          end_date: {
            type: "slack#/types/date",
            description: "Time off end date",
          },
          reason: {
            type: Schema.types.string,
            description: "The reason for the time off request",
          },
        },
        required: [
          "employee",
          "manager",
          "start_date",
          "end_date",
          "interactivity",
        ],
      },
      output_parameters: {
        properties: {},
        required: [],
      },
    });
    

    blocks.ts

    Block kit element layouts are stored inside of this file. Inside you will find a block definition for a message containing the user input of requested dates off.

    /**
     * Based on user-inputted data, assemble a Block Kit approval message for easy
     * parsing by the approving manager.
     */
    // deno-lint-ignore no-explicit-any
    export default function timeOffRequestHeaderBlocks(inputs: any): any[] {
      return [
        {
          type: "header",
          text: {
            type: "plain_text",
            text: `A new time-off request has been submitted`,
          },
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*From:* <@${inputs.employee}>`,
          },
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*Dates:* ${inputs.start_date} to ${inputs.end_date}`,
          },
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*Reason:* ${inputs.reason ? inputs.reason : "N/A"}`,
          },
        },
      ];
    }
    
    
  • Implement a Custom Function

    Now that your custom Function is defined, we will bring it to life by filling out the mod.ts file with various API calls and Block kit blocks.

    Remember, the Request Time Off app collects the time off start and end dates and sends that request to a manager for approval. We can utilize Block Kit buttons to help facilitate the decision process and give the user a richer experience.

    import { SendTimeOffRequestToManagerFunction } from "./definition.ts";
    import { SlackAPI } from "deno-slack-api/mod.ts";
    import { SlackFunction } from "deno-slack-sdk/mod.ts";
    import BlockActionHandler from "./block_actions.ts";
    import { APPROVE_ID, DENY_ID } from "./constants.ts";
    import timeOffRequestHeaderBlocks from "./blocks.ts";
    
    // Custom function that sends a message to the user's manager asking
    // for approval for the time off request. The message includes some Block Kit with two
    // interactive buttons: one to approve, and one to deny.
    export default SlackFunction(
      SendTimeOffRequestToManagerFunction,
      async ({ inputs, token }) => {
        console.log("Forwarding the following time off request:", inputs);
        const client = SlackAPI(token, {});
    
        // Create a block of Block Kit elements composed of several header blocks
        // plus the interactive approve/deny buttons at the end
        const blocks = timeOffRequestHeaderBlocks(inputs).concat([{
          "type": "actions",
          "block_id": "approve-deny-buttons",
          "elements": [
            {
              type: "button",
              text: {
                type: "plain_text",
                text: "Approve",
              },
              action_id: APPROVE_ID, // <-- important! we will differentiate between buttons using these IDs
              style: "primary",
            },
            {
              type: "button",
              text: {
                type: "plain_text",
                text: "Deny",
              },
              action_id: DENY_ID, // <-- important! we will differentiate between buttons using these IDs
              style: "danger",
            },
          ],
        }]);
    

    Now you've created a message with two buttons, each using a unique ACTION_ID to differentiate between an approval or denial of time off. In order to properly utilize the Block Kit buttons, we will rely on the BlockActionsHandler to route the button actions. Check it out below:

        // Send the message to the manager
        const msgResponse = await client.chat.postMessage({
          channel: inputs.manager,
          blocks,
          // Fallback text to use when rich media can't be displayed (i.e. notifications) as well as for screen readers
          text: "A new time off request has been submitted",
        });
    
        if (!msgResponse.ok) {
          console.log("Error during request chat.postMessage!", msgResponse.error);
        }
    
        // IMPORTANT! Set `completed` to false in order to keep the interactivity
        // points (the approve/deny buttons) "alive"
        // We will set the function's complete state in the button handlers below.
        return {
          completed: false,
        };
      },
      // Create an 'actions router' which is a helper utility to route interactions
      // with different interactive Block Kit elements (like buttons!)
    ).addBlockActionsHandler(
      // listen for interactions with components with the following action_ids
      [APPROVE_ID, DENY_ID],
      // interactions with the above components get handled by the function below
      BlockActionHandler,
    );
    

    This mods.ts is responsible for building a rich message to the selected manager and replying with a response that is triggered by the decision of that manager. How do we connect these function steps you may ask? Not to worry, our next step covers how to bring together the functions using a Workflow!

Step complete!

Step 4Create a Workflow

Create a workflow that gives your app some functionality.

  • Define a Workflow

    A Workflow is a set of steps that are executed in order. Each step in a Workflow is a Function. Similar to functions, workflows can also optionally accept input and pass it further along to functions that make the workflow up.

    This app contains a single workflow stored under the workflows/ folder.

    This app's workflow is composed of two functions chained sequentially as steps:

    1. The workflow uses the OpenForm Built-in Function to collect data from the user that triggered the workflow.
    2. Form data is then passed to your app's Custom Function, called SendTimeOffRequestToManagerFunction. This function is stored under the functions/ folder.

    First define the workflow with the DefineWorkflow method. Making sure to set a custom callback_id that you can reference later on once we create our Trigger.

    /**
     * A Workflow composed of two steps: asking for time off details from the user
     * that started the workflow, and then forwarding the details along with two
     * buttons (approve and deny) to the user's manager.
     */
    export const CreateTimeOffRequestWorkflow = DefineWorkflow({
      callback_id: "create_time_off",
      title: "Request Time Off",
      description:
        "Create a time off request and send it for approval to your manager",
      input_parameters: {
        properties: {
          interactivity: {
            type: Schema.slack.types.interactivity,
          },
        },
        required: ["interactivity"],
      },
    });
    

    Then place the functions in order of execution. In this case, using the Built-in OpenForm function to bring up a modal to collect the time off request, then using the Custom Function you built to send the request for approval.

    // Step 1: opening a form for the user to input their time off details.
    const formData = CreateTimeOffRequestWorkflow.addStep(
      Schema.slack.functions.OpenForm,
      {
        title: "Time Off Details",
        interactivity: CreateTimeOffRequestWorkflow.inputs.interactivity,
        submit_label: "Submit",
        description: "Enter your time off request details",
        fields: {
          required: ["manager", "start_date", "end_date"],
          elements: [
            {
              name: "manager",
              title: "Manager",
              type: Schema.slack.types.user_id,
            },
            {
              name: "start_date",
              title: "Start Date",
              type: "slack#/types/date",
            },
            {
              name: "end_date",
              title: "End Date",
              type: "slack#/types/date",
            },
            {
              name: "reason",
              title: "Reason",
              type: Schema.types.string,
            },
          ],
        },
      },
    );
    
    // Step 2: send time off request details along with approve/deny buttons to manager
    CreateTimeOffRequestWorkflow.addStep(SendTimeOffRequestToManagerFunction, {
      interactivity: formData.outputs.interactivity,
      employee: CreateTimeOffRequestWorkflow.inputs.interactivity.interactor.id,
      manager: formData.outputs.fields.manager,
      start_date: formData.outputs.fields.start_date,
      end_date: formData.outputs.fields.end_date,
      reason: formData.outputs.fields.reason,
    });
    
    

    Now with a trusty Workflow in tow, you must define a Trigger to get the wheels turning! Check out the next step to learn more about to ignite your Workflow.

Step complete!

Step 5Create a Trigger

Now that you've been acquainted with functions and workflows, lets dive into the last building block—triggers.

  • Create a Trigger

    A Trigger is a crucial finishing piece of your Slack CLI app. Creating a Trigger ignites the steps of your Workflow, running your custom & built-in Functions, allowing your app to provide a pleasant experience.

    These Triggers can be invoked by a user, or automatically as a response to an event within Slack.

    A Link Trigger is a type of Trigger that generates a Shortcut URL which, when posted in a channel or added as a bookmark, becomes a link. When clicked, the Link Trigger will run the associated Workflow.

    To create a Link Trigger for the "Request Time Off" Workflow, run the following command:

    $ slack trigger create --trigger-def triggers/trigger.ts
    

    After selecting a Workspace, the output provided will include the Link Trigger URL. Copy and paste this URL into a channel as a message, or add it as a bookmark in a channel of the Workspace you selected.

    Note: this link won't run the Workflow until the app is either running locally or deployed! Read on to learn how to run your app locally and eventually deploy it to Slack hosting.

Step complete!

Step 6Run, Deploy and Beyond

Finishing touches for this app but not the end of your Slack CLI journey.

  • Run your app

    There are two CLI commands to consider once you are ready to test your app or deploy your app to a workspace for other users to experience.

    For now, you'll want to locally install the app to the workspace. From the command line, within your app's root folder, run the following command:

    $ slack run
    

    Proceed through the prompts until you have a local server running in that terminal instance.

    It's installed! Utilize your link trigger that was defined in the previous step to start using your app!

  • Deploy your app

    When you're ready to make the app accessible to others, you'll want to deploy it instead of running it:

    $ slack deploy
    

    Congratulations! You've successfully built a approval workflow app, providing snazzy buttons to all who request time off.

  • Going Above & Beyond

    Now that we've posted a message using Block Kit, handled the user interaction of buttons and updated a message -- you now have the capability to either extend this app or create a new one from scratch!

    Be sure to check out other tracks; Welcome Bot or Create a Github issue and more. These tracks are meant to be remixed and expanded upon, so we encourage you to continue on your journey that's just begun!

Step complete!

Was this page helpful?