Connect a synced item to an existing Jira Data Center issue (one to one and many to one)

Platforms: any → Jira Data Center

The problem

By default, the first sync of a remote item creates a new issue in Jira. Often the issue already exists: it was created by hand before the integration, or several remote items belong to the same Jira issue. Creating a new issue then gives duplicates, and the team has to merge or close them by hand.

The approach

  1. The remote side sends the Jira reference. Its outgoing script puts the Jira ID (12345) or key (PROJ-123) in replica.jiraId.
  2. The Jira DC incoming script finds that issue on first sync. A numeric value is looked up with getIssueObject, a key with getIssueByCurrentKey. If a reference is sent but the issue doesn’t exist, the sync stops with a clear error instead of creating a duplicate.
  3. The script connects to the existing issue instead of creating one. It loads the issue as Exalate sees it, fills issueBeforeScript and issue with its current values, and sets issue.id and issue.key to the existing issue.
  4. Nothing sent: normal behavior. Without a jiraId, a new issue is created in the default project, as usual.

The result

  • One to one: a remote item connects to the Jira issue it points to, no duplicate is created.
  • Many to one: when another remote item arrives later with the same Jira ID or key, it connects to that same Jira issue as well.
  • From then on, the items sync like any other pair.

Notes

  • Replace PROJ with your default project key, and fill typemap with your remote type names and the matching Jira issue types.
  • The remote outgoing script must send the reference, for example replica.jiraId = <the field that holds the Jira ID or key>.
  • getIssueByCurrentKey only accepts keys. A numeric Jira ID needs getIssueObject(id as Long), which is why the script checks isLong() first.
  • Maps are copied into issue (new LinkedHashMap(v)), so the changes made by the rest of your incoming script are detected as changes against issueBeforeScript.
  • With many to one, all connected remote items update the same Jira issue. Keep that in mind for fields like summary or status, where the last update wins.
  • Don’t have the ID or key on the remote side, only a value in a custom field? Find the issue with a JQL search first: [link to JQL post].
  • Put your normal incoming mappings (summary, description, comments) below the connect block.

The Scripts

1. Jira DC incoming script: connect to an existing issue

// Exalate | Jira Data Center | Incoming sync
// On first sync, connect the remote item to an EXISTING Jira issue instead of creating a new one.
// Works one to one and many to one: every remote item that sends the same Jira ID or key
// is connected to that same Jira issue.
//
// The remote side sends the Jira ID (12345) or key (PROJ-123) in replica.jiraId.
// Nothing sent: a new issue is created in the default project.
//
// Configure:
//   PROJECT_KEY   default project for new issues
//   typemap       remote type name -> Jira issue type name
//
// Notes:
//   - getIssueByCurrentKey() takes keys only; use getIssueObject(id as Long) for numeric IDs.
//   - Maps are copied into "issue" so the rule's changes show up as diffs against issueBeforeScript.
import com.atlassian.jira.component.ComponentAccessor

final String PROJECT_KEY = "PROJ"

Map typemap = [
  "Bug": "Bug"
]

String typeName = typemap[replica.type?.name] ?: debug.error("Value ${replica.type?.name} Not found in type map.")

def issueManager = ComponentAccessor.issueManager
def mainIssue = null

// Find the existing Jira issue by the ID or key sent by the remote side
if (firstSync && replica.jiraId) {
    def ref = replica.jiraId.toString().trim()   // Jira ID (12345) or key (PROJ-123)
    mainIssue = ref.isLong() ? issueManager.getIssueObject(ref as Long) : issueManager.getIssueByCurrentKey(ref)
    if (!mainIssue) {
        debug.error("Jira issue '${ref}' not found, cannot connect.")
    }
}

if (firstSync && mainIssue) {
    // Use the existing issue's project/type; fall back to defaults
    issue.projectKey = mainIssue.project?.key ?: PROJECT_KEY
    issue.typeName   = mainIssue.issueType?.name ?: typeName

    // Load the existing Jira issue as Exalate sees it
    def issueKey = new com.exalate.basic.domain.BasicIssueKey(mainIssue.id, mainIssue.key)
    def hubObjectHelper = ComponentAccessor
        .getOSGiComponentInstanceOfType(com.exalate.api.hubobject.IHubObjectHelperFactory.class)
        .get(new com.exalate.basic.domain.BasicSemanticVersion(1, 0, 0))
    def hubIssue = hubObjectHelper.getHubIssueOnOutgoingProcessor(issueKey)

    // Fill "before" and "after"; Maps are copied so the rule's changes are detected as diffs
    hubIssue.getProperties().each { k, v ->
        issueBeforeScript[k] = v
        issue[k] = (v instanceof Map) ? new LinkedHashMap(v) : v
    }

    // Connect
    issueBeforeScript.id = null
    issue.id  = mainIssue.id
    issue.key = mainIssue.key
    issue.customKeys.somethingIsChanged = true

} else if (firstSync) {
    issue.projectKey = PROJECT_KEY
    issue.typeName   = typeName
}

