Collecting product feedback

In this tutorial, you'll see how you can build an app-controlled step that can be added by anyone to workflows in Workflow Builder. This step will collect a simple set of feedback via a modal and publish it to a specific channel.

Features you’ll use

Scopes you'll request

Create a pre-configured app

Quickly create an app with the correct configuration of scopes and features for this tutorial by clicking below.

1Before you begin

Every Slack app journey begins with a few common steps. You'll need to create an app, give it some permission scopes, and then install it to a suitable development workspace.

  • Setting up a Slack app
    Watch our video showing how to create and setup a new Slack app.

    We'll show you how to use the app config pages and give you a brief tour of all the features you can setup for your app.

  • Create an app

    You won't get far without a Slack app to accompany you on this journey. Start here by creating an app.

  • Requesting scopes for your app

    Scopes give your app permission to do things (for example, post messages) in Slack. They're an essential component of any functional Slack app.

    Each scope that you add to your app will be presented in a permission request dialog during app installation. When you're building an app in a development workspace that you control, you'll be able to experience both sides of this system.

    Each app can use lots of different scopes, with the exact list depending on the type of access token the app will use. We favour using bot tokens for most apps, so we'll explain how to request scopes for that token type.

    To choose the scopes to add to your app:

    1. Head over to app config page for your app.
    2. Navigate to the OAuth & Permissions page.
    3. Scroll down to the Bot Token Scopes section.
    4. Click Add an OAuth Scope.
    5. Add the scopes listed above.

    Once added, the scope list will be saved automatically. Your app will now request these scopes during installation.

    If you add scopes to your app config after installation, simply reinstall your app to request and grant permission for the added scopes.

    To read more about scopes and permissions, check out our app authentication docs.

  • Install your app

    Your app can request any scope you want—but final say always resides with the user installing your app. A user can choose to refuse any and all installs that seem to request permissions beyond what they're comfortable granting.

    When you're installing your app to your own development workspace, you can experience this process for yourself.

    To install your app to your development workspace:

    1. Head over to app config page for your app.
    2. Navigate to the Install App page.
    3. Click Install to Workspace.

    You'll be redirected to the OAuth permission request dialog:

    Oauth UI for users

    As a user, you're choosing to trust the app. Is it trustworthy? Well, you're building it—hopefully, it's not too bad. After installation, you'll be redirected back to your app config. You'll see the newly generated access token on the Install App page you're redirected to.

    Access tokens are imbued with power. Your app is able to use this new access token to do practically everything that a Slack app can do.

    Remember to keep your access token secret and safe, to avoid violating the security of your app, and to maintain the trust of your app's users.

    At a minimum, avoid checking your access token into public version control. Access it via an environment variable. We've also got plenty more best practices for app security.

  • Use Bolt to build your app

    Bolt is a framework that lets you build Slack apps in a flash—available in JavaScript, Python, and Java.

    Bolt handles much of the foundational setup so you can focus on your app's functionality. Out of the box, Bolt includes:

    • A basic web server to run your app on
    • Authentication and installation handling for all the ins and outs of OAuth
    • Simplified interfaces for all Slack APIs and app features
    • Automatic token validation, retry, and rate-limiting logic

    Bolt also has built-in type support, so you can get more work done right from your code editor.

    Follow our guides on getting started with Bolt for JavaScript, Bolt for Python, or Bolt for Java. Or dig deeper by exploring our resources and sample code.

2Setup workflow steps for your app

Each app can create workflow steps that can be used over and over again in countless workflows through Workflow Builder.

When you're done

After finishing this step, your app will have a workflow step available in Workflow Builder, but the step won't be able to do anything yet.

  • Create your first step

    In your app's config, click on the Workflow Steps section. Select the toggle to opt-in — this will automatically add a few settings (such as permission scopes and event subscriptions) so that you can start creating workflow steps.

    You'll see a Steps section, listing any steps you've already created in your app. Each app can create up to 10 steps. Click the Add Step button, you'll see a modal appear with fields to configure your app's new step:

    • Step name - a descriptive name which is shown to the Workflow Builder user.
    • Callback ID - a string that will act as a unique identifier for this particular step. It will be sent back to you with each action involving your step.

    Click Save and you've created your app's first workflow step!

    Your app's step will now be available to Workflow Builder users in workspaces where:

    • Your app is installed.
    • An owner or admin has enabled the steps from apps feature in the workspace or Enterprise Grid org's settings.

