New to Exalate/Groovy Script and I need help

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!

Hi and welcome to the Exalate Community!

Thanks for sharing your question or insight, we appreciate your contribution.

A member of the Exalate team or community will review this post and respond as soon as possible.

In the meantime, feel free to explore related topics or join other conversations. We’re glad to have you here!

Hi Ben!

I’ve reviewed the Salesforce → Jira scripts you shared, and there don’t appear to be any significant issues in the code itself. Honestly, you’ve done a great job tackling this on your own, so kudos for sticking with it.

At this point, I’d suggest starting the debugging process directly in Jira to track down where things might be going off-course:

  1. Check the Entity Sync Status in Jira

    • Head over to the sync status for the relevant issue.

    • Look for the remote replica payload to confirm whether all expected values are arriving from Salesforce.

    • This will also give you a clear look at how the object is structured.

  2. Debug the Jira Incoming Script

    • Once you’ve confirmed the incoming data, start testing the incoming script.

    • Use debug.error() to stop execution at strategic points, kind of like setting checkpoints so you know exactly where the problem spawns.

    • This will help pinpoint the exact spot that needs tweaking.

For more details, these guides are a great sidekick to your debugging adventure:

Kind regards,
Ariel