Request Time Off

Advanced

Developing automations requires a paid plan. Don't have one? Join the Developer Program and provision a sandbox with access to all Slack features for free.

This tutorial will guide you in creating, running, and deploying a workflow app. The Request Time Off App models how to collect user inputs as well as how to send those inputs to other users in Slack. More specifically, this app showcases one way user interactivity is implemented within an app. By the end, you will have a working app that can post Block Kit messages, handle user interactions, and update messages in real time.

First time creating a workflow app? Try an app to build your confidence, such as Hello World!

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 orchestrating a successful path forward.

Before we begin, ensure you have the following prerequisites completed:

  • Install the Slack CLI.
  • Run slack auth list and ensure your workspace is listed.
  • If your workspace is not listed, address any issues by following along with the Quickstart guide, then come on back.

Step 1Complete the prerequisites

Every app journey begins with a few common steps. Complete the prerequisites below to get started on composing your symphony.

  • Complete the prerequisites

    After you've installed the command-line interface you have two ways you can get started:

    Use a blank app

    You can create a blank app with the Slack CLI using the following command:

    slack create request-time-off-app --template https://github.com/slack-samples/deno-blank-template
    

    Use a pre-built app

    Or, you can use the pre-built Request Time Off app:

    slack create request-time-off-app --template https://github.com/slack-samples/deno-request-time-off
    

    Once you have your new project ready to go, change into your project directory.

Step complete!

Step 2Compose the manifest

At the root of every app, there exists an app manifest, which defines how an app presents itself.

  • Compose the manifest

    The app manifest is where we define the intricacies 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/default_new_app_icon.png",
      workflows: [CreateTimeOffRequestWorkflow],
      functions: [SendTimeOffRequestToManagerFunction],
      outgoingDomains: [],
      botScopes: [
        "commands",
        "chat:write",
        "chat:write.public",
        "datastore:read",
        "datastore:write",
      ],
    });
    
    

    The manifest of an app describes the most important application information, such as its name, description, icon, the list of workflows and functions, and more. Read through the full manifest documentation to learn more.

Step complete!

Step 3Create a function

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

  • Implement 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 (not including test files):

    1. block_actions.ts: An action handler for our interactive blocks.
    2. blocks.ts: A layout of visual blocks that is easy on the eyes.
    3. constants.ts: Constant variables referenced throughout the app.
    4. definition.ts: Our function definition, which houses the function's input_parameters, output_parameters, title, description and implementation source file. This is a custom function as opposed to Slack 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.
    5. mod.ts: Our function implementation.
  • Implement a custom function

    Once you define your custom function, we'll bring it to life by completing the mod.ts file with various API calls and Block Kit blocks.

    Remember, the Request Time Off app collects 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 to create a rich user experience.

    import { SendTimeOffRequestToManagerFunction } from "./definition.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, client }) => {
        console.log("Forwarding the following time off request:", inputs);
    
        // 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",
            },
          ],
        }]);
        // ...continued in the next snippet
    

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

    // ...continued from the snippet above
        // 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 function is responsible for building a message, sending it 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 4Define a workflow

Create a workflow to define the structure of your app.

  • Define a workflow

    A workflow is a set of steps that are executed in order. Each step in a workflow can be a function. Similar to functions, workflows can also optionally accept inputs and pass them further along to other functions that comprise the workflow.

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

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

    1. The workflow uses the OpenForm Slack function to collect data from the user that started the workflow.
    2. Form data is then passed to your app's custom function, which is called SendTimeOffRequestToManagerFunction. This function is stored within the functions/ folder.

    First let's define the workflow with the DefineWorkflow method. Make sure to set a custom callback_id that you can reference later on.

    import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
    import { SendTimeOffRequestToManagerFunction } from "../functions/send_time_off_request_to_manager/definition.ts";
    
    /**
     * 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, use the Slack OpenForm function to open a modal form to collect the time off request data; then use 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,
    });
    
    

    Voilà! Next, let's define a trigger to get the wheels in motion!

Step complete!

Step 5Create a trigger

Now that you've been acquainted with functions and workflows, lets dive into triggers.

  • Creating a trigger

    A trigger is a crucial finishing piece of your app. Creating a trigger sets the steps of your workflow in motion, which runs your custom & Slack 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 our workflow, run the following command:

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

    After selecting a workspace and an app environment, the output provided will include the 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 your app

Finishing touches for this app, but not the end of your developer journey.

  • Running your app

    You're almost to the end! Let's use development mode to run this workflow in Slack directly from the machine you're reading this from now:

    $ slack run
    

    After you've chosen your app and assigned it to your workspace, you can switch over to the app in Slack and try it out. Use the link trigger you created previously; when you paste the shortcut URL into the message box and post them, it'll unfurl and give you a button for invoking your workflow.

Step complete!

Step 7Great work!

Now that our code has been built and run, let's look at our next steps.

  • What's next

    Congratulations! You've successfully built an approval workflow app, providing fancy buttons to all who request time off. Now that we've posted a message using Block Kit, handled the user interaction of buttons, and updated a message — you have the capability to either extend this app or to create a new one from scratch.

    Next steps

    For your next challenge, perhaps consider creating a social app to log runs with virtual running buddies!

Step complete!