Bulk Clone Pro: How to clone entire Epic (with Issues and their Sub-Tasks)?

Hi - I'm evaluating Bulk Clone Professional, and need to clone our "template" Epic.  This Epic contains a handful of Tasks ("Issues in Epic").  Each of these Tasks in the Epic have a bunch of Sub-Tasks.  How do I clone this entire structure? When I clone this "template" Epic using Bulk Clone Professional, I only specify this single Epic as needing to be cloned, because I want to end up with a cloned Epic that contains the same (cloned) Tasks as the source Epic contains, and have each of these (cloned) Tasks point to (cloned) Sub-Tasks.

So in the pic attached, the 5 "Issues in Epic" would also be cloned, and listed in the cloned Epic's "Issues in Epic" section.  Also, I need each of the Sub-Tasks (of each Task) to also be cloned.  So a full Epic clone.



3 answers

1 accepted

Accepted Answer
1 vote

Ok, I figured it out.  Instead of assuming that just cloning the "top level" Epic, I need to use JQL (project = MIG and "Epic Link" = MIG-84 or issuekey  = MIG-84)  - where MIG-84 is the Epic - and clone the Epic and all of the "Issues in Epic" together.  Very cool plug-in.  

Where do you place the JQL?  The Backlog area is the only place you can multi-select (I think) and if I do a JQL search, I am taken to a Search results page where I cannot multi-select.

Hi Kristina - this is Fred, posting by mistake from my manager's account (oops ;_0 )  ... so I go to my project, and click on "view all issues and filters" in the upper right.  Make sure the "Advanced" link is clicked, and create a filter that selects the epic you want to clone - along with all of the tasks in that epic.  So for me, it's:

project = MIG AND "Epic Link" = MIG-84 OR issuekey = MIG-84 ORDER BY issuetype ASC, issuekey ASC

Then I save that filter.  When you're ready to clone that epic (with its tasks and task sub-tasks), run that filter (by going to your project, clicking on "view all issues and filters", and clicking on the filter you just saved).  

You'll now see the epics and tasks in that epic.  You won't see sub-tasks but they're cloned automatically with each task.  Now just follow the add-on instructions (go to tools -> bulk tools, change all X issues, and in bulk operations select all of them, click next, then select the add-on "clone issues", and you're off and running).


Logged in as myself (Fred) now - let me know if you have any other questions on this!

I was able to create JQL and see the list I wanted.  I don't see tools > bulk tools etc.  I'm trying to follow http://www.lbconsultinggroup.org/bulkclone-professional-documentation/ which recommends going to Backlog...I don't want that.

Couldn't see how to save the filter except to save it to a local file.

When you build up the filter (using either 'basic' or 'advanced'), there is a "Save As" button that you can click to save that filter.

We chose Bulk Clone Pro because we could clone Epics and their hierarchies across many different JIRA Projects.

Thanks for all of your help.  Our needs were simpler; may need this in the future though...


I needed the same some time ago. I got a solution with 2 scriptrunner REST Endpoints called sequentially. The first is called from a custom web item (Script Fragments) in the more-menu of an issue. It shows a dialog where you can set the summary/epic name of the new epic. After clicking submit, the second endpoint is called.

Here are the scripts for the two REST Endpoints:


import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys

@BaseScript CustomEndpointDelegate delegate

showDialog() { MultivaluedMap queryParams ->
//get issueKey from parameters of REST Endpoint call
def issueKey = queryParams.getFirst("issueKey") as String
String BASE_URL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL);

//definition of Dialog with some Java Script
def dialog =
//On dialog the user can insert the summary/epic name of the new epic
//After clicking submit button, spinner is displayed and second REST Endpoint is called with new summary value
var newSum = AJS.\$("#text-input").val()
var spinning = false;

spinning = true;

var request = new XMLHttpRequest();

