Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Earn badges and make progress

You're on your way to the next level! Join the Kudos program to earn points and save your progress.

Deleted user Avatar
Deleted user

Level 1: Seed

25 / 150 points

Next: Root


1 badge earned


Participate in fun challenges

Challenges come and go, but your rewards stay with you. Do more to earn more!


Gift kudos to your peers

What goes around comes around! Share the love by gifting kudos to your peers.


Rise up in the ranks

Keep earning points to reach the top of the leaderboard. It resets every quarter so you always have a chance!


Come for the products,
stay for the community

The Atlassian Community can help you and your team get more value out of Atlassian products and practices.

Atlassian Community about banner
Community Members
Community Events
Community Groups

How to restore Epic Link to Issues?

I accidentally changed an Epic, which was linked in thousands of issues, to a Task. This un-linked all of the Issues. Yes, major face-palm, I know.

Is there any way to restore the Epic Link to all of the Issues that previously had them?

4 answers

1 accepted

3 votes
Answer accepted

Hi @Yair Spolter

I was able to reproduce the issue (obviously) but I don't really have good news. I couldn't find an easy way to restore the issues linked to that epic. However you can view the issues that were linked to the Epic in the history. Thus there's a way to figure our which issues were linked.

  1. Simply open the Epic (that was accidentally changed to a task)
  2. On the bottom of the issue open the History tab:
    Screenshot 2018-08-19 at 17.11.05.jpg
  3. I was able to copy and paste this list as a table out of the history. Pasted it into a Google sheet and converted it to a JQL function (using some Sheet formula magic). See the sheet here.
  4. You can then use the JQL function to query all of the previously linked issues and use the Bulk Edit feature in Jira to add them to the Epic again.

Hope this helps.



@Maarten Cautreels that was pure AWESOMENESS!

I can't thank you enough!

It was truly... Epic! (sorry, I could not resist).

@Yair Spolter Happy to help! :-)

As the doc that @Maarten Cautreels linked is now offline, if someone stumbles upon this, my guess is that he suggested:

  1. copying the list of keys from the issues that got de-linked
  2. with some trickery, get a search query in JQL
  3. Execute that search query in the "issue" view of the project
  4. Run "Bulk Change" on the de-linked tasks
  5. Link all the tasks with the corresponding epic

If you only have one epic, is not difficult to do by hand. If your task keys are ABC-1, ABC-2 and ABC3 and your epic is ABC-4, search for

project = "ABC" and (key="ABC-1" or key="ABC-2" or key="ABC-3") ORDER BY created DESC

And run the bulk change on them.


