Overview
This solution helps in automatically detecting and managing related incidents in Jira Service Management. By identifying and linking similar incidents, you can streamline your incident management process, ensuring that associated issues are resolved efficiently.
The Problem
Managing incidents in a large organization can be overwhelming, especially when multiple incidents are related to the same underlying issue. Identifying these related incidents manually is time-consuming and prone to errors. Without proper management, related incidents can go unresolved for longer periods, impacting your organization’s service quality.
The Solution
This script automates the process of identifying and linking related incidents. By focusing on incidents that affect the same service and using advanced AI to analyze incident descriptions, the script can determine if incidents are related and link them to a major incident. This allows for a more organized and efficient incident resolution process.
How It Works
- Detect Related Incidents: The script examines all open incidents related to the same affected service.
- It’s important to note that I’ve seen risky configurations when it comes to automatically associating issues. Therefore, we need to be very careful here. To mitigate risks, the script only considers tickets pointing to the same affected service.
- Identify Major Incident: It identifies the major incident among these related incidents.
- Link Incidents: Related incidents are linked to the major incident, creating a parent-child relationship.
Steps to Implement
- Create a Web Service: The script is hosted as a web service using Flask.
- Set Up Automation: Create a rule in Automation for Jira to trigger the script when an incident is created or updated.
- Configure Script: Adjust the script to match your Jira instance and custom fields.
Expected Results
When the script is triggered, it will:
- Identify Similar Incidents: Find all incidents related to the same affected service.
- Determine Relationships: Use AI to determine if the incidents are related.
- Link Incidents: Automatically link related incidents to the major incident.
Benefits
- Time-Saving: Reduces the time spent on manually identifying and linking related incidents.
- Consistency: Ensures related incidents are consistently identified and managed.
- Efficiency: Improves incident resolution times by linking related issues for collective resolution.
Screenshots
Automation Rule Setup




Incident Linking
Incident 1 that was already opened and marked as MAJOR Incident.

Incident 2 that triggered the rule/script

Comment added to the incident that triggered the rule, saving the response from OpenAI as a internal note explaining why the incidents are correlated.

Configuration Details
Ensure the following variables are set correctly in the script:
SERVICE_ID_FIELD = '10053' # Custom field ID for the Affected Services
MAJOR_INCIDENT_FIELD = '10040' # Custom field ID for major incidents
BASE_URL = "https://your-jira-instance.atlassian.net" # Your Jira instance URL


