Workflow Builder Steps from Apps: Advanced Concepts

By Adam Marinelli

Published: October 6, 2020

This tutorial continues where Workflow Builder Steps from Apps left off, sharing the same Glitch project between the two tutorials. The code for each advanced concept in this tutorial can be found in the /examples folder. Each concept has its own file that is numbered and named to indicate the order in which they appear in the tutorial. Each file is inclusive of the preceding files. In other words, /examples/4-workflow-step-failed.js contains the base code and all the advanced concepts combined! A copy of the original code from Part 1 is saved to the /examples/0-index.js file should you need it.

Concept File name
Code from Part 1 index.js
Copy of code from Part 1 /examples/0-index.js
Saving the step configuration between edits /examples/1-save-config.js
Push an update the the configuration view /examples/2-update-config-view.js
Derive additional output values when the step is executed /examples/3-derive-new-outputs.js
Fail a workflow step execution /examples/4-workflow-step-failed.js

Workflow Builder requires a paid workspace. If you’re developing on a free workspace, please reach out to feedback@slack.com to get a sandbox Slack Plus or an Enterprise Grid instance to develop steps for your app.

Getting started

As a prerequisite to this tutorial, please complete Workflow Builder Steps from Apps tutorial Part 1 where you will set up your Slack app and build a workflow that uses your step.

This tutorial will build on the same workflow, Glitch project, and sample code.

🎏🥫 Source code on Glitch

🎏🍴 Remix (fork) the Glitch repo

When your Glitch server is up and running, the root index.js file will be running your step’s logic. When it comes time to run a file associated with a different concept, you can start your app with that file rather than index.js.

As you work through the concepts below, along with the associated *.js files found in the /examples folder, there are two options to make sure Glitch is running the correct version:

  1. Since Glitch runs the root index.js file by default, you can copy and paste the entire contents of any one of the example files into it. Once Glitch restarts, which typically happens automatically after an edit is made, that newly pasted code will be running and active. When you move to the next advanced concept, simply copy and paste the entirety of another file to the root index.js file to overwrite it. A backup copy of the original script (0-index.js) is in the /examples folder should you need it.

  2. Instead of copying and pasting as noted above, you can instruct Glitch to simply run a different *.js file other than the default index.js in the root folder. In package.json, update the main and scripts.start properties to point to the advanced concepts file you’d like to run. The package.json example below shows what edits need to be made if you are working on the fourth and last advanced concept and need that script file to be running and active.

{
  "name": "workflow-extensions-examples",
  "version": "1.0.0",
  "description": "",
  "main": "examples/4-workflow-step-failed.js",
  "type": "module",
  "scripts": {
    "start": "node examples/4-workflow-step-failed.js"
  },
  "engines": {
    "node": ">=14"
  },
  "author": "Sarah",
  "license": "MIT",
  "dependencies": {
    "@slack/bolt": "2.4.1"
  }
}

Advanced concepts

1. Saving the step configuration between edits

File name: /examples/1-save-config.js

When your app receives the workflow edit interaction payload, it can be the first time your step was added to a workflow, or an instance when the builder edits a previously configured step.

In Part 1 of this tutorial, reediting the modal would show empty text inputs even though they were saved in the step behind-the-scenes. If inputs are not repopulated, the builder may not understand that their step is still configured correctly.

To populate the inputs when a step is edited, access any incoming inputs with step.inputs.

// Check if these inputs exist, if they do, the builder is editing the step that has saved values
// We will want to restore those in the step view returned below using the initial_value property of the inputs
let { taskName, taskDescription, taskAuthorEmail } = step.inputs;

In the blocks that are sent to open the configuration view, the example adds an initial_value property to each input element.

A conditional operator is used to check if the variables exist for the plain text input elements (i.e. inputs were previously configured and saved by the builder) before assigning either the incoming value or a null placeholder. The example below for the task name element is also applied to the other text inputs.

initial_value: taskName ? taskName.value : ""

2. Update the configuration view

File name: /examples/2-update-config-view.js

An app can update the configuration modal in response to user interactions. The example adds a new task project section block with a select menu, which can be used to trigger an update to the initial view. Updating the configuration view is a necessary practice when inputs rely on the value/state of preceding inputs.