// Your normal incoming mappings go below (summary, description, comments, ...)

Great question! You’re absolutely right—by default, Exalate creates a new Jira issue on the first sync, which can lead to duplicates if the issue already exists or if multiple remote items should map to the same Jira issue. The approach you’ve outlined is a solid way to connect incoming remote items to an existing Jira Data Center issue, supporting both one-to-one and many-to-one relationships.

Here’s a summary of how to achieve this, along with the script you can use in your Jira Data Center incoming sync:

How it works:

  • The remote side sends the Jira issue key or ID in a field (e.g., replica.jiraId).
  • On the Jira DC side, the incoming sync script checks if this reference exists.
  • If it does, the script looks up the existing issue (by key or ID), loads its current state, and connects the sync to that issue instead of creating a new one.
  • If the reference isn’t found, the sync fails with a clear error, preventing duplicates.
  • If no reference is sent, Exalate creates a new issue as usual.

Key points:

  • This method supports both one-to-one and many-to-one mappings.
  • All remote items referencing the same Jira issue will sync to that issue.
  • Be mindful that with many-to-one, the last update wins for fields like summary or status.
  • If you only have a custom field value (not the key or ID), you can use a JQL search to find the issue first.

Sample Jira Data Center incoming sync script:

import com.atlassian.jira.component.ComponentAccessor

final String PROJECT_KEY = "PROJ" // Replace with your default project key

Map typemap = [
  "Bug": "Bug" // Map remote type names to Jira issue types
]

String typeName = typemap[replica.type?.name] ?: debug.error("Value ${replica.type?.name} Not found in type map.")

def issueManager = ComponentAccessor.issueManager
def mainIssue = null

// Find the existing Jira issue by the ID or key sent by the remote side
if (firstSync && replica.jiraId) {
    def ref = replica.jiraId.toString().trim()   // Jira ID (12345) or key (PROJ-123)
    mainIssue = ref.isLong() ? issueManager.getIssueObject(ref as Long) : issueManager.getIssueByCurrentKey(ref)
    if (!mainIssue) {
        debug.error("Jira issue '${ref}' not found, cannot connect.")
    }
}

if (firstSync && mainIssue) {
    // Use the existing issue's project/type; fall back to defaults
    issue.projectKey = mainIssue.project?.key ?: PROJECT_KEY
    issue.typeName   = mainIssue.issueType?.name ?: typeName

    // Load the existing Jira issue as Exalate sees it
    def issueKey = new com.exalate.basic.domain.BasicIssueKey(mainIssue.id, mainIssue.key)
    def hubObjectHelper = ComponentAccessor
        .getOSGiComponentInstanceOfType(com.exalate.api.hubobject.IHubObjectHelperFactory.class)
        .get(new com.exalate.basic.domain.BasicSemanticVersion(1, 0, 0))
    def hubIssue = hubObjectHelper.getHubIssueOnOutgoingProcessor(issueKey)

    // Fill "before" and "after"; Maps are copied so the rule's changes are detected as diffs
    hubIssue.getProperties().each { k, v ->
        issueBeforeScript[k] = v
        issue[k] = (v instanceof Map) ? new LinkedHashMap(v) : v
    }

    // Connect
    issueBeforeScript.id = null
    issue.id  = mainIssue.id
    issue.key = mainIssue.key
    issue.customKeys.somethingIsChanged = true

} else if (firstSync) {
    issue.projectKey = PROJECT_KEY
    issue.typeName   = typeName
}

// Your normal incoming mappings go below (summary, description, comments, ...)

What to customize:

  • Replace "PROJ" with your default project key.
  • Adjust the typemap to match your remote type names and Jira issue types.
  • Make sure the remote outgoing script sends the Jira ID or key in replica.jiraId.

For more details and context, check out the official documentation:
https://docs.exalate.com/docs/connect-to-an-existing-jira-issue

This approach should help you avoid duplicates and keep your syncs tidy, whether you’re mapping one or many remote items to a single Jira issue.