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 set of feedback via a modal and publish it to a specific channel.
Features you’ll use
Quickly create an app with the correct configuration of scopes and features for this tutorial by clicking below.
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.
We'll show you how to use the app settings pages and give you a brief tour of all the features you can setup for your app.
You won't get far without a Slack app to accompany you on this journey. Start here by creating an 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:
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 settings after installation, 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.
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:
You'll be redirected to the OAuth permission request dialog:
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 settings. 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.
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:
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.
Each app can create workflow steps that can be used over and over again in countless workflows through Workflow Builder.
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.
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:
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:
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.
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.
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.
Interaction payloads are sent to your app's configured Request URL. To add a Request URL to your app:
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.
With Bolt, the process of receiving and processing workflow_step_edit
interaction payloads is done by creating a new WorkflowStep
class and add an edit
callback:
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);
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 }) => { }, });
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.
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.
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
.
Each block is represented in our APIs as a JSON object. Here's an example of a 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>"
}
}
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.
Individual blocks can be stacked together to create complex visual layouts.
When you've chosen each of the blocks you want in your layout, 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>"
}
}
]
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.
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:
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);
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 }) => { }, });
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.
Not using Bolt? Read how to manually build an app that can receive and process workflow_step_edit
interaction payloads.
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.
After you've finished with this section, your app's step will be ready to use.
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:
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);
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!" } }); }, });
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.
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.
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.
Now you've built your app's step, try it out within a workflow by using Workflow Builder.
Set up your workflow, add collaborators, add steps, and publish. Learn how to build a workflow with our click-by-click walkthrough.