In this scenario, the reason for the view update is to ensure that the task project input is correctly set. Instead of sending an error message to the user that a required field is missing a value, we can disabled the view's submit button until a task project has been selected.

In the edit callback function of the workflow step, three new blocks are added to configuration modal: two dividers (for styling) and a static select menu containing a few sample menu options.

{
  type: "section",
  block_id: "intro-section",
  text: {
    type: "plain_text",
    text:
    "Create a task in one of the listed projects. The link to the task and other details will be available as variable data in later steps.",
    emoji: true
  }
}, {
  type: "divider"
}, {
  type: "section",
  block_id: "project_name_section",
  text: {
    type: "mrkdwn",
    text: "*Project name*"
  },
  accessory: {
    action_id: "project",
    type: "static_select",
    placeholder: {
      type: "plain_text",
      text: "Select a project"
    },
    options: [{
        text: {
          type: "plain_text",
          text: "Bug fix"
        },
        value: "bug-2"
      }, {
        text: {
          type: "plain_text",
          text: "Website update"
        },
        value: "web-3"
      }, {
        text: {
          type: "plain_text",
          text: "Social media post"
        },
        value: "social-4"
      }, {
        text: {
          type: "plain_text",
          text: "Documentation update"
        },
        value: "doc-1"
      }
    ]
  }
}, {
  type: "divider"
}

One interesting quirk is that we are not using an input block to style this specific select menu. This is because the interactivity request that an action block sends your app is required in order to trigger a view update. In a future Block Kit update, input blocks will have the ability to dispatch interaction requests just as action blocks can.

Since we are using an action block, the builder will set the task project once when first configuring the workflow, and each task created by use of the workflow will link the task to that predefined task project. This is because the ability to insert a variable from a previous step into a select menu element is not possible. So while the values for task name or task description can be assigned using variables from previous workflow steps, end users will have their new tasks assigned to whichever project the builder determined when they saved and published the step.

We will need to add an action listener in Bolt to receive user interactions with this new select menu so that we can update the view accordingly. You can find this code near the bottom of the /examples/2-update-config-view.js file.

// Project name listener for interactions with this select menu
app.action("project", async({ ack, body, client }) => {
  await ack();

  try {
    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 no update any blocks
      // Enable the submit/save button now that a project has been set
      view: {
        type: "workflow_step",
        // View identifier
        callback_id: "copy_review",
        // Using the same blocks, only updating the state of the submit/save button
        // Since none of the block or action ids were modified, any input
        // values set by the user are retained
        blocks: body.view.blocks,
        //Setting this to false will enable the button when the view is updated
        submit_disabled: false
      }
    });
  } catch (error) {
    console.error(error);
  }
});

Since we have added a new step input, this needs to be accounted for in the save callback function of the workflow step. A new taskProject variable will hold the value of the selected option:

const {
  project_name_section,
  task_name_input,
  task_description_input,
  task_author_input
} = view.state.values;

const taskProject = project_name_section.project.selected_option;
const taskName = task_name_input.name.value;
const taskDescription = task_description_input.description.value;
const taskAuthorEmail = task_author_input.author.value;

const inputs = {
  taskProject: {
    value: taskProject
  },
  taskName: {
    value: taskName
  },
  taskDescription: {
    value: taskDescription
  },
  taskAuthorEmail: {
    value: taskAuthorEmail
  }
};

We will also add a new output to both the save callback function as well as set the actual value of that output in the execute callback function of the workflow step.

Create the new output in the save callback function:

{
  type: "text",
  name: "taskProject",
  label: "Project Name"
}

Set the new output in the execute callback function:

const { taskProject, taskName, taskDescription, taskAuthorEmail } = step.inputs;

const outputs = {
  taskProject: taskProject.value.text.text,
  taskName: taskName.value,
  taskDescription: taskDescription.value,
  taskAuthorEmail: taskAuthorEmail.value
};

Since we added a new element to the view, we will also want to repopulate any selection the builder makes if the configuration view is saved and reopened, repeating what was implemented in /examples/1-save-config.js for the original three inputs.

