đ Executive Summary
TL;DR: Manual approval processes are often slow, error-prone, and expensive with rigid SaaS solutions. This guide demonstrates building a flexible, custom approval workflow using Slack buttons and Python, integrating directly into team communication for rapid, cost-effective decision-making.
đŻ Key Takeaways
- A custom approval workflow leverages Slackâs Interactivity & Shortcuts, requiring a publicly accessible endpoint (e.g., ngrok, AWS Lambda) for Slack to send interaction payloads.
- Security is paramount, implemented via Slackâs Signing Secret and HMAC-SHA256 verification in the Python Flask backend to ensure incoming requests are legitimate.
- Slackâs Block Kit is used to construct interactive messages with âApproveâ and âDenyâ buttons, and the
slack\_client.chat\_updatemethod modifies the original message to reflect decisions, providing an audit trail.
Build a Custom Approval Workflow using Slack Buttons and Python
As DevOps engineers, we constantly seek ways to streamline processes, eliminate manual toil, and integrate our toolchains for maximum efficiency. One common bottleneck in many organizations is the approval process. Whether itâs for deploying code, granting access, or provisioning resources, manual approvals can be slow, prone to human error, and disrupt the fast-paced development cycle. Existing SaaS solutions often come with a hefty price tag or offer rigid workflows that donât quite fit your unique organizational needs.
What if you could build a flexible, custom approval system right where your team already communicates? In this tutorial, weâll guide you through building a custom approval workflow using Slack buttons and Python. This approach leverages Slackâs interactivity features to provide a rapid, integrated, and cost-effective solution for your approval needs, giving you full control over the logic and integration points.
Prerequisites
Before we dive into the build, ensure you have the following:
- Python 3.8+: Our backend logic will be written in Python.
- pip: Pythonâs package installer.
- A Slack Workspace: With administrative access to create and manage Slack Apps.
- A Publicly Accessible Endpoint: Slack needs to send interaction payloads to your server. For local development, ngrok is an excellent choice. For production, consider a cloud-based solution like AWS Lambda, Google Cloud Run, or a dedicated VM.
- Basic understanding of webhooks and API interactions: Familiarity with HTTP methods (GET, POST) and JSON payloads will be beneficial.
Step-by-Step Guide
Our workflow will involve three main components:
- A Slack App to handle interactivity.
- A Python Flask server to receive and process Slack interactions.
- A Python script to initiate the approval request by sending a message with buttons to Slack.
Step 1: Set up Your Slack App
The first step is to create and configure a Slack App to facilitate communication and interactivity.
- Navigate to api.slack.com/apps and click âCreate New Appâ. Choose âFrom scratchâ, give it a name (e.g., âApproval Botâ), and select your workspace.
-
Enable Interactivity & Shortcuts:
- In the left sidebar, click âInteractivity & Shortcutsâ.
- Toggle âInteractivityâ to On.
- For the âRequest URLâ, enter your publicly accessible endpoint followed by
/slack/events. If using ngrok, it might look likehttps://<your-ngrok-id>.ngrok.io/slack/events. Weâll set up this endpoint in our Python server later. - Click âSave Changesâ.
-
Set OAuth & Permissions:
- In the left sidebar, click âOAuth & Permissionsâ.
- Under âScopesâ > âBot Token Scopesâ, add the following scopes:
-
chat:write: To send messages. -
commands: If you plan to use Slack slash commands (optional for this specific tutorial, but often useful). -
app_mentions:read: To read messages where your bot is mentioned.
-
- Click âSave Changesâ.
-
Install the App to Your Workspace:
- Scroll to the top of the âOAuth & Permissionsâ page.
- Click âInstall to Workspaceâ. Follow the prompts to authorize the app.
- Once installed, youâll see a âBot User OAuth Tokenâ (starts with
xoxb-). Copy this token; youâll need it for your Python script to send messages.
-
Get Your Signing Secret:
- In the left sidebar, click âBasic Informationâ.
- Scroll down to âApp Credentialsâ. Copy your âSigning Secretâ. This is crucial for verifying that incoming requests to your server genuinely originate from Slack.
Step 2: Develop the Python Backend for Interactive Messages
Now, letâs create a Python Flask application that listens for interactive payloads from Slack and processes the button clicks. Install the necessary libraries:
pip install Flask requests python-dotenv slack_sdk
Create a file named app.py:
import os
import json
import hmac
import hashlib
from flask import Flask, request, jsonify, abort
from dotenv import load_dotenv
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
load_dotenv() # Load environment variables from .env file
app = Flask(__name__)
# Load Slack credentials from environment variables
SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
if not SLACK_SIGNING_SECRET or not SLACK_BOT_TOKEN:
print("ERROR: Missing SLACK_SIGNING_SECRET or SLACK_BOT_TOKEN environment variables.")
exit(1)
slack_client = WebClient(token=SLACK_BOT_TOKEN)
# Function to verify Slack requests
def verify_slack_request(req):
timestamp = req.headers.get("X-Slack-Request-Timestamp")
if not timestamp:
return False
req_body = req.get_data().decode('utf-8')
sig_basestring = f"v0:{timestamp}:{req_body}"
my_signature = "v0=" + hmac.new(
SLACK_SIGNING_SECRET.encode("utf-8"),
sig_basestring.encode("utf-8"),
hashlib.sha256
).hexdigest()
slack_signature = req.headers.get("X-Slack-Signature")
return hmac.compare_digest(my_signature, slack_signature)
@app.route("/slack/events", methods=["POST"])
def slack_events():
# Verify the request originates from Slack
if not verify_slack_request(request):
abort(403) # Forbidden
payload = request.form.get("payload")
if not payload:
abort(400) # Bad Request
data = json.loads(payload)
# Handle URL Verification Challenge
if data.get("type") == "url_verification":
return jsonify({"challenge": data.get("challenge")})
# Process block_actions (button clicks)
if data.get("type") == "block_actions":
action = data["actions"][0]
action_id = action.get("action_id")
user_id = data["user"]["id"]
channel_id = data["channel"]["id"]
message_ts = data["message"]["ts"] # Timestamp of the original message
print(f"User {user_id} clicked {action_id} in channel {channel_id}")
response_text = ""
if action_id == "approve_button":
response_text = f"Approval granted by <@{user_id}>!"
# Add your approval logic here (e.g., call another API, update a database)
print("Approval logic triggered!")
elif action_id == "deny_button":
response_text = f"Approval denied by <@{user_id}>."
# Add your denial logic here
print("Denial logic triggered!")
else:
response_text = "Unknown action."
# Update the original message to reflect the decision
try:
slack_client.chat_update(
channel=channel_id,
ts=message_ts,
text=response_text,
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": response_text
}
}
]
)
except SlackApiError as e:
print(f"Error updating Slack message: {e.response['error']}")
return "", 200 # Slack expects an empty 200 OK response quickly
return "", 200 # Acknowledge other types of events
if __name__ == "__main__":
app.run(port=5000)
Create a .env file in the same directory as app.py with your Slack credentials:
SLACK_SIGNING_SECRET=your_slack_signing_secret_here
SLACK_BOT_TOKEN=xoxb-your_bot_user_oauth_token_here
Explanation:
- We use Flask to create a simple web server.
- The
/slack/eventsroute is configured to receive POST requests from Slack. - The
verify_slack_requestfunction is critical for security, ensuring that incoming requests are legitimate Slack events by checking the signature header. - We parse the payload to identify
block_actions, which are triggered by button clicks. - Based on the
action_id(approve_buttonordeny_button), we can implement our specific approval or denial logic. - After processing, we use
slack_client.chat_updateto modify the original Slack message, showing who made the decision and what the outcome was. This prevents multiple users from acting on the same request and provides a clear audit trail.
Run your Flask app:
python app.py
If youâre developing locally, start ngrok to expose your Flask app to the internet:
ngrok http 5000
Make sure the ngrok URL (e.g., https://<your-ngrok-id>.ngrok.io/slack/events) matches the Request URL you configured in your Slack App settings.
Step 3: Sending the Approval Request to Slack
Now letâs create a script that initiates an approval request by sending an interactive message with buttons to a specified Slack channel. Create a file named send_approval.py:
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from dotenv import load_dotenv
load_dotenv()
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
TARGET_CHANNEL = "#devops-approvals" # Replace with your target Slack channel ID or name
if not SLACK_BOT_TOKEN:
print("ERROR: Missing SLACK_BOT_TOKEN environment variable.")
exit(1)
slack_client = WebClient(token=SLACK_BOT_TOKEN)
def send_approval_request(
requester_name: str,
request_details: str,
channel: str = TARGET_CHANNEL
):
try:
response = slack_client.chat_postMessage(
channel=channel,
text=f"New approval request from {requester_name}",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"
### Approval Request from {requester_name}
\n\n{request_details}"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Approve"
},
"style": "primary",
"value": "approved", # This value can be used if you don't use action_id
"action_id": "approve_button" # Unique ID for this action
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Deny"
},
"style": "danger",
"value": "denied",
"action_id": "deny_button"
}
]
}
]
)
print(f"Approval request sent to {channel}. Message timestamp: {response['ts']}")
return response
except SlackApiError as e:
print(f"Error sending message to Slack: {e.response['error']}")
return None
if __name__ == "__main__":
send_approval_request(
requester_name="John Doe",
request_details="Please approve the deployment of feature X to production."
)
Explanation:
- This script uses the
slack_sdklibrary to interact with the Slack Web API. - We construct a message using Slackâs Block Kit, specifically using
sectionblocks for text andactionsblocks for buttons. - Each button has an
action_id(e.g.,approve_button,deny_button) which our Flask backend will use to identify the action taken. - The
chat_postMessagemethod sends this interactive message to the specifiedTARGET_CHANNEL. Make sure your bot is a member of this channel.
Run this script to send an approval request to Slack:
python send_approval.py
You should see a new message in your Slack channel with âApproveâ and âDenyâ buttons.
Step 4: Process the Approval Decision and Trigger Actions
This step primarily happens within your app.py from Step 2. When a user clicks a button in Slack, the /slack/events endpoint is hit. Our Flask app then:
- Verifies the requestâs authenticity using the signing secret.
- Parses the interaction payload to determine which button was clicked (
approve_buttonordeny_button). - Executes custom logic based on the decision. This is where you would integrate with other systems.
- Updates the original Slack message to confirm the action, providing real-time feedback and an audit trail.
Example Integration (within app.py):
Inside the if action_id == "approve_button": or if action_id == "deny_button": blocks, you can add code to:
- Call an external API: Trigger a CI/CD pipeline, provision resources in a cloud provider, or update a ticket in an ITSM system.
- Update a database: Log the approval decision, who made it, and when.
- Send further notifications: Alert other teams or systems about the outcome.
For instance, an approval might trigger a Jenkins job or an AWS Lambda function:
# Inside app.py, under the 'if action_id == "approve_button":' block
import requests
# ... existing code ...
if action_id == "approve_button":
response_text = f"Approval granted by <@{user_id}>! Initiating deployment..."
try:
# Example: Triggering a Jenkins job (replace with your actual API call)
jenkins_url = "http://your-jenkins-server/job/your-deployment-job/buildWithParameters"
jenkins_params = {"PARAM1": "value", "MESSAGE_TS": message_ts} # Pass relevant info
jenkins_headers = {"Authorization": "Basic BASE64_ENCODED_USER:API_TOKEN"} # Use env vars for credentials
jenkins_response = requests.post(jenkins_url, params=jenkins_params, headers=jenkins_headers)
jenkins_response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
print(f"Jenkins job triggered successfully: {jenkins_response.status_code}")
response_text += "\n_Deployment initiated successfully._"
except requests.exceptions.RequestException as e:
print(f"Error triggering Jenkins job: {e}")
response_text += "\n_Failed to initiate deployment._"
elif action_id == "deny_button":
response_text = f"Approval denied by <@{user_id}>. Deployment cancelled."
# Logic to cancel a pending action, if applicable
print("Denial logic triggered!")
# ... rest of app.py
Remember to handle credentials securely using environment variables for any external API calls.
Common Pitfalls
When building custom integrations, you might encounter a few common issues:
-
Slack Signing Secret Verification Errors (403 Forbidden): This is often due to an incorrect
SLACK_SIGNING_SECRETin your.envfile, or an issue with the signature verification logic itself. Double-check your secret and ensure the timestamp and request body are handled precisely as Slack expects. -
Missing OAuth Scopes: If your bot isnât performing actions (e.g., sending messages) as expected, verify that youâve granted all necessary OAuth Bot Token Scopes (like
chat:write,app_mentions:read) in your Slack App settings and reinstalled the app to apply changes. - Public Endpoint Accessibility: Slack needs to be able to reach your Request URL. If your server isnât publicly accessible (e.g., behind a firewall without proper port forwarding, or ngrok isnât running), Slack wonât be able to send payloads, leading to timeouts or errors.
- Rate Limiting: Slack APIs have rate limits. If your backend makes many rapid calls, you might hit these limits. Implement exponential backoff for retries to handle this gracefully. For approval workflows, this is less common unless youâre updating many messages or channels simultaneously.
Conclusion
By following this tutorial, youâve learned how to build a powerful and flexible custom approval workflow using Slack buttons and Python. This approach empowers your team to make quick decisions directly within their communication hub, reducing delays and enhancing productivity. You now have a solid foundation to:
- Integrate with your CI/CD pipelines for deployment approvals.
- Manage access requests for sensitive systems.
- Approve infrastructure changes or resource provisioning.
- Create custom workflows tailored to your organizationâs unique processes.
The beauty of this solution lies in its adaptability. You can extend the Python backend to interact with virtually any API or system, turning Slack into a central command center for your operational approvals. Explore Slackâs rich API documentation and Block Kit to further customize your messages and interactive elements, unlocking even more possibilities for automation and efficiency.
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)