The Script
Below is the full script:
import os
import json
import logging
import requests
from requests.auth import HTTPBasicAuth
from flask import Flask, request, jsonify
from openai import OpenAI
# Define global variables for custom fields
SERVICE_ID_FIELD = '10053'
MAJOR_INCIDENT_FIELD = '10040'
BASE_URL = "https://bortolin.atlassian.net"
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
app = Flask(__name__)
@app.errorhandler(Exception)
def handle_exception(e):
logging.error(f"An error occurred: {e}")
return jsonify({"error": str(e)}), 500
def check_incident_correlation(source, target, api_key):
client = OpenAI(api_key=api_key)
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are an assistant that helps with incident correlation."},
{"role": "user", "content": (
f"Compare the following two incidents and determine if they are related:\n\n"
f"Source: {source}\n\n"
f"Target: {target}. If they are related, explain way. Finally, at the very end of your response, always insert [[related]] or [[not related]] so I can easily parse the response."
)}
],
temperature=0,
top_p=1,
frequency_penalty=0,
presence_penalty=0
)
return response.choices[0].message.content.strip()
except Exception as e:
logging.error(f"An error occurred while checking incident correlation: {e}")
return ""
def compare_issues(source_issue, target_issue, api_key):
try:
source_description = source_issue['fields']['description'] if source_issue['fields']['description'] else "No description provided."
target_description = target_issue['fields']['description'] if target_issue['fields']['description'] else "No description provided."
source = f"Summary: {source_issue['fields']['summary']}\nDescription: {source_description}"
target = f"Summary: {target_issue['fields']['summary']}\nDescription: {target_description}"
return check_incident_correlation(source, target, api_key)
except Exception as e:
logging.error(f"An error occurred while comparing issues: {e}")
return ""
def get_issue_details(issue_key, email, token):
try:
url = f"{BASE_URL}/rest/api/3/issue/{issue_key}"
auth = HTTPBasicAuth(email, token)
headers = {"Accept": "application/json"}
response = requests.get(url, auth=auth, headers=headers)
if response.status_code != 200:
logging.error(f"Failed to get issue details: {response.text}")
return None
return response.json()
except Exception as e:
logging.error(f"An error occurred while getting issue details: {e}")
return None
def get_related_issues(service_id, email, token):
try:
jql = f'cf[{SERVICE_ID_FIELD}] = "{service_id}" AND resolution = Unresolved'
url = f"{BASE_URL}/rest/api/3/search"
auth = HTTPBasicAuth(email, token)
headers = {"Accept": "application/json"}
start_at = 0
max_results = 50
all_issues = []
while True:
params = {
'jql': jql,
'startAt': start_at,
'maxResults': max_results
}
response = requests.get(url, auth=auth, headers=headers, params=params)
if response.status_code != 200:
logging.error(f"Failed to get issues: {response.text}")
return []
issues = response.json().get('issues', [])
all_issues.extend(issues)
logging.info(f"Fetched {len(issues)} issues in this batch, total fetched so far: {len(all_issues)}")
if len(issues) < max_results:
break
start_at += max_results
logging.info(f"Total issues found related to service {service_id}: {len(all_issues)}")
return all_issues
except Exception as e:
logging.error(f"An error occurred while getting related issues: {e}")
return []
def find_major_incident_issue(issues):
try:
for issue in issues:
if issue['fields'].get(f'customfield_{MAJOR_INCIDENT_FIELD}') == "MAJOR_INCIDENT":
logging.info(f"Major incident found: {issue['key']}")
return issue
logging.info("No major incident found")
return None
except Exception as e:
logging.error(f"An error occurred while finding major incident issue: {e}")
return None
def are_issues_linked(source_issue, major_incident):
try:
issue_links = source_issue.get('fields', {}).get('issuelinks', [])
for link in issue_links:
link_type = link.get('type', {}).get('name')
if link_type == "Duplicate":
if (link.get('inwardIssue', {}).get('key') == major_incident['key']) or \
(link.get('outwardIssue', {}).get('key') == major_incident['key']):
return True
return False
except Exception as e:
logging.error(f"An error occurred while checking if issues are linked: {e}")
return False
def compare_and_link_issues(source_issue, major_incident, api_key, email, token):
try:
if are_issues_linked(source_issue, major_incident):
logging.info(f"Issues {source_issue['key']} and {major_incident['key']} are already linked.")
return False
correlation_result = compare_issues(source_issue, major_incident, api_key)
if "[related]" in correlation_result.lower():
logging.info(f"Linking issue {source_issue['key']} to major incident {major_incident['key']}")
url = f"{BASE_URL}/rest/api/3/issueLink"
auth = HTTPBasicAuth(email, token)
headers = {"Accept": "application/json", "Content-Type": "application/json"}
payload = {
"type": {
"name": "Duplicate"
},
"inwardIssue": {
"key": major_incident['key']
},
"outwardIssue": {
"key": source_issue['key']
}
}
response = requests.post(url, auth=auth, headers=headers, data=json.dumps(payload))
if response.status_code != 201:
logging.error(f"Failed to link issues: {response.text}")
return False
logging.info(f"Issues successfully linked to {major_incident['key']}")
return True
else:
logging.info(f"Issues {source_issue['key']} and {major_incident['key']} are not related.")
return False
except Exception as e:
logging.error(f"An error occurred while comparing and linking issues: {e}")
return False
@app.route('/jira-webhook', methods=['POST'])
def jira_webhook():
try:
data = request.json
issue_key = data.get('issuekey')
api_key = request.headers.get('OpenAI-Api-Key')
email = request.headers.get('Email')
token = request.headers.get('Token')
logging.info("Received webhook with issue_key: %s", issue_key)
if not issue_key:
logging.error("Issue key missing")
return jsonify({"error": "Issue key missing"}), 400
if not api_key:
logging.error("OpenAI API key missing in headers")
return jsonify({"error": "OpenAI API key missing in headers"}), 400
if not email or not token:
logging.error("Email or Token missing in headers")
return jsonify({"error": "Email or Token missing in headers"}), 400
source_issue = get_issue_details(issue_key, email, token)
if not source_issue:
return jsonify({"error": "Issue not found"}), 404
service_id = source_issue['fields'].get(f'customfield_{SERVICE_ID_FIELD}', [{}])[0].get('id')
if not service_id:
logging.error("Service ID missing in issue fields")
return jsonify({"error": "Service ID missing in issue fields"}), 400
related_issues = get_related_issues(service_id, email, token)
major_incident = find_major_incident_issue(related_issues)
issue_linked = False
correlation_result = ""
if major_incident:
if not are_issues_linked(source_issue, major_incident):
correlation_result = compare_issues(source_issue, major_incident, api_key)
issue_linked = compare_and_link_issues(source_issue, major_incident, api_key, email, token)
else:
logging.info(f"Issues {source_issue['key']} and {major_incident['key']} are already linked.")
return jsonify({
"message": "Processed successfully",
"related_issues_count": len(related_issues),
"major_incident": major_incident['key'] if major_incident else None,
"issue_linked": issue_linked,
"correlation_result": correlation_result
}), 200
except Exception as e:
logging.error(f"An error occurred in jira_webhook: {e}")
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
try:
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))
except Exception as e:
logging.error(f"An error occurred while starting the server: {e}")
Additional Automation Possibilities
Once incidents are linked, you can create additional automation rules in Jira to manage these linked incidents more effectively:
- Automatic Resolution: Create a rule that automatically resolves linked incidents when the major incident is resolved. This ensures that all related issues are closed simultaneously, reducing the need for manual intervention.
- Notification Triggers: Set up notifications to alert relevant teams or stakeholders when a major incident and its related incidents are identified and linked.
- Priority Adjustments: Automate the adjustment of priorities for linked incidents based on the severity of the major incident.
- Status Synchronization: Ensure that the status of linked incidents is synchronized with the major incident, keeping all related parties informed about the progress.
Comparing Atlassian’s Similar Incidents Feature with this Automated Incident Linking Solution
Atlassian’s Similar Incidents Feature
Atlassian’s feature uses Natural Language Processing (NLP) and AI to identify and display past resolved incidents that are similar to the current issue. This helps users find historical context and related incidents that have already been resolved, enhancing their ability to resolve new incidents efficiently.
Custom Automated Incident Linking Solution
The custom solution described in this post takes a different approach:
- Focus: It specifically examines open incidents affecting the same service.
- Automation: It automates the process of linking related open incidents to a major incident, establishing a parent-child relationship among them.
- Purpose: The primary goal is to streamline the management and resolution of current incidents by automatically organizing them under a major incident.
Conclusion
This solution is a critical first step towards intelligently managing related incidents in Jira Service Management. By automating the detection and linking of similar incidents, you can enhance your incident management process, leading to faster resolutions and improved service quality. Additionally, leveraging Jira’s automation capabilities can further streamline the resolution of linked incidents, ensuring a comprehensive and efficient incident management strategy.