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)
requestslibrary: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
- Save the script as
jira_extractor.py - Update the configuration variables:
JIRA_URL: Your Jira instance URLEMAIL: Your Jira emailAPI_TOKEN: Your API tokenPROJECT_KEY: The project key to extract data from
- 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
Rate Limiting: Add delays between API calls to avoid rate limits
import time time.sleep(0.5) # 500ms delay between requestsError Handling: Always handle API errors gracefully
try: response.raise_for_status() except requests.exceptions.HTTPError as e: print(f"HTTP Error: {e}") return []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_issuesSecure 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
parentinstead 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.