We have a Checklist addon on both of our Jira Cloud instances and we want to sync this from one of the instances to the other, but we also want to be able to make changes on the other instance and have that being synced back (two-way sync).
The checklist addon works in a way that you can add items to the checklist directly in the ticket, and the entire checklist is exposed with a single text field through custom fields (using its own markup language to populate the checklists). So if we add an item to the checklist, the text field is updated to include the new item:
“customFields”: {
“Checklist Text”: {
“id”: 10171,
“name”: “Checklist Text”,
“uid”: “10171”,
“type”: “TEXT”,
“value”: “# Default checklist\n* [done] test\n* [done] Test 2\n* [open] Test 3\n* [open] Test 4\n* [open] Test 5\n* [open] Test 6”
}
},
On both sites we have the following in our outgoing scripts:
replica.customFields.“Checklist Text” = issue.customFields.“Checklist Text”
On both sites we have the following in our incomming scripts:
issue.customFields.“Checklist Text”.value = replica.customFields.“Checklist Text”?.value
If we make a simple change (ex. mark one of the checklist items as “done”, or add a new item to the checklist), the change is synced over to the other site with no problem. However, if we make multiple rapid changes (ex. mark several of the checklist items as “done” within a few seconds) it creates a sync loop where only some of the changes are being transferred over to the other site, and since there is a two-way sync they don’t match and it creates an infinite loop where they are trying to correct each other. The end result is that there is always something in the sync que for those tickets and both sites have the incorrect value of the checklist text field (ex. only 3/5 is marked as done, when it was supposed to be 5/5 of the items marked as done).
If we comment out the outgoing sync for the Checklist Text-field on one of the sites (disabling two-way sync), the loop is broken and all rapid changes are synced over from one site to the other without issues.
How can we add support for two-way sync without introducing this loop?
Thanks for the detailed explanation! This is a classic challenge with two-way sync of custom fields that can be rapidly updated, especially when the field is a single text value representing a complex object (like a checklist). The loop happens because both sides try to “correct” each other when changes are made in quick succession, and the sync engines can’t always determine which version is the latest.
Here’s what the Exalate documentation recommends for handling this scenario:
Implement conflict resolution or change detection: Instead of always overwriting the checklist field on both sides, you can add logic to your sync scripts to only update the field if the incoming value is different and newer than the current value. This often involves using timestamps or versioning.
Use a “last updated” timestamp: If your checklist add-on or Jira custom field supports a last-modified timestamp, you can sync this as well and compare it in your incoming script. Only update the field if the incoming value is newer.
Prevent echoing changes: You can add a mechanism to detect if the change originated from the other side and avoid re-sending it back, breaking the loop.
To avoid infinite sync loops, implement logic in your sync scripts to detect and ignore changes that originated from the other side. This can be done by using a custom field or property to track the source of the last update, or by comparing timestamps.
A common approach is to use a custom field (e.g., “Last Synced By Exalate”) or a property in the replica to track the source and time of the last update. Then, in your incoming script, only apply the update if it’s newer than the current value.
Thank you for the detailed description of the issue. This is a classic race condition with two-way sync of frequently changing fields. The root cause is that when Site B receives the update and writes it locally, that write triggers Site B’s outgoing sync, which echoes the value back to Site A, and so on – creating an infinite loop when rapid changes are involved.
The solution is to use the previous. keyword in your incoming scripts on both sites. By comparing replica. (the current incoming payload) with previous. (the payload received from the remote side during the last sync), you can detect whether the remote side actually made a new change or is simply echoing back a value you sent. If the value hasn’t changed on the remote side, you skip the update entirely, which breaks the loop.
But we received the following error pointing to line 24 (def previousChecklist = previous.customFields.“Checklist Text”?.value):
Impact: RELATION
Local entity: null
Remote entity: SISM-120
Connection: …
Error type: Incoming script error
Error Creation Time: Feb 20, 2026 08:26:29
Error Detail Message: No such property: previous for class: Script15
Error Stack Trace: com.exalate.api.exception.script.ScriptException: No such property: previous for class: Script15 at com.exalate.error.services.ScriptExceptionCategoryService.categorizeProcessorAndIssueTrackerExceptionsIntoScriptExceptions(ScriptExceptionCategoryService.scala:51) at com.exalate.processor.ExalateProcessor.executeProcessor(ExalateProcessor.java:77) at com.exalate.replication.services.processor.CreateIssueProcessor.$anonfun$executeScriptRules$1(CreateIssueProcessor.scala:221) at scala.util.Try$.apply(Try.scala:210) at com.exalate.replication.services.processor.CreateIssueProcessor.executeScriptRules(CreateIssueProcessor.scala:213) at com.exalate.replication.services.processor.CreateIssueProcessor.$anonfun$createIssue$1(CreateIssueProcessor.scala:119) at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:470) at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:63) at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:100) at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18) at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94) at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:100) at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:49) at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:48) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) Caused by: javax.script.ScriptException: javax.script.ScriptException: groovy.lang.MissingPropertyException: No such property: previous for class: Script15 at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:164) at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262) at com.exalate.processor.ExalateProcessor.execute(ExalateProcessor.java:114) at com.exalate.processor.ExalateProcessor.executeProcessor(ExalateProcessor.java:75) … 17 more Caused by: javax.script.ScriptException: groovy.lang.MissingPropertyException: No such property: previous for class: Script15 at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:334) at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:161) … 20 more Caused by: groovy.lang.MissingPropertyException: No such property: previous for class: Script15 at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:67) at org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.unwrap(IndyGuardsFiltersAndSignatures.java:161) at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:318) at Script15.run(Script15.groovy:24) at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:331) … 21 more
Now rapid changes and semi-rapid changes syncs completely without any loops.
The only issue we have now is if we perform changes on both sides at the same time, but this is not that important for us to fix as it will likely never happen. It does not create any loops, but the result is incorrect.