Remote Work Tools

How to Create Automated Client Progress Report for Remote Projects

Automated client progress reports aggregate task completion, sprint metrics, and timeline data without manual compilation, saving hours weekly. You can script reports from Linear, Jira, or GitHub APIs, format them as PDF or email, and schedule weekly/monthly delivery. This guide covers reporting pipeline architecture, template examples, and integrations for remote project teams.

Understanding the Reporting Pipeline

An automated reporting system consists of three core components:

  1. Data Collection — Gathering metrics from your project management tools, version control, and CI/CD systems
  2. Template Processing — Injecting data into a structured report format
  3. Delivery Mechanism — Sending the completed report via email, Slack, or webhook

For a typical remote project, you’ll pull data from sources like GitHub issues, Jira tickets, Linear boards, or Trello. The automation layer then compiles this into a human-readable format.

Choosing the Right Data Sources

Before writing a single line of automation code, decide which data sources will power your reports. The choice depends on your team’s toolstack and what your clients actually care about. Here is a comparison of the most common sources:

Tool Best For API Quality Free Tier
GitHub Engineering teams, open-source Excellent Yes
Jira Large enterprise projects Good Limited
Linear Modern engineering teams Excellent Yes
Trello Simple project boards Good Yes
Asana Cross-functional teams Good Yes

For most remote development agencies, GitHub covers the majority of client-facing metrics: what shipped, what is in progress, and what is blocking progress. If your team also uses a project management layer like Linear, pull from both and merge the data in your template.

Building the Data Collection Layer

Start by identifying which metrics matter to your clients. Common choices include:

Here’s a Python script that collects data from GitHub and formats it for reporting:

import os
import requests
from datetime import datetime, timedelta
from github import Github

GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
REPO_OWNER = "your-org"
REPO_NAME = "your-project"
g = Github(GITHUB_TOKEN)
repo = g.get_repo(f"{REPO_OWNER}/{REPO_NAME}")

def get_completed_prs(days=7):
    """Fetch merged PRs from the past week."""
    since = datetime.now() - timedelta(days=days)
    closed_prs = repo.get_pulls(state="closed", sort="updated", direction="desc")

    completed = []
    for pr in closed_prs:
        if pr.merged_at and pr.merged_at >= since:
            completed.append({
                "title": pr.title,
                "number": pr.number,
                "author": pr.user.login,
                "merged_at": pr.merged_at.strftime("%Y-%m-%d")
            })
    return completed

def get_open_issues():
    """Fetch open issues excluding pull requests."""
    issues = repo.get_issues(state="open", sort="updated", direction="desc")
    return [{"title": i.title, "number": i.number, "labels": [l.name for l in i.labels]}
            for i in issues if not i.pull_request][:10]

This script fetches merged pull requests from the past week and lists current open issues. You can extend it to include commits, milestones, or any other GitHub API data relevant to your client.

Pulling Data from Linear

If your team tracks work in Linear alongside GitHub, use the Linear GraphQL API to include issue completion data. Linear’s API is particularly clean and well-documented:

import requests

LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY")
LINEAR_TEAM_ID = "your-team-id"

def get_linear_completed_issues(days=7):
    """Fetch completed Linear issues from the past week."""
    since = (datetime.now() - timedelta(days=days)).isoformat()
    query = """
    query CompletedIssues($teamId: String!, $since: DateTime!) {
      issues(
        filter: {
          team: { id: { eq: $teamId } }
          completedAt: { gte: $since }
          state: { type: { eq: "completed" } }
        }
      ) {
        nodes {
          title
          identifier
          assignee { name }
          completedAt
          estimate
        }
      }
    }
    """
    response = requests.post(
        "https://api.linear.app/graphql",
        json={"query": query, "variables": {"teamId": LINEAR_TEAM_ID, "since": since}},
        headers={"Authorization": LINEAR_API_KEY}
    )
    data = response.json()
    return data["data"]["issues"]["nodes"]

Combining Linear issues with GitHub PRs gives clients a complete picture: business-level work items alongside the actual code changes that delivered them.

Creating the Report Template

With data in hand, the next step is formatting it into a readable report. Markdown works well because it converts cleanly to HTML, PDF, or plain text depending on your delivery method.