In my case, I delinked ALL my epics (I changed their type to a custom "epic", only to discover that there's no such thing). With ~70 epics I needed more automation, so I wrote a script. Is super-tailored to what I needed, so I'll leave it here and then it's up to you to modify it accordingly. 

My workflow consisted in copying the history of an Epic into a text file called "History.txt" straight from my browser. Then the script will look for changes made by the specified user (as my mistake was caused by an automation, the user was "Automation for Jira"). The script will output the JQL query needed for Bulk Changing. As that was going to be a lot of work, I looked around and found GitHub - ankitpokhrel/jira-cli: 🔥 [WIP] Feature-rich interactive Jira command line., a Jira CLI interface. My script also will build the command that needs to be executed to link a number of issues to an epic, but if you don't want that, just comment the lines. It's based on Go, so you might need that too, or if you don't want to go through that hassle, use the JQL query.

To run it, for example, to search for changes made by "Automation by Jira" in the epic's history that is pasted on "History.txt", of an Epic with key "ABC-4" on a project with acronym "ABC", I would call it like this

python3 -p ABC -t History.txt -u "Automation for Jira" -e ABC-4
The script: PS: looks like this garbage doesn't like big code sections, and renders them as normal text!
****************** START OF THE SCRIPT ***************************
from pathlib import Path
from typing import Any
from re import findall
from enum import Enum
from subprocess import Popen, STDOUT, PIPE
class Lvl(str, Enum):
    """Debug log levels

    The different logging levels and the colours associated to the output
    ERROR = "\x1b[0;31;22m"  # RED + normal mode
    OK = "\x1b[0;32;22m"  # GREEN
    WARNING = "\x1b[0;33;22m"  # YELLOW
    # YELLOW, but I can use in temporary debugs and then delete
    DEBUG = "\x1b[0;33;22m"
    INFO = "\x1b[0;36;22m"  # CYAN
    NORMAL = "\x1b[0;37;22m"  # NORMAL white text
    RESET = "\x1b[0m"       # Reset

def debugPrint(level: Lvl, msg: Any) -> None:
    """Prints coloured text

        level: a member of the class lvl, that indicates the level of the messsage
        msg: a string to be printed with the selected colour
    print(f"{level}" + f"{msg}" + f"{Lvl.RESET}")

def buildQuery(listOfIssues: list[str], projectAcronym: str) -> str:
    searchQuery: str = 'project = "' + projectAcronym + '" and ('
    for key in listOfIssues[:-1]:
        searchQuery += 'key="' + f'{key}' + '" or '
    searchQuery += 'key="' + listOfIssues[-1] + '") ORDER BY created DESC'
    return searchQuery

# Parse CLI arguments
parser = ArgumentParser()
parser.add_argument("-p", "--projectAcronym",
                    help="The acronym of the project containing the issues", type=str, required=True)
parser.add_argument("-t", "--history",
                    help="Path to the file that contains the pasted history of the epic", type=str, required=True)
parser.add_argument("-e", "--epicKey",
                    help="The issue number or key of the Epic that has had its children removed",
                    type=str, required=True)
parser.add_argument("-u", "--user",
                    help="The user that created all the havoc!",
                    type=str, required=True)

args = parser.parse_args()

filename: Path = Path(args.history)
project: str = args.projectAcronym
epicKey: str = args.epicKey
user: str = args.user

    with open(filename, encoding="utf-8") as f:
        history =
        # This will need to be changed in order to match your case
        childs = findall(rf'({user})(\n )(updated the Epic Child)([\s\w\d,:]*)({project}-[\d]+)(\n)(None)', history)
        if childs:
            issueNumbers = [child[4] for child in childs]
            for child in childs:
                debugPrint(Lvl.OK, child[0] + " " + child [2] + " " + child[4] + " -> " + child[6])
            jqlQuery = buildQuery(issueNumbers, project)
            debugPrint(Lvl.OK, "JQL Search Query (for bulk editing)")
            debugPrint(Lvl.INFO, jqlQuery)

            jiraCliCommand = "jira epic add " + str(epicKey) + " " + " ".join(issueNumbers)
            debugPrint(Lvl.OK, "Jira CLI Command)")
            debugPrint(Lvl.INFO, jiraCliCommand)
                # Run the following command, redirecting stdin and stdout to be able to send input and display the output of the
                # command. Stderr is redirected to sdtout so it's displayed in context. Universal Newlines to true makes
                # Popen return text, instead of binary strings
                # Redirecting stdin allows us to send an enter, becuase the batch file ends in a "press ENTER to continue..."
                process = Popen(jiraCliCommand.split(), stdin=PIPE,
                                stdout=PIPE, stderr=STDOUT, universal_newlines=True)
                # Send the enter the script is waiting for to end execution
                debugPrint(Lvl.INFO, f"Output of '{jiraCliCommand}'")
                # Print the output of the command. communicate() returns a string tuple with the stdout and stderr, although
                # stderr is redirected to stdout already, and will be empty
                (stdout, _) = process.communicate()
                if stdout:
                    debugPrint(Lvl.INFO, stdout)
                # If the command returns other than 0, exit
                if (process.returncode != 0):
            except FileNotFoundError as err:
                debugPrint(Lvl.ERROR, 'ERROR: ' + str(err))

            debugPrint(Lvl.WARNING, "No Epic Child updates found in the history")

except FileNotFoundError:
    debugPrint(Lvl.ERROR, "File not found?")
****************** END OF THE SCRIPT ***************************
I hope somebody founds this useful

Does anyone have the google doc tamplate ? 

You probably won't need this anymore, but check my answer in case you still do

0 votes

Hi Yair,

Just so we are looking in the right direction a few questions:

  • Are you on Jira Server or Cloud? 
  • I'm also assuming (from the Epic Link) that you're using Jira Software instead of just Core? (Do you have Kanban/Scrum boards available on your Jira?)
  • How/what did you change on the Epic?
  • What kind of link do you want to restore? The issues being linked to the Epic through the Epic Link field or another type of link (relates to, is blocked by, ...) to this Epic?

I'd love to help search for an easy restore solution but I'm afraid restoring a backup will most likely be the easiest way.



Thanks for your help Maarten.

We are on Jira server.

We do have Kanban etc.

I changed the "Type" from Epic to Task

I want to restore the the issues being linked to the Epic through the Epic Link field.

Restoring a backup means going back to a previous state (which affects ALL changeds made since)?

Thanks for the info and quick response. I'm going to try and reproduce this on my test instance to see if I can find an easy way to restore it.



Thank you. That would be great!

Suggest an answer

Log in or Sign up to answer

Atlassian Community Events