Preserving Order While Synchronizing a Multi-Select Picklist with a Text Field in Salesforce

The Problem

In a recent Salesforce implementation, we encountered an interesting data consistency issue involving two related fields on a standard object:

  • Field A → Multi-Select Picklist (source of truth)
  • Field B → Text Area (formatted display version used for downstream processes like badge printing)

The expectation was simple:

Whenever Field A changes, Field B should reflect the same values in a user-friendly, formatted way.

However, over time, inconsistencies started appearing:

  • Field A populated but Field B was null
  • Field B populated but Field A was null
  • Values mismatching between both fields
  • Correct values but wrong order
  • Inconsistent behavior across user-created and system-updated records

The issue became critical because Field B was used in an external-facing process.

Initial Implementation

Originally, the synchronization logic looked something like this:

record.FieldB__c = 
String.isBlank(inputValue)
? null
: inputValue.replace(';', ', ');

How It Worked

  • Salesforce stores multi-select picklists as semicolon-separated values: Value1;Value2;Value3
  • The logic simply replaced ; with , for display: Value1, Value2, Value3

At first glance, this seemed correct.

Why Issues Started Appearing

After investigation, we identified several architectural gaps:

Synchronization Was Not Centralized

The logic only executed in specific Apex flows.
If records were updated through:

  • Data imports
  • Admin edits
  • Background jobs
  • Automation
  • Integrations

The synchronization logic did not always execute.

This caused stale or mismatched data.

Entire Field Was Being Rebuilt

The original logic completely overwrote Field B whenever Field A changed.

This introduced a more subtle problem:

  • Any manually set ordering was lost
  • Custom formatting was overwritten
  • Existing sequence was rearranged

For processes like badge printing, order matters.

Multi-Select Picklists Do Not Guarantee Order

Salesforce multi-select picklists:

  • Store values as strings
  • Do not reliably preserve user selection order
  • May return values alphabetically depending on context

This meant that simply regenerating Field B from Field A was insufficient when order had business meaning.

Investigation Approach

The investigation involved:

  1. Reviewing all user entry points
  2. Checking Apex classes and automation
  3. Verifying whether Field B was referenced anywhere else
  4. Analyzing how updates were happening across different channels

Key finding:

Field B was only being assigned inside selective Apex logic and was not globally enforced.

This explained the inconsistencies.

The Solution

Instead of rebuilding the text field entirely, we redesigned the synchronization logic with order preservation in mind.

New Requirements

When Field A changes:

  • ❌ Do NOT overwrite existing order
  • ❌ Do NOT rebuild the entire string
  • ✅ Remove only values removed from Field A
  • ✅ Append newly added values at the end
  • ✅ Preserve existing order
  • ✅ Respect manual edits

Updated Trigger Approach

The improved logic works as follows:

  1. Convert Field A (semicolon-separated) into a Set.
  2. Convert existing Field B (comma-separated) into a List.
  3. Remove only values no longer present in Field A.
  4. Append newly added values to the end.
  5. Reconstruct the string.

Simplified version:

trigger RecordSync on ObjectName (before insert, before update) {for (ObjectName rec : Trigger.new) {        if (Trigger.isUpdate &&
rec.FieldA__c == Trigger.oldMap.get(rec.Id).FieldA__c) {
continue;
}
Set<String> newValues = new Set<String>();
if (!String.isBlank(rec.FieldA__c)) {
newValues.addAll(rec.FieldA__c.split(';'));
}
List<String> existingOrder = new List<String>();
if (!String.isBlank(rec.FieldB__c)) {
for (String val : rec.FieldB__c.split(',')) {
existingOrder.add(val.trim());
}
}
List<String> updatedOrder = new List<String>(); // Preserve existing order
for (String val : existingOrder) {
if (newValues.contains(val)) {
updatedOrder.add(val);
newValues.remove(val);
}
}
// Append newly added values
for (String val : newValues) {
updatedOrder.add(val);
}
rec.FieldB__c =
updatedOrder.isEmpty()
? null
: String.join(updatedOrder, ', ');
}
}

Why This Works Better

This implementation:

  • Preserves user-defined order
  • Prevents destructive overwrites
  • Handles delta changes instead of rebuilding everything
  • Works regardless of update source (UI, import, automation)
  • Ensures long-term data integrity

Testing Strategy

The test class validates:

  • Insert behavior
  • Order preservation
  • Removal of deleted values only
  • Appending of newly added values
  • Clearing when source field is cleared

This ensures stable behavior across future changes.

Key Takeaways

  1. Never blindly mirror a multi-select picklist into a text field- Order and formatting can matter more than expected.
  2. Avoid destructive rebuild- Delta-based synchronization is safer than full overwrite logic.
  3. Centralize enforcement- Triggers (or before-save flows) are better than page-specific logic.
  4. Consider order preservation early- If order has business meaning (badges, display names, exports), design accordingly from the start.

Final Thought

Sometimes the issue is not a “bug” it’s a design assumption that doesn’t scale across real-world update paths.

Thoughtful synchronization logic can turn fragile behavior into a stable, predictable system.

In this article:
Share on social media: