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.
Adding a workflow step to your app and implementing a corresponding function listener is how you define a custom Workflow Builder step.
In this tutorial, you'll use the Bolt for JavaScript SDK or Bolt for Python SDK to add a workflow step, then wire it up as a workflow step in Workflow Builder.
When finished, you'll be ready to build scalable and innovative workflow steps for anyone using Workflow Builder in your workspace.
In this tutorial, you'll be wiring up a sample app with a sample step and corresponding function listener to be used as a workflow step in Workflow Builder. Here's how it works:
Skip to the code
If you'd rather skip the tutorial and just head straight to the code, create a new app and use our Bolt JS custom step sample or Bolt Python custom step sample
as a template. The sample custom step provided in the template will be a good place to start exploring!
Ready? Let's get started!
Before we begin, let's make sure you're set up for success.
Make sure you have a development workspace where you have permission to install apps. We recommend setting up your own space used for exploration and testing in a developer sandbox.
For this tutorial, We'll use boltstep
as the app name. For your app, be sure to use a unique name that will be easy for you to find, then use that name wherever you see boltstep
in this tutorial. Regardless, the app will be named "Bolt Custom Step", as that is defined in the manifest.json
file of the sample app code.
Let's start by opening a terminal and cloning the starter template repository:
git clone https://github.com/slack-samples/bolt-js-custom-step-template.git boltstep
git clone https://github.com/slack-samples/bolt-python-custom-step-template.git boltstep
When the terminal is finished cloning the template, change directories into your newly prepared app project:
cd boltstep
If you're using VSCode (highly recommended), you can enter code .
from your project's directory and VSCode will open your new project.
You can also open a terminal window from inside VSCode like this: Ctrl
+ ~
Once in VSCode, open the terminal. Let's now install our package dependencies; run the following command(s) in the terminal inside VSCode:
npm install
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
We now have a Bolt app ready for development! Open the manifest.json
file and copy its contents; we will need this in the next step.
Open a browser and navigate to api.slack.com/apps
. This is where we will create a new app with our previously copied manifest details. Click the Create New App button, then select From an app manifest when prompted to choose how you'd like to configure your app's settings.
Next, select a workspace where you have permissions to install apps, then confirm Next. Select the JSON tab and clear the existing contents. Paste the contents of the manifest.json
file you previously copied.
Click Next. Then you will be shown a brief overview of the features your app includes. You'll see we are creating an app with a chat:write
bot scope, an App Home and Bot User, as well as Socket Mode, Interactivity, an Event Subscription, and Org Deploy. We'll get into these details later. Click Create.
All of your app's settings can be configured in these screens. By creating an app from an existing manifest, you will notice many settings have already been configured. Navigate to Org Level Apps and notice that we've already opted in. This is a requirement for adding workflow steps to an app.
Navigate to Event Subscriptions and expand Subscribe to bot events to see that we have subscribed to the function_executed
event. This is also a requirement for adding workflow steps to our app, as it lets our app know when a step has been triggered, allowing our app to respond to it.
Another configuration to note is Socket Mode. We have turned this on for our local development, but socket mode is not intended for use in a production environment. When you are satisfied with your app and ready to deploy it to a production environment, you should switch to using public HTTP request URLs. Read more about getting started with HTTP in Bolt for JavaScript here and Bolt for Python here.
Clicking on Workflow Steps in the left nav will show you that one workflow step has been added! This reflects the function
defined in our manifest; functions are workflow steps. We will get to this step's implementation later.
In order to connect our app here with the logic of our sample code set up locally, we need to gather two tokens, a bot token and an app token.
To generate an app token, navigate to Basic Information and scroll down to App-Level Token.
Click Generate Token and Scopes, then Add Scope and choose connections:write
. Choose a name for your token and click Generate. Copy that value, save it somewhere accessible, and click Done to close out of the modal.
Next up is the bot token. We can only get this token by installing the app into the workspace. Navigate to Install App and click the button to install, choosing Allow at the next screen.
You will then have a bot token. Again, copy that value and save it somewhere accessible.
💡 Treat your tokens like passwords and keep them safe. Your app uses them to post and retrieve information from Slack workspaces. Minimally, do not commit them to version control.
While building your app, you can see your changes appear in your workspace in real-time with npm start
. Soon we'll start our local development server and see what our sample code is all about! But first, we need to store those tokens we gathered as environment variables.
Navigate back to VSCode. Rename the .env.sample
file to .env
. Open this file and update SLACK_APP_TOKEN
and SLACK_BOT_TOKEN
with the values you previously saved. It will look like this, with your actual token values where you see <your_app_token>
and <your_bot_token>
:
SLACK_APP_TOKEN=<your_app_token>
SLACK_BOT_TOKEN=<your_bot_token>
Now save the file and try starting your app:
npm start
python3 app.py
You'll know the local development server is up and running successfully when it emits a bunch of [DEBUG]
statements to your terminal, the last one containing connected:ready
.
With your development server running, continue to the next step.
Note: If you need to stop running the local development server, press <CTRL>
+ c
to end the process.
The starter project you cloned contains a sample custom step lovingly titled “Sample step". Let’s see how a custom step defined in Bolt appears in Workflow Builder.
In the Slack Client of your development workspace, open Workflow Builder by clicking on the workspace name, Tools, then Workflow Builder. Create a new workflow, then select Build Workflow:
Select Choose an event under Start the workflow..., then From a link in Slack to configure this workflow to start when someone clicks its shortcut link:
Click the Continue button to confirm that this is workflow should start with a shortcut link:
Find the sample step provided in the template by either searching for the name of your app (e.g., Bolt Custom Step
) or the name of your step (e.g. Sample step
) in the Steps search bar.
If you search by app name, any custom step that your app has defined will be listed.
Add the “Sample step" in the search results to the workflow:
As soon as you add the “Sample step" to the workflow, a modal will appear to configure the step's input—in this case, a user variable:
Configure the user input to be “Person who used this workflow”, then click the Save button:
Click the Finish Up button, then provide a name and description for your workflow.
Finally, click the Publish button:
Copy the shortcut link, then exit Workflow Builder and paste the link to a message in any channel you’re in:
After you send a message containing the shortcut link, the link will unfurl and you’ll see a Start Workflow button.
Click the Start Workflow button:
You should see a new direct message from your app:
The message from your app asks you to click the Complete step button:
Once you click the button, the direct message to you will be updated to let you know that the step interaction was successfully completed:
Now that we’ve gotten a feel for how we will use the custom step, let’s learn more about how function listeners work.
Now that we’ve seen how custom steps are used in Workflow Builder, let’s understand how the function listener code works to respond to an event when the step is triggered.
We’ll first review the step definition in the manifest.json
, then we’ll look at the two listener functions in our app code: one to let us know when the step starts, and one to let us know when someone clicks or taps one of the buttons we sent over.
Opening up the manifest.json
file included in the sample app shows a functions
property that includes a definition for our sample_step
.
// manifest.json
...
"functions": {
"sample_step": {
"title": "Sample step",
"description": "Runs sample step",
"input_parameters": {
"user_id": {
"type": "slack#/types/user_id",
"title": "User",
"description": "Message recipient",
"is_required": true,
"hint": "Select a user in the workspace",
"name": "user_id"
}
},
"output_parameters": {
"user_id": {
"type": "slack#/types/user_id",
"title": "User",
"description": "User that completed the step",
"is_required": true,
"name": "user_id"
}
}
}
}
From the step definition, we can see an input parameter and an output parameter defined.
The step will take the following input:
|
The step will produce the following output:
|
The first thing we’ll do when adding a custom workflow step to our Bolt app is register a new function listener. In Bolt, a function listener allows developers to execute custom code in response to specific Slack events or actions by registering a method that handles predefined requests or commands. We register a function listener via the function
method provided by our app instance.
Follow along:
app.js
or app.py
file in your code editor.sample_step
registration, you'll see a listener defined for our custom step:// app.js
...
app.function('sample_step', async ({ client, inputs, fail }) => {
try {
const { user_id } = inputs;
await client.chat.postMessage({
channel: user_id,
text: 'Click the button to signal the step has completed',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'Click the button to signal the step has completed',
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Complete step',
},
action_id: 'sample_button',
},
},
],
});
} catch (error) {
console.error(error);
fail({ error: `Failed to handle a step request: ${error}` });
}
});
.function()
listenerThe function listener registration method (.function()
) takes two arguments:
sample_step
. Every custom step you implement in an app needs to have a unique callback ID.sample_step
custom step.The callback function offers various utilities that can be used to take action when a function execution event is received. The ones we’ll be using here are:
client
provides access to Slack API methods — like the chat.postMessage
method, which we’ll use later to send a message to a channelinputs
provides access to the workflow variables passed into the step when the workflow was startedfail
is a utility method for indicating that the step invoked for the current workflow step had an error# app.py
...
@app.function("sample_step")
def handle_sample_step_event(inputs: dict, say: Say, fail: Fail, logger: logging.Logger):
user_id = inputs["user_id"]
try:
say(
channel=user_id, # sending a DM to this user
text="Click the button to signal the step has completed",
blocks=[
{
"type": "section",
"text": {"type": "mrkdwn", "text": "Click the button to signal the step has completed"},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Complete step"},
"action_id": "sample_click",
},
}
],
)
except Exception as e:
logger.exception(e)
fail(f"Failed to handle a step request (error: {e})")
function()
listenerThe function decorator (function()
) accepts an argument of type str
and is the unique callback ID of the step. For our custom step, we’re using sample_step
. Every custom step you implement in an app needs to have a unique callback ID.
The callback function is where we define the logic that will run when Slack tells the app that a user in the Slack client started a workflow that contains the sample_step
custom step.
The callback function offers various utilities that can be used to take action when a step execution event is received. The ones we’ll be using here are:
inputs
provides access to the workflow variables passed into the step when the workflow was startedfail
indicates when the step invoked for the current workflow step has an errorlogger
provides a Python standard logger instancesay
calls the chat.Postmessage
API methodWhen our step is executed, we want a message to be sent to the invoking user. That message should include a button that prompts the user to complete the step.
When Slack tells your Bolt app that the sample_step
step was invoked, this step uses chat.postMessage
to send a message to the user_id
channel (which means this will be sent as a DM to the Slack user whose ID == user_id
) with some text and blocks. The Block Kit element being sent as part of the message is a button, labeled 'Complete step' (which sends the sample_click
action ID).
Once the message is sent, your Bolt app will wait until the user has clicked the button. As soon as they click or tap the button, Slack will send back the action ID associated with the button to your Bolt app.
In order for your Bolt app to listen for these actions, we’ll now define an action listener.
The message we send to the user will include the button prompting them to complete the step.
To listen for and respond to this button click, you'll see an .action()
listener to app.js
, right after the function listener definition:
// app.js
...
app.action('sample_button', async ({ body, client, complete, fail }) => {
const { channel, message, user } = body;
try {
// Steps should be marked as successfully completed using `complete` or
// as having failed using `fail`, else they'll remain in an 'In progress' state.
// Learn more at https://api.slack.com/automation/interactive-messages
await complete({ outputs: { user_id: user.id } });
await client.chat.update({
channel: channel.id,
ts: message.ts,
text: 'Step completed successfully!',
});
} catch (error) {
console.error(error);
fail({ error: `Failed to handle a step request: ${error}` });
}
});
.action()
listenerSimilar to a function listener, the action listener registration method (.action()
) takes two arguments:
Just like the function listener’s callback function, the action listener’s callback function offers various utilities that can be used to take action when an action event is received. The ones we’ll be using here are:
client
, which provides access to Slack API methodsaction
, which provides the action’s event payloadcomplete
, which is a utility method indicating to Slack that the step behind the workflow step that was just invoked has completed successfullyfail
, which is a utility method for indicating that the step invoked for the current workflow step had an errorTo listen for and respond to these button clicks, you'll see an .action()
listener to app.py
, right after the function listener you just defined:
# app.py
...
@app.action("sample_click")
def handle_sample_click(
ack: Ack, body: dict, context: BoltContext, client: WebClient, complete: Complete, fail: Fail, logger: logging.Logger
):
ack()
try:
# Since the button no longer works, we should remove it
client.chat_update(
channel=context.channel_id,
ts=body["message"]["ts"],
text="Congrats! You clicked the button",
)
# Signal that the step completed successfully
complete({"user_id": context.actor_user_id})
except Exception as e:
logger.exception(e)
fail(f"Failed to handle a step request (error: {e})")
.action()
listenerSimilar to a function listener, the action listener registration method (.action()
) takes a single argument: the unique callback ID of the action that your app will respond to.
The callback function argument is where we define the logic that will run when Slack tells our app that the user has clicked or tapped the button.
Just like the function listener’s callback function, the action listener’s callback function offers various utilities that can be used to take action when an action event is received. The ones we’ll be using here are:
ack
returns acknowledgement to the Slack serversaction
provides the action’s event payloadbody
returns the parsed request body dataclient
provides access to Slack API methodscomplete
indicates to Slack that the step behind the invoked workflow step has completed successfullyfail
indicates when the step invoked for the current workflow step has an errorlogger
provides a Python standard logger instanceRecall that we sent over a message with the button back in the function listener.
When the button is pressed, we want to complete the step, update the message, and define outputs
that can be used for subsequent steps in Workflow Builder.
Slack will send an action event payload to your app when the button is clicked or tapped. In the action listener, we extract all the information we can use, and if all goes well, let Slack know the step was successful by invoking complete
. We also handle cases where something goes wrong and produces an error.
That's it for this tutorial — we hope you learned a lot!
Let's recap what we did:
In this tutorial, we added custom steps via the manifest, but if you'd like to see how to add custom steps in the app settings to an existing app, follow along with the Create a custom step for Workflow Builder: existing Bolt app tutorial.
If you're interested in exploring how to create custom steps to use in Workflow Builder as steps with our Deno Slack SDK, too, a similar tutorial can be found here.