đ Executive Summary
TL;DR: DevOps engineers can automate the tedious, error-prone process of manually collating daily standup updates from Slack into Confluence. This guide provides a Python-based solution to programmatically fetch Slack messages and publish them as a structured Confluence page, saving time and ensuring consistency.
đŻ Key Takeaways
- Slack App configuration requires specific bot token scopes like
channels:history,channels:read, andusers:readto fetch messages and user information. - Confluence API authentication uses an Atlassian API token and username (email), along with the Confluence URL, Space Key, and an optional Parent Page ID for page creation/updates.
- Content published to Confluence must be formatted in Confluence Storage Format (XHTML) for proper rendering, requiring careful handling of HTML tags and special characters.
Daily Standup Automation: Collecting Updates from Slack to Confluence
As DevOps engineers and system administrators, we are constantly seeking ways to streamline operations, eliminate repetitive tasks, and free up valuable time for more strategic work. One such common, yet often manual, chore is the daily standup update. Teams collaborate asynchronously on Slack, providing their âyesterday, today, blockersâ updates, and someone then typically has the unenviable task of collating these into a more formal record, often in Confluence.
This manual copy-pasting is not just boring; it is error-prone, inconsistent, and a significant time sink. Furthermore, relying on expensive, specialized SaaS solutions for a relatively straightforward data aggregation task might be overkill for many organizations. This tutorial from TechResolve will guide you through building a custom automation solution to collect daily standup updates directly from Slack and publish them to a Confluence page, saving you time, ensuring consistency, and keeping your team informed without the manual overhead.
By the end of this guide, you will have a working Python script that can be scheduled to run daily, transforming raw Slack messages into a structured Confluence page. Letâs get started!
Prerequisites
Before diving into the automation, ensure you have the following in place:
- Slack Workspace: Administrator access to create a Slack App and manage permissions.
- Confluence Cloud Instance: Administrator access to generate API tokens and create/update pages.
- Python 3.8+: The primary scripting language for our automation.
- pip: Pythonâs package installer, usually bundled with Python.
- Basic understanding of REST APIs: Familiarity with HTTP methods (GET, POST, PUT) and JSON data structures.
-
A dedicated Slack Channel: For daily standup updates (e.g.,
#daily-standups).
Step-by-Step Guide: Automating Your Daily Standup
Step 1: Configure Your Slack App and Permissions
First, we need to create a Slack App that can read messages from your chosen standup channel.
- Navigate to the Slack API App management page and click âCreate New Appâ.
- Choose âFrom scratchâ, give your app a name (e.g., âStandup Botâ), and select your workspace.
- Once created, go to âFeaturesâ > âOAuth & Permissionsâ in the left sidebar.
- Under âScopesâ, find âBot Token Scopesâ and add the following:
-
channels:history: To read messages from public channels the bot is in. -
channels:read: To view basic information about public channels. -
users:read: To read basic information about users in the workspace (for displaying author names). -
chat:write(optional): If you want the bot to post confirmation messages.
-
- Scroll up and click âInstall to Workspaceâ. Grant the necessary permissions.
- After installation, copy the âBot User OAuth Tokenâ (starts with
xoxb-). Keep this token secure; it will be used in your Python script. - Finally, invite your new bot to your daily standup channel using the command
/invite @YourBotNamein Slack.
Logic Explained: The Bot User OAuth Token is your appâs credential to interact with the Slack API. The scopes define what actions your app is allowed to perform (e.g., read channel history, identify users). Without these, the script wonât be able to fetch updates.
Step 2: Generate Confluence API Token and Identify Target Page
Next, we need credentials to interact with your Confluence Cloud instance.
- Log in to your Atlassian account (the one associated with your Confluence Cloud).
- Go to Atlassian API tokens.
- Click âCreate API tokenâ, give it a label (e.g., âStandup Automationâ), and copy the generated token. This token, along with your Atlassian email, will serve as your Confluence API credentials.
- Identify your Confluence Cloud URL (e.g.,
https://yourcompany.atlassian.net/wiki). - Determine the Confluence Space Key where you want the updates to be published. This can usually be found in the URL when viewing a page within that space (e.g.,
/wiki/spaces/SPACEKEY/pages/...). - (Optional) If you want to create the daily update page as a child of an existing page, note the âPage IDâ of the parent page. You can find this in the URL when viewing the page:
/wiki/spaces/SPACEKEY/pages/123456789/...
Logic Explained: The Confluence API Token is a secure way to authenticate programmatic access to your Confluence instance without using your actual password. The Space Key and (optional) Parent Page ID tell the API exactly where to create or update the standup summary page.
Step 3: Develop the Python Automation Script
Now, letâs write the Python script that orchestrates the data flow. Create a file named standup_automation.py.
First, install the necessary libraries:
pip install requests slack_sdk
Hereâs the Python script:
import os
import requests
import json
from datetime import datetime, timedelta
# --- Configuration (Set these as environment variables for production!) ---
SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN", "xoxb-YOUR-SLACK-BOT-TOKEN")
SLACK_CHANNEL_ID = os.getenv("SLACK_CHANNEL_ID", "C01ABCDEFGH") # Get this from Slack channel details
CONFLUENCE_URL = os.getenv("CONFLUENCE_URL", "https://yourcompany.atlassian.net/wiki")
CONFLUENCE_USERNAME = os.getenv("CONFLUENCE_USERNAME", "your.email@example.com") # Your Atlassian email
CONFLUENCE_API_TOKEN = os.getenv("CONFLUENCE_API_TOKEN", "YOUR-CONFLUENCE-API-TOKEN")
CONFLUENCE_SPACE_KEY = os.getenv("CONFLUENCE_SPACE_KEY", "DEV") # Your Confluence space key
CONFLUENCE_PARENT_PAGE_ID = os.getenv("CONFLUENCE_PARENT_PAGE_ID", None) # Optional: Parent page ID
# --- Slack API Functions ---
def get_slack_channel_history(channel_id, token, oldest_ts):
"""Fetches messages from a Slack channel since a specific timestamp."""
slack_api_url = "https://slack.com/api/conversations.history"
headers = {"Authorization": f"Bearer {token}"}
params = {
"channel": channel_id,
"oldest": oldest_ts,
"limit": 200 # Adjust as needed
}
response = requests.get(slack_api_url, headers=headers, params=params)
response.raise_for_status() # Raise an exception for HTTP errors
return response.json()
def parse_standup_updates(messages):
"""Parses Slack messages to extract standup updates."""
updates = []
# A simple parser: collect message text and user display name
# You might need more sophisticated parsing (e.g., regex for "Yesterday:", "Today:", "Blockers:")
for msg in messages:
if "subtype" not in msg and "text" in msg: # Exclude bot messages, join/leave messages etc.
user_id = msg.get("user")
# In a real scenario, you'd cache user info to avoid repeated API calls
user_info = get_slack_user_info(user_id, SLACK_BOT_TOKEN)
user_name = user_info.get("profile", {}).get("display_name", user_id)
updates.append({"user": user_name, "text": msg["text"], "ts": msg["ts"]})
return updates
def get_slack_user_info(user_id, token):
"""Fetches user information from Slack."""
slack_api_url = "https://slack.com/api/users.info"
headers = {"Authorization": f"Bearer {token}"}
params = {"user": user_id}
response = requests.get(slack_api_url, headers=headers, params=params)
response.raise_for_status()
return response.json().get("user", {})
# --- Confluence API Functions ---
def format_updates_for_confluence(updates, report_date):
"""Formats the collected updates into Confluence Storage Format (XHTML)."""
html_content = f"<h1>Daily Standup Report - {report_date.strftime('%Y-%m-%d')}</h1>"
if not updates:
html_content += "<p>No standup updates found for today.</p>"
return html_content
html_content += "<ul>"
for update in sorted(updates, key=lambda x: x['user']): # Sort by user for consistency
# Basic parsing for common standup patterns (can be greatly enhanced)
update_text = update['text'].replace('\n', '<br/>') # Preserve line breaks
html_content += f"<li><strong>{update['user']}:</strong> {update_text}</li>"
html_content += "</ul>"
return html_content
def get_confluence_page(url, username, api_token, title, space_key):
"""Checks if a Confluence page with the given title exists."""
auth = (username, api_token)
headers = {"Accept": "application/json"}
search_url = f"{url}/rest/api/content"
params = {
"spaceKey": space_key,
"title": title,
"expand": "version"
}
response = requests.get(search_url, auth=auth, headers=headers, params=params)
response.raise_for_status()
data = response.json()
if data["size"] > 0:
return data["results"][0]
return None
def create_or_update_confluence_page(url, username, api_token, space_key, parent_id, title, content):
"""Creates a new Confluence page or updates an existing one."""
auth = (username, api_token)
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
existing_page = get_confluence_page(url, username, api_token, title, space_key)
if existing_page:
page_id = existing_page["id"]
current_version = existing_page["version"]["number"]
print(f"Page '{title}' exists (ID: {page_id}), updating...")
data = {
"id": page_id,
"type": "page",
"title": title,
"space": {"key": space_key},
"body": {
"storage": {
"value": content,
"representation": "storage"
}
},
"version": {
"number": current_version + 1
}
}
update_url = f"{url}/rest/api/content/{page_id}"
response = requests.put(update_url, auth=auth, headers=headers, data=json.dumps(data))
else:
print(f"Page '{title}' does not exist, creating...")
data = {
"type": "page",
"title": title,
"space": {"key": space_key},
"body": {
"storage": {
"value": content,
"representation": "storage"
}
}
}
if parent_id:
data["ancestors"] = [{"id": parent_id}]
create_url = f"{url}/rest/api/content"
response = requests.post(create_url, auth=auth, headers=headers, data=json.dumps(data))
response.raise_for_status()
print(f"Confluence page '{title}' successfully {'updated' if existing_page else 'created'}.")
return response.json()
# --- Main Execution ---
if __name__ == "__main__":
report_date = datetime.now()
page_title = f"Daily Standup - {report_date.strftime('%Y-%m-%d')}"
# Calculate timestamp for messages from the last 24 hours
oldest_timestamp = (report_date - timedelta(days=1)).timestamp()
print(f"Collecting standup updates for {report_date.strftime('%Y-%m-%d')}...")
try:
slack_history = get_slack_channel_history(SLACK_CHANNEL_ID, SLACK_BOT_TOKEN, oldest_timestamp)
standup_updates = parse_standup_updates(slack_history.get("messages", []))
if not standup_updates:
print("No standup updates found in Slack for the last 24 hours.")
# Still generate an empty page in Confluence to indicate "no updates"
confluence_content = format_updates_for_confluence([], report_date)
else:
print(f"Found {len(standup_updates)} updates.")
confluence_content = format_updates_for_confluence(standup_updates, report_date)
create_or_update_confluence_page(
CONFLUENCE_URL,
CONFLUENCE_USERNAME,
CONFLUENCE_API_TOKEN,
CONFLUENCE_SPACE_KEY,
CONFLUENCE_PARENT_PAGE_ID,
page_title,
confluence_content
)
except requests.exceptions.RequestException as e:
print(f"An API request error occurred: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
Logic Explained:
- The script starts by defining configuration variables for your Slack and Confluence credentials and identifiers. Always store these sensitive values as environment variables in a production setup, not hardcoded.
-
get_slack_channel_historyuses the Slack API to fetch all messages from your standup channel posted within the last 24 hours (or since your specifiedoldest_ts). -
get_slack_user_infois a helper to resolve Slack user IDs to display names, making the Confluence report more readable. -
parse_standup_updatesis a very basic parser. For a more robust solution, you might want to identify specific keywords (e.g., âyesterday:â, âtoday:â, âblockers:â) within the messages using regular expressions to structure the content further. -
format_updates_for_confluencetakes the parsed updates and converts them into Confluence Storage Format, which is essentially a specific flavour of XHTML. This is critical for Confluence to render the content correctly. -
get_confluence_pagechecks if a standup page for the current date already exists. -
create_or_update_confluence_pagethen either creates a new page or updates an existing one. If updating, it increments the page version number, which is required by the Confluence API. - The main block orchestrates these functions, handling potential API errors with basic try-except blocks.
Step 4: Schedule Your Automation
With the script ready, the final step is to schedule it to run daily. You have several options:
- Cron Job (Linux/macOS): A simple and common method for servers.
0 9 * * 1-5 cd /path/to/your/script && python3 standup_automation.py >> standup.log 2>&1
This example runs the script at 9:00 AM, Monday through Friday, redirecting output to a log file.
- Scheduled Task (Windows): Use the Task Scheduler to set up a daily execution.
- CI/CD Pipeline: If you use Jenkins, GitLab CI, GitHub Actions, or similar, you can integrate this script into a daily scheduled pipeline.
- Cloud Functions/Serverless: AWS Lambda, Azure Functions, or Google Cloud Functions offer ways to run this script on a schedule without managing a server.
Important: Ensure that the environment where the script runs has access to the internet and the necessary environment variables (SLACK_BOT_TOKEN, CONFLUENCE_API_TOKEN, etc.) are properly set.
Common Pitfalls
- API Authentication Errors:
Issue: You get 401 (Unauthorized) or 403 (Forbidden) errors from Slack or Confluence.
Solution: Double-check your API tokens. Ensure they are correct, not expired, and have the necessary scopes/permissions. For Slack, verify the bot is invited to the channel. For Confluence, confirm the API token is generated by a user with permission to create/edit pages in the target space.
- Confluence Storage Format Issues:
Issue: Your Confluence page content appears as raw HTML or is malformed.
Solution: The Confluence Storage Format is specific XHTML. Ensure your generated HTML is valid and meets Confluenceâs expectations. Use tools like an HTML validator if needed. Pay close attention to escaping special characters (like < and >) when they are part of the content and not HTML tags.
- Rate Limiting:
Issue: API calls occasionally fail with 429 (Too Many Requests) errors.
Solution: While unlikely for a daily standup script, if you expand the script to make many calls, you might hit Slack or Confluence API rate limits. Implement exponential backoff for retries to handle this gracefully.
Conclusion
Automating your daily standup updates from Slack to Confluence is a powerful way to reduce manual toil, ensure consistent reporting, and keep your teamâs progress documented without human intervention. This tutorial provides a robust foundation for building such a system, leveraging the flexibility of Python and the power of APIs.
This solution can be further enhanced by:
- Adding more sophisticated natural language processing (NLP) to parse standup messages into structured fields (e.g., âWhat I didâ, âWhat Iâll doâ, âBlockersâ).
- Integrating with other tools, such as Jira, to automatically link tickets mentioned in standups.
- Implementing error notifications (e.g., sending a Slack message to an admin channel) if the automation fails.
- Creating a more dynamic Confluence page structure based on team members or project status.
Embrace automation, reclaim your time, and let your systems work for you. Happy automating!
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)