Extracting JIRA Data for Status Reports and Documentation

READER BEWARE: THE FOLLOWING WRITTEN ENTIRELY BY AI WITHOUT HUMAN EDITING.

Introduction

If you manage projects in Jira, you often need to provide status reports and documentation for stakeholders, audits, or project reviews. This guide shows how to pull Epic data from the Jira API—including all child items (stories, tasks, bugs)—and compile summaries, dates, and statuses into a text document suitable for evidence and reporting purposes.

Why Extract Jira Data?

  • Status Reports: Automatically generate comprehensive status reports for stakeholders
  • Compliance & Audits: Provide evidence and documentation without granting system access
  • Historical Records: Create snapshots of project state at specific points in time for archival
  • Executive Summaries: Transform technical project data into readable reports for management

Prerequisites

Before you begin, you’ll need:

  • A Jira instance (Cloud or Server)
  • API access credentials (email + API token for Cloud, or username + password for Server)
  • Python 3.7+ (for the examples in this guide)
  • requests library: pip install requests

Getting Started: Jira API Basics

Authentication

For Jira Cloud, create an API token at https://id.atlassian.com/manage-profile/security/api-tokens

import requests
from requests.auth import HTTPBasicAuth
import json

# Jira Cloud configuration
JIRA_URL = "https://your-domain.atlassian.net"
EMAIL = "your-email@example.com"
API_TOKEN = "your-api-token"

# Create authentication object
auth = HTTPBasicAuth(EMAIL, API_TOKEN)
headers = {
    "Accept": "application/json",
    "Content-Type": "application/json"
}

Testing Your Connection

def test_jira_connection():
    """Test Jira API connection"""
    url = f"{JIRA_URL}/rest/api/3/myself"
    response = requests.get(url, auth=auth, headers=headers)
    
    if response.status_code == 200:
        user = response.json()
        print(f"Connected as: {user['displayName']}")
        return True
    else:
        print(f"Connection failed: {response.status_code}")
        return False

Fetching Epic Data

Step 1: Get All Epics from a Project

def get_project_epics(project_key):
    """Retrieve all Epics from a project"""
    jql = f'project = {project_key} AND type = Epic'
    url = f"{JIRA_URL}/rest/api/3/search"
    
    params = {
        'jql': jql,
        'maxResults': 100,
        'fields': 'summary,status,created,updated,duedate,customfield_10016'
    }
    
    response = requests.get(url, auth=auth, headers=headers, params=params)
    
    if response.status_code == 200:
        return response.json()['issues']
    else:
        print(f"Failed to fetch epics: {response.status_code}")
        return []

Step 2: Retrieve Child Issues of an Epic

Child issues can be stories, tasks, bugs, or any other issue type linked to an Epic.

def get_epic_children(epic_key):
    """Retrieve all child issues for a given Epic"""
    # For Jira Cloud, use the parent link
    jql = f'"Epic Link" = {epic_key} OR parent = {epic_key}'
    url = f"{JIRA_URL}/rest/api/3/search"
    
    params = {
        'jql': jql,
        'maxResults': 100,
        'fields': 'summary,status,issuetype,created,updated,assignee,priority'
    }
    
    response = requests.get(url, auth=auth, headers=headers, params=params)
    
    if response.status_code == 200:
        return response.json()['issues']
    else:
        print(f"Failed to fetch children: {response.status_code}")
        return []

Extracting and Organizing Data

Step 3: Parse Issue Data

def extract_issue_data(issue):
    """Extract relevant fields from a Jira issue"""
    fields = issue['fields']
    
    return {
        'key': issue['key'],
        'summary': fields['summary'],
        'status': fields['status']['name'],
        'issue_type': fields['issuetype']['name'],
        'created': fields['created'][:10],  # Just the date
        'updated': fields['updated'][:10],
        'assignee': fields.get('assignee', {}).get('displayName', 'Unassigned'),
        'priority': fields.get('priority', {}).get('name', 'None')
    }

Step 4: Compile Complete Epic Information

def compile_epic_data(epic):
    """Compile complete Epic information with all children"""
    epic_data = extract_issue_data(epic)
    epic_data['children'] = []
    
    # Get all child issues
    children = get_epic_children(epic['key'])
    
    for child in children:
        child_data = extract_issue_data(child)
        epic_data['children'].append(child_data)
    
    return epic_data

Generating the Text Document

Step 5: Format Data as Markdown