3Prepare your app for workflow editing interactions

When your app's steps are inserted or edited by Workflow Builder users, we'll send you an interaction payload. Your app will need to be prepared to receive, process, and acknowledge these payloads correctly at any time.

When you're done

After finishing this step, your app will be able to handle and respond to workflow_step_edit interaction payloads and can be successfully inserted into workflows.

  • A brief overview of workflow step insertion

    The process of inserting or editing an app's workflow step using Workflow Builder will trigger some interactions with the app:

    A user adds or edits an app's custom step in one of their workflows using Workflow Builder.

    A workflow_step_edit interaction payload is sent to the app, which correctly receives and processes it.

    Using data from the interaction payload, the app opens a modal for the user to configure the step.

    The user submits the modal, and the app saves this workflow step configuration.

    In the next few steps, we'll get your app ready to handle this flow.

  • Configure a Request URL

    Interaction payloads are sent to your app's configured Request URL. To add a Request URL to your app:

    1. Head over to app config page for your app.
    2. Navigate to the Interactivity & Shortcuts page.
    3. If you haven't already, switch on the Interactivity toggle.
    4. Add your Request URL.
    5. Click Save Changes at the bottom of the page.

    If you're using Socket Mode, you won't need to configure a Request URL. Interaction payloads will instead be sent to your WebSocket URL, and will be wrapped in some Socket Mode specific metadata.

  • Process the workflow edit interaction payload

    When you're using the Bolt framework, the process of receiving and processing workflow_step_edit interaction payloads is simple. All you need to do is create a new WorkflowStep class and add an edit callback:

    Capturing workflow_step_edit interactions
    Java
    JavaScript
    Python
    Java
    import com.slack.api.bolt.App;
    import com.slack.api.bolt.middleware.builtin.WorkflowStep;
    
    // Initiate the Bolt app as you normally would
    App app = new App();
    
    WorkflowStep step = WorkflowStep.builder()
      .callbackId("your_callback_id")
      .edit((req, ctx) -> {
        ctx.configure(asBlocks(
          section(s -> s.blockId("intro-section").text(plainText("text"))),
          input(i -> i
            .blockId("feedback_name_input")
            .element(plainTextInput(pti -> pti.actionId("feedback_name")))
            .label(plainText("Who is the author?"))
          ),
          input(i -> i
            .blockId("feedback_description_input")
            .element(plainTextInput(pti -> pti.actionId("feedback_description")))
            .label(plainText("What is the feedback?"))
          )
        ));
        return ctx.ack();
      })
      .save((req, ctx) -> { return ctx.ack(); })
      .execute((req, ctx) -> { return ctx.ack(); })
      .build();
    
    app.step(step);
    
    JavaScript
    const ws = new WorkflowStep('add_task', {
    	edit: async ({ ack, step, configure }) => {
    		await ack();
    
    		const blocks = [
    			{
    				type: 'input',
    				block_id: 'feedback_name_input',
    				element: {
    					type: 'plain_text_input',
    					action_id: 'feedback_name',
    					placeholder: {
    						type: 'plain_text',
    						text: 'Select the author field',
    					},
    				},
    				label: {
    					type: 'plain_text',
    					text: 'Who is the author?',
    				},
    			},
    			{
    				type: 'input',
    				block_id: 'feedback_description_input',
    				element: {
    					type: 'plain_text_input',
    					action_id: 'feedback_description',
    					placeholder: {
    						type: 'plain_text',
    						text: 'Select the feedback field',
    					},
    				},
    				label: {
    					type: 'plain_text',
    					text: 'What is the feedback?',
    				},
    			},
    		];
    
    		await configure({ blocks });
    	},
    	save: async ({ ack, step, update }) => { },
    	execute: async ({ step, complete, fail }) => { },
    });
    Python
    def edit(ack, step, configure):
        ack()
    
        blocks = [
            {
                "type": "input",
                "block_id": "feedback_name_input",
                "element": {
                    "type": "plain_text_input",
                    "action_id": "feedback_name",
                    "placeholder": {"type": "plain_text", "text": "Select the author field"},
                },
                "label": {"type": "plain_text", "text": "Who is the author?"},
            },
            {
                "type": "input",
                "block_id": "feedback_description_input",
                "element": {
                    "type": "plain_text_input",
                    "action_id": "feedback_description",
                    "placeholder": {"type": "plain_text", "text": "Select the feedback field"},
                },
                "label": {"type": "plain_text", "text": "What is the feedback?"},
            },
        ]
        configure(blocks=blocks)
    
    ws = WorkflowStep(
        callback_id="your_callback_id",
        edit=edit,
        save=save,
        execute=execute,
    )
    app.step(ws)

    The blocks array here defines a configuration modal, which we'll explain below.

    You can view a full list of the fields in the viewflow_step_edit payload by reading our reference guide.

  • Acknowledge receipt of the interaction payload

    Apps must acknowledge the receipt of all interaction payloads within 3 seconds. To enable this acknowledgment response, Bolt interaction listeners are passed a callable ack() function (in Bolt for Java this function is ctx.ack()).

    This ack() function requires no arguments. We recommend calling ack() immediately, before any other processing, since you only have 3 seconds to respond.

  • Design a configuration modal

    When someone building a workflow inserts your app's step, they will need a way to pass along information entered by end-users in earlier steps. We can use what we call a configuration modal to enable this.

    Each field in your configuration modal should allow the builder to define the source for all input data that your app requires. The builder will be able to insert Handlebars-like {{variables}} into any plain-text field to auto-populate output data from previous steps.

    The configuration modal can also be used to configure data that doesn't come from end-users, but is defined only by the Workflow Builder user.

    A configuration modal can be updated by your app as long as it remains open. However, unlike regular modals, you cannot push new views.

    Slack provides a range of visual components, called Block Kit, that can be used to layout configuration modals with various types of inputs. To design your configuration modal, you'll need to create an array of blocks.

    Define a single block

    Each block is represented in our APIs as a JSON object. Here's an example of a simple section block:

    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "New Paid Time Off request from <example.com|Fred Enriquez>\n\n<https://example.com|View request>"
      }
    }
    

    Preview

    Every block contains a type field — specifying which of the available blocks to use — along with other fields that describe the content of the block.

    Block Kit Builder is a visual prototyping sandbox that will let you choose from, configure, and preview all the available blocks.

    If you want to skip the builder, the block reference guide contains the specifications of every block, and the JSON fields required for each of them.

    Stacking multiple blocks

    Individual blocks can be stacked together to create complex visual layouts.

    When you've chosen each of the blocks you want in your layout, simply place each of them in an array, in visual order, like this:

    [
    	{
    		"type": "header",
    		"text": {
    			"type": "plain_text",
    			"text": "New request"
    		}
    	},
    	{
    		"type": "section",
    		"fields": [
    			{
    				"type": "mrkdwn",
    				"text": "*Type:*\nPaid Time Off"
    			},
    			{
    				"type": "mrkdwn",
    				"text": "*Created by:*\n<example.com|Fred Enriquez>"
    			}
    		]
    	},
    	{
    		"type": "section",
    		"fields": [
    			{
    				"type": "mrkdwn",
    				"text": "*When:*\nAug 10 - Aug 13"
    			}
    		]
    	},
    	{
    		"type": "section",
    		"text": {
    			"type": "mrkdwn",
    			"text": "<https://example.com|View request>"
    		}
    	}
    ]
    

    Preview

    Block Kit Builder will allow you to drag, drop, and rearrange blocks to design and preview Block Kit layouts.

    Alternatively you can use the block reference guide to manually generate a complete blocks array, like the one shown above.

    Once you have your blocks array created, use it in the WorkflowStep edit callback you added in the step above.

  • Listen for submission of the configuration modal

    When a configuration modal is submitted, your app will be sent a view_submission interaction payload.

    If you're using Bolt, you can easily acknowledge and process this payload by using the save callback in the WorkflowStep class:

    Capturing configuration modal view_submission interactions
    Java
    JavaScript
    Python
    Java
    import java.util.*;
    import com.slack.api.model.workflow.*;
    import static com.slack.api.model.workflow.WorkflowSteps.*;
    
    static String extract(Map<String, Map<String, ViewState.Value>> stateValues, String blockId, String actionId) {
      return stateValues.get(blockId).get(actionId).getValue();
    }
    
    WorkflowStep step = WorkflowStep.builder()
      .callbackId("copy_review")
      .edit((req, ctx) -> { return ctx.ack(); })
      .save((req, ctx) -> {
        Map<String, Map<String, ViewState.Value>> stateValues = req.getPayload().getView().getState().getValues();
        Map<String, WorkflowStepInput> inputs = new HashMap<>();
        inputs.put("taskName", stepInput(i -> i.value(extract(stateValues, "task_name_input", "task_name"))));
        inputs.put("taskDescription", stepInput(i -> i.value(extract(stateValues, "task_description_input", "task_description"))));
        inputs.put("taskAuthorEmail", stepInput(i -> i.value(extract(stateValues, "task_author_input", "task_author"))));
        List<WorkflowStepOutput> outputs = asStepOutputs(
          stepOutput(o -> o.name("taskName").type("text").label("Task Name")),
          stepOutput(o -> o.name("taskDescription").type("text").label("Task Description")),
          stepOutput(o -> o.name("taskAuthorEmail").type("text").label("Task Author Email"))
        );
        ctx.update(inputs, outputs);
        return ctx.ack();
      })
      .execute((req, ctx) -> { return ctx.ack(); })
      .build();
    
    app.step(step);
    
    JavaScript
    const ws = new WorkflowStep('add_task', {
    	edit: async ({ ack, step, configure }) => { },
    	save: async ({ ack, step, view, update }) => {
    		await ack();
    
    		const { values } = view.state;
    		const taskName = values.task_name_input.name;
    		const taskDescription = values.task_description_input.description;
    
    		const inputs = {
    			taskName: { value: taskName.value },
    			taskDescription: { value: taskDescription.value }
    		};
    
    		const outputs = [
    			{
    				type: 'text',
    				name: 'taskName',
    				label: 'Task name',
    			},
    			{
    				type: 'text',
    				name: 'taskDescription',
    				label: 'Task description',
    			}
    		];
    
    		await update({ inputs, outputs });
    	},
    	execute: async ({ step, complete, fail }) => { },
    });
    Python
    def save(ack, view, update):
        ack()
    
        values = view["state"]["values"]
        task_name = values["task_name_input"]["name"]
        task_description = values["task_description_input"]["description"]
    
        inputs = {
            "task_name": {"value": task_name["value"]},
            "task_description": {"value": task_description["value"]}
        }
        outputs = [
            {
                "type": "text",
                "name": "task_name",
                "label": "Task name",
            },
            {
                "type": "text",
                "name": "task_description",
                "label": "Task description",
            }
        ]
        update(inputs=inputs, outputs=outputs)
    
    ws = WorkflowStep(
        callback_id="add_task",
        edit=edit,
        save=save,
        execute=execute,
    )
    app.step(ws)

    The update function will make a call to the workflows.updateStep Web API method. This method stores the configuration of your app's step for a specified workflow.

    There are a couple of arguments that the update function requires - input and output.

    • inputs is a key-value map of objects describing data required by your app's step. Input values can contain Handlebars {{variables}}, which are used to include values from the outputs of some earlier step in the workflow.
    • outputs is an array of objects that describe the data your app's step will return when completed. Subsequent steps in the workflow after your app's step will be able to use these outputs as variables.

    Read the reference guides for input objects and output objects to learn their full structure.

  • AlternativeProcess interaction payloads manually

    Not using Bolt? Read how to manually build an app that can receive and process workflow_step_edit interaction payloads.

