Developing automations requires a paid plan. Don't have a paid plan? Join the Developer Program and provision a sandbox with access to all Slack features for free.
Custom functions are how you define custom Workflow Builder steps.
In this tutorial, you'll use the Bolt for Python SDK to develop a custom function, then wire it up as a workflow step in Workflow Builder.
When finished, you'll be ready to build scalable and innovative custom functions for anyone using Workflow Builder in your workspace.
In this tutorial, you'll be writing a custom function called Request Time Off. You'll add this custom function as a step in Workflow Builder, then test it out in your development workspace. 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 Python function sample
as a template. The sample custom function 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.
To complete this tutorial, you first need to install and configure the Slack CLI. Step-by-step instructions can be found in our Quickstart Guide.
Run slack update
to see if you have the latest version. If you don’t, you’ll be prompted to update. Go ahead and do that, then re-run slack update
. You should see the following:
You are using the latest Slack CLI and SDK
Check if your CLI is authenticated in your development workspace by running slack auth list
. If you’ve previously authenticated into your workspace, you should see it listed in the terminal output.
If you haven’t authenticated into your development workspace yet, run slack auth login
, and follow the instructions. You can also follow the instructions here.
For this tutorial, We'll use boltfunc
as the app name. For your app, be sure to use a unique name that will be easy for you to find in Workflow Builder, then use that name wherever you see boltfunc
in this tutorial.
Let's start by opening a terminal and cloning the starter template repository:
slack create boltfunc -t slack-samples/bolt-python-custom-function-template
When the CLI is finished cloning the template, change directories into your newly prepared app project:
cd boltfunc
You'll also want to set up your virtual environment and install the required dependecies within it:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
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
+ ~
While building your app, you can see your changes appear in your workspace in real-time with slack run
. The CLI will prompt you to select a workspace to install the app in during app creation. You'll know an app is the development version if its name has the string (local)
appended to it.
Try starting your development server now:
$ slack run
Since this is the first time you’re starting the development server for this app, you’ll be prompted to choose a local environment. Select the option to install to a new team, then select your development workspace.
$ slack run
? Choose a local environment
❱ Install to a new team
Once you select your development workspace, the CLI will do some work behind the scenes to get your app ready for local development.
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 function lovingly entitled “Sample function”. We’re going to build our own custom function later, but first, let’s see how a custom function defined in Bolt appears in Workflow Builder.
Open Workflow Builder in your development workspace, and create a new workflow:
Select "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 function provided in the template by searching for the name of your app (e.g., boltfunc
) in the Steps search bar.
Any custom function that your app has defined will be listed.
Add the “Sample function” in the search results to the workflow:
As soon as you add the “Sample function” 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 function] button:
Once you click the button, the direct message to you will be updated to let you know that the function interaction was successfully completed:
Now that we’ve gotten a feel for how we will use the custom function we’re writing in this tutorial, let’s go ahead and get started with actually defining our custom function.
Now that we’ve seen how custom functions are used in Workflow Builder, let’s write one from scratch with the Bolt SDK for Python.
We’ll first describe our function so we know what we’re going to build, then we’ll implement two listener functions in our app code: one to let us know when the function starts, and one to let us know when someone clicks or taps one of the buttons we sent over.
We're going to write a custom function called Request Time Off.
The function will take the following inputs:
|
The function will produce the following outputs:
|
With a clear path forward, let’s head to our code editor and begin building our custom function.
The first thing we’ll do when adding a custom function 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.py
file in your code editor.sample_function
registration, define a new listener for our custom function:@app.function("request_time_off")
def handle_request_time_off(inputs: dict, fail: Fail, logger: logging.Logger, say: Say):
# We'll implement this function soon...
Ignore any errors being reported right now, as they’ll go away once we implement this function.
Before we do that, let’s take a closer look at what’s going on when we register a function listener.
function()
listenerThe function decorator (function()
) accepts an argument of type str
and is the unique callback ID of the function. For our custom function, we’re using request_time_off
. Every custom function 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 function.
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 function when the workflow was startedfail
indicates when the function invoked for the current workflow step has an errorlogger
provides a Python standard logger instancesay
calls the chat.Postmessage
API methodWith that context, let’s implement the listener’s logic.
When our function is executed, we want a message to be sent to the approving manager. That message should include information about the request, as well as two buttons that indicate their decision (approve or deny).
Fill out your function listener’s logic with the code below:
@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})")
When Slack tells your Bolt app that the request_time_off
function was invoked, this function uses say
to call chat.postMessage
, which then sends a message to the manager_id
channel (which means this will be sent as a DM to the Slack user whose ID is manager_id
) with some text and blocks. The Block Kit elements being sent as part of the message are two buttons, one labeled ‘Approve’ (which sends the approve_button
action ID) and the other labelled ‘Deny’ (which sends the deny_button
action ID).
Once the message is sent, your Bolt app will wait until the manager has made their decision. As soon as they click or tap one of the buttons, Slack will send back the action ID associated with the button that was pressed 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 approving manager above will include two buttons that indicate their approval or denial of the request.
To listen for and respond to these button clicks, we'll add an .action()
listener to app.py
, right after the function listener you just defined:
@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):
# Your code will go here
Let’s now take a closer look at what’s going on when we register an action listener.
.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 function behind the invoked workflow step has completed successfullyfail
indicates when the function invoked for the current workflow step has an errorlogger
provides a Python standard logger instanceLet's go ahead and define the callback logic for our action listener.
Recall that we sent over a message with two buttons back in the function listener.
When one of the buttons is pressed, we want to complete the function, update the message, and define outputs
that can be used for subsequent steps in Workflow Builder.
Fill out your action listener’s logic with the code below:
@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})")
Slack 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 function was successful by invoking complete
. We’ll also handle cases where something goes wrong and produces an error.
With the listeners and their callbacks all set up, it’s time to turn to our manifest.
An app’s manifest (manifest.json
) contains the blueprint of the application, and, in the world of functions, helps Slack understand what it offers for use in Workflow Builder.
Since we’ve added a new function to our app, we want to make sure it’s accounted for in the manifest, otherwise Slack won’t know that we want to make it available for use in Workflow Builder. To do that, we’ll add a new function definition to our manifest.
In your manifest.json
file, add the following function definition for request_time_off
to the functions
key as you see below:
// ... more manifest above
"functions": {
"request_time_off": {
"title": "Request time off",
"description": "Submit a request to take time off",
"input_parameters": {
// Function inputs will go here
},
"output_parameters": {
// Function outputs will go here
}
}
}
// ...
This definition tells Slack that the function in our workspace with the callback ID of request_time_off
belongs to our app, and that when it runs, we want to receive information about its execution event.
Slack expects functions to have input_parameters
and output_parameters
, so we’ll define those here in the manifest next.
Let’s think back to what we’re trying to accomplish with this function:
inputs
.outputs
.Function inputs and outputs (input_parameters
and output_parameters
in manifest-speak) define what information goes into a function before it runs and what comes out of a function after it completes, respectively.
Using the same variables that we’ve used in our code, let’s now add the following input_parameters
and output_parameters
to the request_time_off
function definition:
// ... more manifest above
"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"
}
}
}
}
// ...
As you can see, both inputs and outputs adhere to the same schema (the shape of it, or expected properties), and consist of a unique identifier and an object that describes the input or output.
This next part is important!
Since you’ve changed your manifest, go ahead and restart your development server:
Ctrl
+ c
slack run
Manifest changes will only be reflected after you stop and restart your development server.
With the manifest changes in place, navigate back to Workflow Builder, and search for your app name again (just like we did before), and you should see the new Request time off function that we’ve been building:
What would you like to happen when the request is approved or denied? Add the decision to a spreadsheet? Send a message to the submitting user?
The possibilities are endless! Check out which steps you have available for immediate use in Workflow Builder, and if you can’t find what you’re looking for, you’re now well-equipped to create a custom step to make it a reality.
Here's one possibility:
Now that you’ve got a working function, it’s time to deploy it to a production environment. Doing this will enable others to use your function as long as they have permissions.
To use the CLI to deploy to infrastructure of your choice, edit the slack.json
file to include a deploy
hook:
{
"hooks": {
"get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks",
"deploy": ""
}
}
For the value of deploy
, use the script needed to run that will see to the appropriate build and deployment process suited to your needs.
To run this script, from the CLI, you can use the deploy
command:
slack deploy
The above will run the script you've included for "deploy"
in slack.json
.
If you're not sure where to host your app, consider following our guide for deploying Bolt apps to Heroku.
That's it for this tutorial — we hope you learned a lot!
Let's recap what we did:
If you're interested in exploring how to create custom functions to use in Workflow Builder as steps with our Deno Slack SDK, too, a similar tutorial can be found here.