I’m new to Exalate and Groovy script, trying to set up a sync between Salesforce and Jira on a custom SF Object named “Ticket__c” with certain conditions and a good amount of mapping since the data from Salesforce isn’t always directly comparable with Jira.
I have been using AIDA and ChatGPT for days, but continue running into error after error. I fix one, only to find another. I’m hoping to have some of you with more experience than me review my script and help me fix some obvious errors that the AI isn’t picking up on.At the moment, I’m working on the Salesforce –> Jira portion of the sync, I’m sure I’ll have to post again when I work on the other direction, but I’ll focus on fixing this portion with this post first.
Here’s my Salesforce Outgoing sync script:
def allowedTypes = ['Incident', 'SF_Bug', 'Task', 'SF_Enhancement']
if (entity.entityType == "Ticket__c" && allowedTypes.contains(entity.Record_Type_Name__c)) {
//Assignee mapping requires specific logic
replica.sfOwnerId = entity.OwnerId
replica.sfOwnerName = entity.OwnerName
if (entity.Owner && entity.Owner.Email) {
replica.ownerEmail = entity.Owner.Email
} else {
replica.ownerEmail = null // fallback to unassigned
// Owner email could not be resolved for OwnerId: ${entity.OwnerId}
}
if (entity.OwnerId?.startsWith('005')) {
replica.ownerType = 'User'
} else if (entity.OwnerId?.startsWith('00G')) {
replica.ownerType = 'Queue'
} else {
replica.ownerType = 'Unknown'
}
//Non-conditional field mapping
replica.actualCompletionDate = entity.Actual_Completion_Date__c
replica.attachments = entity.attachments
replica.blockedReason = entity.Blocked_Reason__c
replica.businessImpact = entity.Business_Impact__c
replica.comments = entity.comments
replica.description = entity.Summary__c
replica.dispositionNotes = entity.Disposition_Notes__c
replica.enhancementLevel = entity.Enhancement_Level__c
replica.sfRecordId = entity.Id
replica.summary = entity.Headline__c
replica.location = entity.Location__c
replica.sfTicketNumber = entity.Name
replica.numberOfUsersAffected = entity.Number_of_Users_Affected__c
replica.otherTypeDetails = entity.Other_Type_Details__c
replica.priority = entity.Priority__c
replica.quantity = entity.Quantity__c
replica.recordTypeId = entity.RecordTypeId
replica.sfRecordTypeName = entity.Record_Type_Name__c
replica.releaseNotes = entity.Release_Notes__c
replica.sfUser = entity.Requested_By__r?.Name
replica.requestedByOther = entity.Requested_By_Other__c
replica.resolution = entity.Resolution__c
replica.severity = entity.Severity__c
replica.solutionDesign = entity.Solution_Design__c
replica.status = entity.Status__c
replica.stepsToReproduce = entity.Steps_to_Reproduce__c
replica.sfTestRecord = entity.Test_Record__c
replica.ticketVisibility = entity.Ticket_Visibility__c
replica.trainingRequirement = entity.Training_Requirement__c
replica.type = entity.Type__c
replica.userStory = entity.User_Story__c
replica.vendorCaseNumber = entity.Vendor_Case_Number__c
replica.vendorNotes = entity.Vendor_Notes__c
replica.vendorSupportLink = entity.Vendor_Support_Link__c
replica.vendor = entity.Vendor_Party__r?.Name
replica.workaroundDetails = entity.Workaround_Details__c
replica.workaroundInPlace = entity.Workaround_in_Place__c
}
This is my Jira Incoming sync script:
if (firstSync) {
// === Determine Issue Type based on Salesforce Record Type ===
def recordType = replica.sfRecordTypeName
def issueTypeName
if (["SF_Bug", "Incident"].contains(recordType)) {
issueTypeName = "Incident"
} else if (["SF_Enhancement", "Task"].contains(recordType)) {
issueTypeName = "Request"
} else {
issueTypeName = "Task" // fallback
}
def defaultProjectKey = "SFIT"
issue.projectKey = defaultProjectKey
issue.typeName = nodeHelper.getIssueType(issueTypeName, defaultProjectKey)
}
// === Map Record Type to Team UUID (ID) ===
def rtToTeamIdMap = [
"Incident" : "62f2cff2-f909-4c1d-b2b8-ac7f8b6fc243", // CJL IT
"Task" : "62f2cff2-f909-4c1d-b2b8-ac7f8b6fc243", // CJL IT
"SF_Bug" : "7a9dfb4d-0b43-4692-bf27-903019b3a2fb", // CJL Salesforce
"SF_Enhancement" : "7a9dfb4d-0b43-4692-bf27-903019b3a2fb" // CJL Salesforce
]
// Default fallback to CJL IT if record type is missing/unmapped
def teamId = rtToTeamIdMap[replica.sfRecordTypeName] ?: "62f2cff2-f909-4c1d-b2b8-ac7f8b6fc243"
// Apply directly to the custom field
issue.customFields."Team".value = teamId
if (replica.ownerId?.startsWith("00G")) {
issue.assignee = null // Queue or group
} else if (replica.ownerEmail) {
issue.assignee = nodeHelper.getUserByEmail(replica.ownerEmail)
}
// === Unconditional field mappings ===
// These fields will be synced regardless of whether it's first sync or update
issue.summary = replica.summary ?: "No Summary Provided"
issue.description = replica.description ?: "No Description Provided"
issue.priority = nodeHelper.getPriority(replica.priority?.name ?: "Medium")
issue.status = replica.status?.name ?: "New"
issue.resolution = replica.resolution?.value ?: null
issue.customFields."Actual Completion Date" = replica.actualCompletionDate ?: null
issue.customFields."Blocked Reason".value = replica.blockedReason ?: null
issue.customFields."Business Impact".value = replica.businessImpact ?: null
issue.customFields."Disposition Notes".value = replica.dispositionNotes ?: null
issue.customFields."Enhancement Level".value = replica.enhancementLevel?.value ?: null
issue.customFields."SF Record ID".value = replica.sfRecordId ?: null
issue.customFields."Location PL".value = replica.location?.value ?: null
issue.customFields."SF Ticket Number".value = replica.sfTicketNumber ?: null
issue.customFields."Number of Users Affected".value = replica.numberOfUsersAffected ?: null
issue.customFields."Other Type Details".value = replica.otherTypeDetails ?: null
issue.customFields."Owner ID".value = replica.sfOwnerId ?: null
issue.customFields."Owner Name".value = replica.sfOwnerName ?: null
issue.customFields."Quantity".value = replica.quantity ?: null
issue.customFields."Record Type ID".value = replica.recordTypeId ?: null
issue.customFields."Record Type Name".value = replica.recordTypeName ?: null
issue.customFields."Release Notes".value = replica.releaseNotes ?: null
issue.customFields."SF User".value = replica.sfUser?.value ?: null
issue.customFields."Requested By Other".value = replica.requestedByOther ?: null
issue.customFields."Solution Design".value = replica.solutionDesign ?: null
issue.customFields."Steps to Reproduce".value = replica.stepsToReproduce ?: null
issue.customFields."Test Record".value = replica.sfTestRecord ?: false
issue.customFields."Training Requirement".value = replica.trainingRequirement?.value ?: "Undetermined"
issue.customFields."Type".value = replica.type?.value ?: null
issue.customFields."User Story".value = replica.userStory ?: null
issue.customFields."Vendor Case Number" = replica.vendorCaseNumber ?: null
issue.customFields."Vendor Notes".value = replica.vendorNotes ?: null
issue.customFields."Vendor Support Link".value = replica.vendorSupportLink ?: null
issue.customFields."Vendor PL".value = replica.vendor?.value ?: null
issue.customFields."Workaround Details".value = replica.workaroundDetails ?: null
issue.customFields."Workaround In Place" = replica.workaroundInPlace?.value ?: null
issue.customFields."Ticket Visibility".value = replica.ticketVisibility?.value ?: "Everyone"
/*
Custom Fields (CF)
To add incoming values to a Jira custom field, follow these steps:
1/ Find the Display Name of the CF. Note: If you have multiple custom fields with the same name,
then you can sync it using the custom field ID instead of its name. Know more about the steps here:
https://docs.exalate.com/docs/how-to-synchronize-custom-fields-in-jira-cloud
2/ Check how the value is coming over from the source side, by checking the "Entity Sync Status"
of an issue in sync and then selecting the "Show Remote Replica".
3/ Add it all together like this:
issue.customFields."CF Name".value = replica.customFields."CF Name".value
*/
/*
Status Synchronization
For Status sync, we map the source status, to the destination status with a hash map.
The syntax is as follows:
def statusMap = [
"remote status name": "local status name"
]
Go to Entity Sync Status, put in the entity key, and it will show you where to find the remote replica
by clicking on Show remote replica.
def statusMap = [
"New" : "Open",
"Done" : "Resolved"
]
def remoteStatusName = replica.status.name
issue.setStatus(statusMap[remoteStatusName] ?: "Add a default status in these double quotes")
*/
/*
User Synchronization (Assignee/Reporter)
Set a Reporter/Assignee from the source side. If the user can't be found, set a default user.
You can also use this approach for custom fields of the type User
def defaultUser = nodeHelper.getUserByEmail("default@exalate.com")
issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
*/
/*
Comment Synchronization
Impersonate comments with the original author. The sync will work if the user already exists
in the local instance.
Note: Don’t forget to remove the original comments sync line if you are using this approach.
issue.comments = commentHelper.mergeComments(issue, replica) {
it.executor = nodeHelper.getUserByEmail(it.author?.email)
}
*/
// Exalate API Reference Documentation: https://docs.exalate.com/docs/exalate-api-reference-documentation
Any help or advice will be welcomed!