4Prepare your app for step executions

Once your step has been added to a workflow, your app will start to receive notifications when the step is being executed. These notifications will be sent via the Events API.

The Events API offers a range of subscriptions that push payloads to an app when specific events occur in Slack.

When you build a workflow step for an app, the app must be ready to receive, process, and respond to these event subscriptions.

When you're done

After you've finished with this section, your app's step will be ready to use.

  • Listen for step execution events using Bolt

    When you switched on the Workflow Steps feature in your app, you were automatically subscribed to the workflow_step_execute event. This event will be sent to your app every time a workflow containing one of the app's steps is used.

    Using Bolt, you can listen for and respond to this event by adding an execute callback to your WorkflowStep class:

    Capturing workflow_step_execute events
    Java
    JavaScript
    Python
    Java
    import java.util.*;
    import com.slack.api.model.workflow.*;
    
    WorkflowStep step = WorkflowStep.builder()
      .callbackId("copy_review")
      .edit((req, ctx) -> { return ctx.ack(); })
      .save((req, ctx) -> { return ctx.ack(); })
      .execute((req, ctx) -> {
        WorkflowStepExecution wfStep = req.getPayload().getEvent().getWorkflowStep();
        Map<String, Object> outputs = new HashMap<>();
        outputs.put("taskName", wfStep.getInputs().get("taskName").getValue());
        outputs.put("taskDescription", wfStep.getInputs().get("taskDescription").getValue());
        outputs.put("taskAuthorEmail", wfStep.getInputs().get("taskAuthorEmail").getValue());
        try {
          ctx.complete(outputs);
        } catch (Exception e) {
          Map<String, Object> error = new HashMap<>();
          error.put("message", "Something wrong!");
          ctx.fail(error);
        }
        return ctx.ack();
      })
      .build();
    
    app.step(step);
    JavaScript
    const ws = new WorkflowStep('add_task', {
    	edit: async ({ ack, step, configure }) => { },
    	save: async ({ ack, step, update }) => { },
    	execute: async ({ step, complete, fail }) => {
    		const { inputs } = step;
    
    		const outputs = {
    			taskName: inputs.taskName.value,
    			taskDescription: inputs.taskDescription.value,
    		};
    
    		// if everything was successful
    		await complete({ outputs });
    
    		// if something went wrong
    		// fail({ error: { message: "Just testing step failure!" } });
    	},
    });
    Python
    def execute(step, complete, fail):
        inputs = step["inputs"]
        # if everything was successful
        outputs = {
            "task_name": inputs["task_name"]["value"],
            "task_description": inputs["task_description"]["value"],
        }
        complete(outputs=outputs)
    
        # if something went wrong
        error = {"message": "Just testing step failure!"}
        fail(error=error)
    
    ws = WorkflowStep(
        callback_id="add_task",
        edit=edit,
        save=save,
        execute=execute,
    )
    app.step(ws)

    The event payload will include inputs that contain any values that your step requires. These inputs come from earlier in the workflow, or from something entered in the configuration modal when the workflow step was being inserted.

    You can process information from the payload or use any other relevant data to complete the step. Your app can make API calls, publish to a database, switch a lightbulb on, refresh a user's App Home, or do anything else that is relevant to the step.

    That said, within the execute callback your app must either call complete() to indicate that step execution was successful, or fail() to indicate that step execution failed.

    The complete function expects an outputs object, the shape of which is dictated by the outputs array sent earlier in the WorkflowStep.save callback. Use the name from each object in that array as keys, mapped to the calculated value for each output.

  • AlternativeManually build a listener for step execution events

    Not using Bolt? Read our guide to the Events API to read how you can manually construct a listener for workflow_step_execute events. Then read our guide to creating workflow steps if you need more information.

  • OptionalPublish an interactive notification

    Your app can do anything you can think of within the execute callback. For this tutorial, a suggested addition is to publish an interactive notification to a feedback-oriented Slack channel.

    The interactive notification could include options for acting on the feedback right from Slack. Convert it to an internal task, file it in a spreadsheet, or send it to a team dashboard. It's up to you!

    Keep going with our tutorial on publishing interactive notifications to see how it can be done.

5Try your new step in Workflow Builder

Now you've built your app's step, try it out within a workflow by using Workflow Builder.

  • OptionalUse Workflow Builder

    Set up your workflow, add collaborators, add steps, and publish. Learn how to build a workflow with our click-by-click walkthrough.

Was this page helpful?