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.
If you followed along with our Create a custom step for Workflow Builder: new Bolt app tutorial, you have seen how to add custom steps to a brand new app. But what if you have an app up and running currently to which you'd like to add custom steps? You've come to the right place!
In this tutorial we will:
The custom steps feature is compatible with Bolt version 1.20.0 and above.
First update your package.json
file to reflect version 1.20.0 of Bolt, then run the following command in your terminal:
npm install
First update your requirements.txt
file to reflect version 1.20.0 of Bolt, then run the following commands in your terminal:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
In order to add custom workflow steps to an app, the app needs to be org-ready. To do this, navigate to your app settings page and select your Bolt app.
Navigate to Org Level Apps in the left nav and click Opt-In, then confirm Yes, Opt-In.
function_executed
event subscriptionIn order for our app to know when a workflow step is executed, it needs to listen for the function_executed
event.
Navigate to App Manifest in the left nav and add the function_executed
event subscription, then click Save Changes:
...
"settings": {
"event_subscriptions": {
"bot_events": [
...
"function_executed"
]
},
}
Now we're ready to add a workflow step!
Navigate to Workflow Steps in the left nav and click Add Step. This is where we'll configure our step's inputs, outputs, name, and description.
For illustration purposes in this tutorial, we're going to write a custom step called Request Time Off. When the step is invoked, a message will be sent to the provided manager with an option to approve or deny the time-off request. When the manager takes an action (approves or denies the request), a message is posted with the decision and the manager who made the decision. The step will take two user IDs as inputs, representing the requesting user and their manager, and it will output both of those user IDs as well as the decision made.
Add the pertinent details to the step:
Remember this callback_id
. We will use this later when implementing a function listener. Then add the input and output parameters:
And save your changes.
Navigate to App Manifest and notice your new step reflected in the functions
property! Exciting. It should look like this:
"functions": {
"request_time_off": {
"title": "Request time off",
"description": "Submit a request to take time off",
"input_parameters": {
"manager_id": {
"type": "slack#/types/user_id",
"title": "Manager",
"description": "Approving manager",
"is_required": true,
"hint": "Select a user in the workspace",
"name": "manager_id"
},
"submitter_id": {
"type": "slack#/types/user_id",
"title": "Submitting user",
"description": "User that submitted the request",
"is_required": true,
"name": "submitter_id"
}
},
"output_parameters": {
"manager_id": {
"type": "slack#/types/user_id",
"title": "Manager",
"description": "Approving manager",
"is_required": true,
"name": "manager_id"
},
"request_decision": {
"type": "boolean",
"title": "Request decision",
"description": "Decision to the request for time off",
"is_required": true,
"name": "request_decision"
},
"submitter_id": {
"type": "slack#/types/user_id",
"title": "Submitting user",
"description": "User that submitted the request",
"is_required": true,
"name": "submitter_id"
}
}
}
}
Next up, we'll define a function listener to handle what happens when the workflow step is used.
Direct your attention back to your app project in VSCode or your preferred code editor. Here we'll add logic that your app will execute when the custom step is executed.
Open your app.js
file and add the following function listener code for the request_time_off
step.
app.function('request_time_off', async ({ client, inputs, fail }) => {
try {
const { manager_id, submitter_id } = inputs;
await client.chat.postMessage({
channel: manager_id,
text: `<@${submitter_id}> requested time off! What say you?`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `<@${submitter_id}> requested time off! What say you?`,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Approve',
emoji: true,
},
value: 'approve',
action_id: 'approve_button',
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'Deny',
emoji: true,
},
value: 'deny',
action_id: 'deny_button',
},
],
},
],
});
} catch (error) {
console.error(error);
fail({ error: `Failed to handle a function request: ${error}` });
}
});
.function()
listenerThe function listener registration method (.function()
) takes two arguments:
request_time_off
. Every custom step you implement in an app needs to have a unique callback ID.request_time_off
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 errorOpen your app.py
file and add the following function listener code for the request_time_off
step.
@app.function("request_time_off")
def handle_request_time_off(inputs: dict, fail: Fail, logger: logging.Logger, say: Say):
submitter_id = inputs["submitter_id"]
manager_id = inputs["manager_id"]
try:
say(
channel=manager_id,
text=f"<@{submitter_id}> requested time off! What say you?",
blocks=[
{
"type": 'section',
"text": {
"type": 'mrkdwn',
"text": f"<@{submitter_id}> requested time off! What say you?",
},
},
{
'type': 'actions',
'elements': [
{
'type': 'button',
'text': {
'type': 'plain_text',
'text': 'Approve',
'emoji': True,
},
'value': 'approve',
'action_id': 'approve_button',
},
{
'type': 'button',
'text': {
'type': 'plain_text',
'text': 'Deny',
'emoji': True,
},
'value': 'deny',
'action_id': 'deny_button',
},
],
},
],
)
except Exception as e:
logger.exception(e)
fail(f"Failed to handle a function 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 request_time_off
. 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 request_time_off
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:
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 methodThis custom step also requires an action listener to respond to the action of a user clicking a button.
In that same app.js
file, add the following action listener:
app.action(/^(approve_button|deny_button).*/, async ({ action, body, client, complete, fail }) => {
const { channel, message, function_data: { inputs } } = body;
const { manager_id, submitter_id } = inputs;
const request_decision = action.value === 'approve';
try {
await complete({ outputs: { manager_id, submitter_id, request_decision } });
await client.chat.update({
channel: channel.id,
ts: message.ts,
text: `Request ${request_decision ? 'approved' : 'denied'}!`,
});
} catch (error) {
console.error(error);
fail({ error: `Failed to handle a function request: ${error}` });
}
});
.action()
listenerSimilar to a function listener, the action listener registration method (.action()
) takes two arguments:
approve_button
and deny_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:
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 errorIn that same app.py
file, add the following action listener:
@app.action(re.compile("(approve_button|deny_button)"))
def manager_resp_handler(ack: Ack, action, body: dict, client: WebClient, complete: Complete, fail: Fail, logger: logging.Logger):
ack()
try:
inputs = body['function_data']['inputs']
manager_id = inputs['manager_id']
submitter_id = inputs['submitter_id']
request_decision = action['value']
client.chat_update(
channel=body['channel']['id'],
message=body['message'],
ts=body["message"]["ts"],
text=f'Request {"approved" if request_decision == 'approve' else "denied"}!'
)
complete({
'manager_id': manager_id,
'submitter_id': submitter_id,
'request_decision': request_decision == 'approve'
})
except Exception as e:
logger.exception(e)
fail(f"Failed to handle a function 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.
In our case, because we want to execute the same logic for both buttons, we’re using a little bit of RegEx magic to listen for two callback IDs at the same time — approve_button
and deny_button
.
The callback function argument is where we define the logic that will run when Slack tells our app that the manager has clicked or tapped the Approve button or the Deny 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 instanceSlack will send an action event payload to your app when one of the buttons is clicked or tapped. In the action listener, we’ll extract all the information we can use, and if all goes well, let Slack know the step was successful by invoking complete. We’ll also handle cases where something goes wrong and produces an error.
Now that the custom step has been added to the app and we've defined step and action listeners for it, we're ready to see the step in action in Workflow Builder. Go ahead and run your app to pick up the changes.
Turn your attention to the Slack client where your app is installed.
Open Workflow Builder by clicking on the workspace name, then Tools, then Workflow Builder.
Click the button to create a New Workflow, then Build Workflow. Choose to start your workflow from a link in Slack.
In the Steps pane to the right, search for your app name and locate the Request time off step we created.
Select the step and choose the desired inputs and click Save.
Next, click Finish Up, give your workflow a name and description, then click Publish. Copy the link for your workflow on the next screen, then click Done.
In any channel where your app is installed, paste the link you copied and send it as a message. The link will unfurl into a button to start the workflow. Click the button to start the workflow. If you set yourself up as the manager, you will then see a message from your app!
Pressing either button will return a confirmation or denial of your time off request. Nice work!
Now that you've added a workflow step to your Bolt app, a world of possibilities is open to you! Create and share workflow steps across your organization to optimize Slack users' time and make their working lives more productive. Let's recap what we did here:
If you're looking to create a brand new Bolt app with custom workflow steps, check out the tutorial here.
If you're interested in exploring how to create custom Workflow Builder steps with our Deno Slack SDK, too, a tutorial can be found here.