//Keep the dialog open for 15s, then close it and reload the source epic
function loadUrl(newLocation){
window.location.href = newLocation;
//show error message if field for new summary is empty
AJS.\$("#text-input").next().append('<div class="error">Summary for new Epic is required</div>')

//Dialog definition, got from scriptrunner documentation and adapted
<section role="dialog" id="sr-dialog" class="aui-layer aui-dialog2 aui-dialog2-medium" aria-hidden="true" data-aui-remove-on-hide="true">
<header class="aui-dialog2-header">
<h2 class="aui-dialog2-header-main">Clone Epic with Issues in Epic</h2>
<a class="aui-dialog2-header-close">
<span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>

<div class="aui-dialog2-content">
<p>Please insert Summary of new Epic</p>
<form class="aui">
<div class="field-group">
<label for="text-input">New Epic Name<span class="aui-icon icon-required">required</span></label>
<input class="text" type="text" id="text-input" name="text-input" title="summary">
<div class="description">Please insert Name of new Epic</div>

<footer class="aui-dialog2-footer">
<div class="aui-dialog2-footer-actions">
<button id="dialog-submit-button" class="aui-button aui-button-primary">Submit</button>
<span style="padding-left: 15px; padding-bottom: 10px; display: inline-block;" class="button-spinner"></span>
<button id="dialog-close-button" class="aui-button aui-button-link">Cancel</button>

//Show dialog

Clone Epic:

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.sal.api.user.UserManager
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.Response
import org.apache.log4j.Category
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.user.util.UserUtil
import com.atlassian.jira.config.properties.APKeys
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
import java.sql.Timestamp;
import org.apache.log4j.Logger;

@BaseScript CustomEndpointDelegate delegate

cloneEpicWithIssues(httpMethod: "GET") { MultivaluedMap queryParams,body, HttpServletRequest request ->

// Variable definition
def issueKey = queryParams.getFirst("issueKey") as String // issuekey from request parameters
def newSummary = queryParams.getFirst("newSum") as String // newSummary value from request parameters
def epic_link_field="customfield_12739";
def epic_name_field="customfield_12738";
def issueMgr = ComponentAccessor.getIssueManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def linkMgr = ComponentAccessor.getIssueLinkManager()
def issueFactory = ComponentAccessor.getIssueFactory()
Issue issue = issueMgr.getIssueObject(issueKey)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
Timestamp Ts = new Timestamp(System.currentTimeMillis());
CustomField epicLink = customFieldManager.getCustomFieldObject(epic_link_field);
CustomField epicName = customFieldManager.getCustomFieldObject(epic_name_field);

//Get current issues in epic -> links from and to source epic
List<IssueLink> outwardLinks = linkMgr.getOutwardLinks(issue.getId())
List<IssueLink> inwardLinks = linkMgr.getInwardLinks(issue.getId())
List<Issue> outwardLinkedIssues = [] as List<Issue>;
List<Issue> inwardLinkedIssues = [] as List<Issue>;



//sorting of epic links - not neccessary, was requirement from users
outwardLinkedIssues.sort{a,b -> a.getId()<=>b.getId()}
outwardLinkedIssues = outwardLinkedIssues.reverse()

inwardLinkedIssues.sort{a,b -> a.getId()<=>b.getId()}
inwardLinkedIssues = inwardLinkedIssues.reverse();

ModifiedValue mVal;

MutableIssue clonedIssue;
MutableIssue clonedEpic;

try {
//Clone epic with all fields
def toClone = issueFactory.cloneIssueWithAllFields(issue)
clonedEpic = issueMgr.createIssueObject(issue.reporter, toClone) as MutableIssue

//set current system time as created date, otherwise it would have created date from source epic
//issue.store() is deprecated but issueManager.updateIssue didn't work, don't know why

//Set new summary and epic name (same in this case)

mVal = new ModifiedValue(clonedEpic.getCustomFieldValue(epicName), newSummary);
epicName.updateValue(null, clonedEpic, mVal, new DefaultIssueChangeHolder());

//if source epic has outward links, clone each linked issue and set epic link to new epic
//For sub-tasks, this is unfortunately not working. Therefore they are excluded
clonedIssue = issueMgr.createIssueObject(issue.reporter, toClone) as MutableIssue


mVal = new ModifiedValue(clonedIssue.getCustomFieldValue(epicLink), clonedEpic);
epicLink.updateValue(null, clonedIssue, mVal, new DefaultIssueChangeHolder());

clonedIssue = issueMgr.createIssueObject(issue.reporter, toClone) as MutableIssue


mVal = new ModifiedValue(clonedIssue.getCustomFieldValue(epicLink), clonedEpic);
epicLink.updateValue(null, clonedIssue, mVal, new DefaultIssueChangeHolder());
return Response.serverError().entity([error: e.message]).build();

String BASE_URL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL);

//show success message and load new epic - doesn't work in some cases, don't know why
String clonedMessage = """
Navigating to created issue <a href="${BASE_URL}/browse/${clonedEpic.getKey()}">${clonedEpic.getKey()}</a>...
function loadUrl(newLocation){
window.location.href = newLocation;

 Maybe not the best solution, but it is working for us. At least useful as a workaround until Atlassian maybe is providing such a function in the standard JIRA application in the future.

Hi Fred

But is the EPIC-Link also changed?

JQL: key = TEST-1756 OR "EPIC Link"= TEST-1756 ORDER BY issuetype ASC

The issues in the list are from different projects.

Then I do the Clone and select all the issues.

I select the following issues in the 3rd step:

  • Clone Subtasks
  • Clone Issue Links
  • Rebuild Issue Links
  • Rebuild Epic Link

Everything looks fine - except the Epic Link. This is still to the old Epic.

Suggest an answer

Log in or Sign up to answer
Community showcase
Posted Thursday in Marketplace Apps

You + one app + a desert island...

Hi all! My name is Miles and I work on the Marketplace team. We’re looking for better ways to recommend and suggest apps that are truly crowd favorites, so of course we wanted to poll the Community. ...

947 views 4 5
Join discussion

Atlassian User Groups

Connect with like-minded Atlassian users at free events near you!

Find a group

Connect with like-minded Atlassian users at free events near you!

Find my local user group

Unfortunately there are no AUG chapters near you at the moment.

Start an AUG

You're one step closer to meeting fellow Atlassian users at your local meet up. Learn more about AUGs

Groups near you