def format_epic_as_markdown(epic_data):
    """Format Epic data as markdown text"""
    md = f"## {epic_data['key']}: {epic_data['summary']}\n\n"
    md += f"**Status:** {epic_data['status']}\n\n"
    md += f"**Created:** {epic_data['created']} | **Updated:** {epic_data['updated']}\n\n"
    
    if epic_data['children']:
        md += f"### Child Items ({len(epic_data['children'])})\n\n"
        
        # Group by status
        status_groups = {}
        for child in epic_data['children']:
            status = child['status']
            if status not in status_groups:
                status_groups[status] = []
            status_groups[status].append(child)
        
        for status, items in sorted(status_groups.items()):
            md += f"#### {status} ({len(items)})\n\n"
            for item in items:
                md += f"- **{item['key']}**: {item['summary']}\n"
                md += f"  - Type: {item['issue_type']}\n"
                md += f"  - Assignee: {item['assignee']}\n"
                md += f"  - Updated: {item['updated']}\n\n"
    
    return md

Step 6: Create Complete Report

def generate_jira_report(project_key, output_file="jira_report.md"):
    """Generate a complete Jira report for a project"""
    print(f"Fetching Epics from {project_key}...")
    epics = get_project_epics(project_key)
    
    with open(output_file, 'w') as f:
        f.write(f"# Jira Project Report: {project_key}\n\n")
        f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
        f.write(f"Total Epics: {len(epics)}\n\n")
        f.write("---\n\n")
        
        for epic in epics:
            print(f"Processing Epic: {epic['key']}")
            epic_data = compile_epic_data(epic)
            f.write(format_epic_as_markdown(epic_data))
            f.write("\n---\n\n")
    
    print(f"Report generated: {output_file}")

Complete Working Example

Here’s the full script you can use:

#!/usr/bin/env python3
"""
Jira Epic Data Extractor
Extracts Epic and child issue data from Jira and generates a markdown report.
"""

import requests
from requests.auth import HTTPBasicAuth
from datetime import datetime
import json

# Configuration
JIRA_URL = "https://your-domain.atlassian.net"
EMAIL = "your-email@example.com"
API_TOKEN = "your-api-token"
PROJECT_KEY = "PROJ"

# Authentication
auth = HTTPBasicAuth(EMAIL, API_TOKEN)
headers = {
    "Accept": "application/json",
    "Content-Type": "application/json"
}

def get_project_epics(project_key):
    """Retrieve all Epics from a project"""
    jql = f'project = {project_key} AND type = Epic ORDER BY created DESC'
    url = f"{JIRA_URL}/rest/api/3/search"
    
    params = {
        'jql': jql,
        'maxResults': 100,
        'fields': 'summary,status,created,updated,duedate'
    }
    
    response = requests.get(url, auth=auth, headers=headers, params=params)
    response.raise_for_status()
    return response.json()['issues']

def get_epic_children(epic_key):
    """Retrieve all child issues for a given Epic"""
    jql = f'"Epic Link" = {epic_key} OR parent = {epic_key}'
    url = f"{JIRA_URL}/rest/api/3/search"
    
    params = {
        'jql': jql,
        'maxResults': 100,
        'fields': 'summary,status,issuetype,created,updated,assignee,priority'
    }
    
    response = requests.get(url, auth=auth, headers=headers, params=params)
    response.raise_for_status()
    return response.json()['issues']

def extract_issue_data(issue):
    """Extract relevant fields from a Jira issue"""
    fields = issue['fields']
    
    return {
        'key': issue['key'],
        'summary': fields['summary'],
        'status': fields['status']['name'],
        'issue_type': fields.get('issuetype', {}).get('name', 'Unknown'),
        'created': fields['created'][:10],
        'updated': fields['updated'][:10],
        'assignee': fields.get('assignee', {}).get('displayName', 'Unassigned') if fields.get('assignee') else 'Unassigned',
        'priority': fields.get('priority', {}).get('name', 'None') if fields.get('priority') else 'None'
    }

def compile_epic_data(epic):
    """Compile complete Epic information with all children"""
    epic_data = extract_issue_data(epic)
    epic_data['children'] = []
    
    children = get_epic_children(epic['key'])
    
    for child in children:
        child_data = extract_issue_data(child)
        epic_data['children'].append(child_data)
    
    return epic_data