Relatedly, we cannot always rely on a builder's interaction with the select menu to trigger the code path to enable the submit/save button. This is because a project may have already been set the last time the builder saved the configuration modal. So when the app receives the edit interaction, we should check to see if a value for the task project has already been set. If so, assign it as the initial_option in the project name section block and then enable the submit/save button.

// The default is to disable the input in this view
let submit_disabled = true;

// If the step was previously configured and saved, we should
// repopulate the select menu with that value and enable the submit button
// using the `initial_option` property of the static select menu.
// We have hardcoded the position of this select menu in our view as
// the 3rd element (2nd index) in the `blocks` array so that we can modify it.
if (taskProject) {
  blocks[2].accessory.initial_option = taskProject.value;
  submit_disabled = false;
}

Now that the task project input and output variables have been configured, update the Send a message step that posts to channel to utilize this new task project output value.

3. Derive additional output values when the step is executed

File name: /examples/3-derive-new-outputs.js

Up until this point, the inputs collected from the user using the built-in workflow form were simply passed directly through our step and made available as outputs. The exception is the task project input since that is set by the builder rather than the end user.

When you use the Send a message step, you can choose the variables that the form outputs or the ones that our step outputs. Since the step does not modify them, they are identical.

In this section, we will create two additional outputs which are only generated when the step executes: taskId and taskUrl.

First we'll need to add these two outputs when saving our step in the save callback function of our workflow step. This way when modifying a workflow in the builder, these output variables are available in subsequent steps even before anything has been executed. The outputs array now looks like this:

const outputs = [{
    type: "text",
    name: "taskProject",
    label: "Project Name"
  }, {
    type: "text",
    name: "taskName",
    label: "Task Name"
  }, {
    type: "text",
    name: "taskDescription",
    label: "Task Description"
  }, {
    type: "text",
    name: "taskAuthorEmail",
    label: "Task Author Email"
  }, {
    type: "text",
    name: "taskId",
    label: "Task ID"
  }, {
    type: "text",
    name: "taskUrl",
    label: "Task URL"
  }
];

In the execute callback function of our workflow step, we can assign values to these outputs. It is this part of the code logic where an external endpoint would be called to create a new task in an external system. This has been mocked in the code comments of the file. For the purposes of this tutorial, the taskId value is assigned as a random number between 0 and 1000 and the taskUrl simply uses the example.com domain with the current timestamp in the URL.

Once the /examples/3-derive-new-outputs.js file is running in your Glitch project, you will not immediately see these two new outputs available in Workflow Builder. For example, if trying to use them in the Send a message step. You'll first have to open your workflow step and resave it. This is because modifications to the outputs only happen when the app sends a request to workflows.updateStep. And that request is not made until the builder saves the step and the save callback function is run to completion.

Once the step is resaved, edit the Send a message step and insert one or both of the new output variables that are now available.

Send message

4. Fail a workflow step execution

File name: /examples/4-workflow-step-failed.js

The last feature we will add to the step is to make sure that if there are issues, the step fails in such a way that it notifies someone. A request to workflows.stepFailed instead of workflows.stepCompleted will achieve this.

As an example in this tutorial, we will fail the step if the task name submitted by the end user via the form matches the text "test."

// To demonstrate a step failure, imagine that the task name "test" is a reserved name.
// The step will fail if this value is supplied by the user who submitted the form.

if (taskName.value.toLowerCase() == "test") {
  // fail is Bolt's way of sending a request to workflows.stepFailed
  return fail({
    error: {
      message: `Task not created! The task name '${taskName.value}' is not allowed`
    }
  });
}

The workflow Activity tab is where this custom error message is surfaced (as well as previously successful/failed executions).

The Activity Tab

Once the tutorial code for this concept is running in Glitch, start the workflow via the shortcut menu and submit the value "Test" as the name of the task when running the workflow. It should fail and an entry will be added to the Activity tab. This view does not update automatically so you will need to switch away from the tab and back again to trigger a refresh.

Next steps

Great work! You now have a step set up for your app with a handful of advanced concepts integrated into it. The Glitch project remixed in this tutorial can be modified in any way you choose to match a use case specific to your team or organization's needs. We have a few more resources to help you continue on your journey with Workflow Builder steps from apps.

Related documentation

Was this page helpful?