def generate_report(completed_prs, open_issues, milestone_info):
    """Generate a markdown report from project data."""
    report = []
    report.append("# Project Progress Report")
    report.append(f"**Report Date:** {datetime.now().strftime('%Y-%m-%d')}\n")

    report.append("## Completed This Week")
    if completed_prs:
        for pr in completed_prs:
            report.append(f"- #{pr['number']}: {pr['title']} (merged {pr['merged_at']})")
    else:
        report.append("- No pull requests merged this week.")

    report.append("\n## Active Issues")
    if open_issues:
        for issue in open_issues:
            labels = f"[{', '.join(issue['labels'])}]" if issue['labels'] else ""
            report.append(f"- #{issue['number']}: {issue['title']} {labels}")
    else:
        report.append("- No open issues.")

    report.append("\n## Milestones")
    if milestone_info:
        for m in milestone_info:
            report.append(f"- **{m['title']}**: {m['progress']}% complete (due {m['due_date']})")

    return "\n".join(report)

This generates a clean, scannable report that highlights what shipped, what’s in progress, and where milestones stand.

Automating Delivery

The final piece is scheduling and delivering the report. For email delivery, you can use a simple SMTP approach:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_email_report(report_content, recipients):
    """Send report via email."""
    msg = MIMEMultipart()
    msg["Subject"] = f"Project Progress Report - {datetime.now().strftime('%Y-%m-%d')}"
    msg["From"] = "project-reports@yourcompany.com"
    msg["To"] = ", ".join(recipients)

    msg.attach(MIMEText(report_content, "plain"))

    with smtplib.SMTP("smtp.yourprovider.com", 587) as server:
        server.starttls()
        server.login(os.environ.get("SMTP_USER"), os.environ.get("SMTP_PASS"))
        server.send_message(msg)

For Slack integration, use the incoming webhook approach:

import json

def send_slack_report(report_content, webhook_url):
    """Send report to Slack channel."""
    payload = {
        "text": f"*Project Progress Report - {datetime.now().strftime('%Y-%m-%d')}*",
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": report_content
                }
            }
        ]
    }

    requests.post(webhook_url, data=json.dumps(payload),
                  headers={"Content-Type": "application/json"})

Scheduling the Automation

With the scripts in place, schedule them using cron for continuous delivery. A typical setup runs weekly:

# Run every Monday at 9 AM
0 9 * * 1 /usr/bin/python3 /path/to/generate_report.py >> /var/log/project-reports.log 2>&1

For containerized environments, GitHub Actions provides a clean scheduling mechanism:

name: Weekly Progress Report
on:
  schedule:
    - cron: '0 9 * * 1'
jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run report generator
        run: python generate_report.py
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SMTP_PASS: ${{ secrets.SMTP_PASS }}

Report Format Options: Choosing What Works for Each Client

Not every client wants a plain-text email. Tailor your delivery format based on the client’s preferences and technical comfort level.

Format Best For Tooling Required
Plain-text email Non-technical executives SMTP only
HTML email Most clients Jinja2 + SMTP
PDF attachment Compliance-heavy clients WeasyPrint or ReportLab
Slack message Technical teams Slack Webhooks
Notion page update Teams using Notion Notion API

For HTML emails, render your Markdown to HTML using Python’s markdown library before sending. This produces professional-looking reports without requiring a dedicated email platform.

Enhancing Reports with Additional Context

Basic metrics tell part of the story. Consider adding:

You can gather this context through structured conventions like a weekly standup bot that collects status updates, or by pulling from a dedicated “status” label in your issue tracker.

A practical approach is to maintain a report-notes.md file in the repository that team members update throughout the week. Your automation script reads this file at report generation time and appends it as the “Highlights” section. This keeps qualitative context attached to quantitative metrics without requiring any additional tooling.

Security and Access Considerations

When automating client reports, keep these best practices in mind:

For multi-client setups, use a configuration file per client that specifies their repository, recipients, and preferred format. This prevents accidental data cross-contamination between clients and makes it easy to onboard new accounts without modifying the core script.

Measuring Report Effectiveness

Track whether your automated reports achieve their purpose:

Adjust your template and delivery frequency based on feedback. The goal is consistent, valuable communication—not information overload. If a client starts ignoring reports within a few weeks, that is a signal to shorten the format, increase the signal-to-noise ratio, or shift to a different delivery channel rather than continuing to send reports nobody reads.


Building an automated client progress reporting system requires upfront development time but pays dividends through consistent stakeholder communication. Start with simple metrics and expand as you identify what matters most to your clients.

Built by theluckystrike — More at zovo.one