def format_epic_as_markdown(epic_data):
    """Format Epic data as markdown text"""
    md = f"## {epic_data['key']}: {epic_data['summary']}\n\n"
    md += f"**Status:** {epic_data['status']}\n\n"
    md += f"**Created:** {epic_data['created']} | **Updated:** {epic_data['updated']}\n\n"
    
    if epic_data['children']:
        md += f"### Child Items ({len(epic_data['children'])})\n\n"
        
        status_groups = {}
        for child in epic_data['children']:
            status = child['status']
            if status not in status_groups:
                status_groups[status] = []
            status_groups[status].append(child)
        
        for status, items in sorted(status_groups.items()):
            md += f"#### {status} ({len(items)})\n\n"
            for item in items:
                md += f"- **{item['key']}**: {item['summary']}\n"
                md += f"  - Type: {item['issue_type']}\n"
                md += f"  - Assignee: {item['assignee']}\n"
                md += f"  - Updated: {item['updated']}\n\n"
    else:
        md += "*No child items found*\n\n"
    
    return md

def generate_jira_report(project_key, output_file="jira_report.md"):
    """Generate a complete Jira report for a project"""
    print(f"Fetching Epics from {project_key}...")
    epics = get_project_epics(project_key)
    
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(f"# Jira Project Report: {project_key}\n\n")
        f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
        f.write(f"Total Epics: {len(epics)}\n\n")
        f.write("---\n\n")
        
        for epic in epics:
            print(f"Processing Epic: {epic['key']}")
            epic_data = compile_epic_data(epic)
            f.write(format_epic_as_markdown(epic_data))
            f.write("\n---\n\n")
    
    print(f"Report generated: {output_file}")

if __name__ == "__main__":
    generate_jira_report(PROJECT_KEY)

Usage

  1. Save the script as jira_extractor.py
  2. Update the configuration variables:
    • JIRA_URL: Your Jira instance URL
    • EMAIL: Your Jira email
    • API_TOKEN: Your API token
    • PROJECT_KEY: The project key to extract data from
  3. Run: python jira_extractor.py

Advanced Features

Filtering by Date Range

def get_epics_by_date_range(project_key, start_date, end_date):
    """Get Epics created within a date range"""
    jql = f'project = {project_key} AND type = Epic AND created >= "{start_date}" AND created <= "{end_date}"'
    # ... rest of implementation

Including Custom Fields

Many organizations use custom fields. To include them:

# Add custom fields to the fields parameter
params = {
    'jql': jql,
    'fields': 'summary,status,customfield_10001,customfield_10002'
}

# Access in extraction
custom_value = fields.get('customfield_10001', 'N/A')

Best Practices

  1. Rate Limiting: Add delays between API calls to avoid rate limits

    import time
    time.sleep(0.5)  # 500ms delay between requests
    
  2. Error Handling: Always handle API errors gracefully

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e}")
        return []
    
  3. Pagination: For projects with many issues, implement pagination

    def get_all_issues(jql):
        all_issues = []
        start_at = 0
        max_results = 50
    
        while True:
            params = {
                'jql': jql,
                'startAt': start_at,
                'maxResults': max_results
            }
            response = requests.get(url, auth=auth, headers=headers, params=params)
            data = response.json()
            all_issues.extend(data['issues'])
    
            if start_at + max_results >= data['total']:
                break
            start_at += max_results
    
        return all_issues
    
  4. Secure Credentials: Use environment variables

    import os
    API_TOKEN = os.environ.get('JIRA_API_TOKEN')
    

Troubleshooting

Common Issues

Authentication Failed (401)

  • Verify your API token is correct and not expired
  • Ensure you’re using the correct authentication method for your Jira version

Custom Fields Not Found

  • Custom field IDs vary by Jira instance
  • Find your custom field IDs: /rest/api/3/field

Epic Link Field Missing

  • In newer Jira versions, use parent instead of "Epic Link"
  • Check your Jira version’s API documentation

Empty Child Lists

  • Verify your JQL query matches your Jira hierarchy
  • Some organizations use different parent-child relationships

Conclusion

Extracting Jira data programmatically enables powerful workflows for documentation, reporting, and evidence collection. This approach works well for:

  • Regular project status reports for management
  • Quarterly planning reviews and retrospectives
  • Compliance documentation and audit trails
  • Historical project analysis and lessons learned

The Python script provided is a solid foundation that you can extend based on your organization’s specific needs. Consider adding features like:

  • Attachment downloads for evidence gathering
  • Comment extraction for detailed context
  • Sprint information and velocity metrics
  • Custom field mapping for organization-specific data
  • Multiple project support for portfolio reports
  • Email notifications for automated distribution
  • Export to PDF or Word formats for formal documentation

The generated markdown reports can be easily converted to other formats using tools like Pandoc for distribution to stakeholders who need formal documentation.

Resources