DEV Community

Cover image for Solved: Daily Standup Automation: Collecting Updates from Slack to Confluence
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Daily Standup Automation: Collecting Updates from Slack to Confluence

🚀 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, and users:read to 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.

  1. Navigate to the Slack API App management page and click “Create New App”.
  2. Choose “From scratch”, give your app a name (e.g., “Standup Bot”), and select your workspace.
  3. Once created, go to “Features” > “OAuth & Permissions” in the left sidebar.
  4. 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.
  5. Scroll up and click “Install to Workspace”. Grant the necessary permissions.
  6. After installation, copy the “Bot User OAuth Token” (starts with xoxb-). Keep this token secure; it will be used in your Python script.
  7. Finally, invite your new bot to your daily standup channel using the command /invite @YourBotName in 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.

  1. Log in to your Atlassian account (the one associated with your Confluence Cloud).
  2. Go to Atlassian API tokens.
  3. 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.
  4. Identify your Confluence Cloud URL (e.g., https://yourcompany.atlassian.net/wiki).
  5. 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/...).
  6. (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
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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_history uses the Slack API to fetch all messages from your standup channel posted within the last 24 hours (or since your specified oldest_ts).
  • get_slack_user_info is a helper to resolve Slack user IDs to display names, making the Confluence report more readable.
  • parse_standup_updates is 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_confluence takes 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_page checks if a standup page for the current date already exists.
  • create_or_update_confluence_page then 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
Enter fullscreen mode Exit fullscreen mode

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!


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)