Skip to main content

Clone-Aware Default Logic: Protecting Carried-Forward Values

A common Apex trigger pattern is to apply default values to fields when a record is created: if the field is null, populate it with a sensible default. This works perfectly for genuinely new records. But when a record is cloned, the same trigger fires on the INSERT of the clone -- and it can silently overwrite values that were deliberately carried forward from the source record.

The Problem Scenario

Suppose you have a trigger that defaults a project category based on the record type:

trigger ProjectTrigger on Project__c (before insert) {
for (Project__c proj : Trigger.new) {
// Default category based on record type
if (proj.RecordTypeId == commercialRecordTypeId) {
proj.Category__c = 'Standard Commercial';
}
}
}

This works for new records. But when a user clones a project that has Category__c = 'Premium Commercial' (a value they specifically set), the trigger overwrites it with 'Standard Commercial' because it runs unconditionally on every insert.

The user sees their carefully chosen category replaced with a default and has no idea why.

The Fix: Guard Against Non-Null Values

The simplest fix is to check whether the field already has a value before applying the default:

trigger ProjectTrigger on Project__c (before insert) {
for (Project__c proj : Trigger.new) {
// Only apply default if the field is truly empty
if (proj.RecordTypeId == commercialRecordTypeId
&& proj.Category__c == null) {
proj.Category__c = 'Standard Commercial';
}
}
}

Now, when a clone carries forward Category__c = 'Premium Commercial', the guard clause prevents the overwrite. Genuinely new records (where the field is null) still get the default.

Distinguishing New Records from Clones

Sometimes you need more nuanced behavior -- applying different logic for clones vs. truly new records. Salesforce provides the isClone() method on SObjects:

trigger ProjectTrigger on Project__c (before insert) {
for (Project__c proj : Trigger.new) {
if (proj.isClone()) {
// Clone-specific logic
proj.Status__c = 'Draft'; // Always reset status on clone
// But preserve Category__c, Owner, and other carried-forward fields
} else {
// Truly new record -- apply full defaults
if (proj.Category__c == null
&& proj.RecordTypeId == commercialRecordTypeId) {
proj.Category__c = 'Standard Commercial';
}
proj.Status__c = 'New';
}
}
}

The isClone() method returns true when the record was created via the Clone button, record.clone() in Apex, or a Flow clone action. Note that isClone() is only available in the before insert context -- by the time an after insert trigger fires, the record has been assigned its own ID and isClone() returns false.

A Broader Pattern: The Default Application Helper

For projects with many default fields, extract the logic into a helper that respects existing values:

public class ProjectDefaults {

public static void applyDefaults(List<Project__c> newProjects) {
for (Project__c proj : newProjects) {
applyIfNull(proj, 'Category__c', deriveCategory(proj));
applyIfNull(proj, 'Priority__c', 'Medium');
applyIfNull(proj, 'Region__c', deriveRegion(proj));

// Some fields should ALWAYS be reset, even on clones
proj.Approval_Status__c = 'Pending';
proj.Integration_Status__c = null;
}
}

private static void applyIfNull(SObject record, String fieldName, Object value) {
if (record.get(fieldName) == null && value != null) {
record.put(fieldName, value);
}
}

private static String deriveCategory(Project__c proj) {
// Business logic to determine default category
return proj.RecordTypeId == getRecordTypeId('Commercial')
? 'Standard Commercial'
: 'Standard Residential';
}

private static String deriveRegion(Project__c proj) {
return proj.State__c != null ? regionMap.get(proj.State__c) : 'Unassigned';
}
}

This approach makes the intention explicit: fields that use applyIfNull are clone-safe, while fields that are set directly (like Approval_Status__c) are intentionally reset regardless of cloning.

Testing Clone Scenarios

Always include clone-specific test methods:

@isTest
static void testClonePreservesCategory() {
Project__c original = new Project__c(
Name = 'Original Project',
Category__c = 'Premium Commercial'
);
insert original;

Project__c clone = original.clone(false, true, false, false);
insert clone;

Project__c result = [SELECT Category__c FROM Project__c WHERE Id = :clone.Id];
System.assertEquals('Premium Commercial', result.Category__c,
'Clone should preserve the carried-forward Category value');
}

Key Takeaways

  • Any before insert trigger that sets field values can interfere with cloning.
  • Always guard default logic with null checks: if (field == null) { field = default; }.
  • Use isClone() in before insert when you need entirely different logic for clones vs. new records.
  • Distinguish between fields that should carry forward on clone and fields that should always reset.
  • Write explicit test cases for clone scenarios -- they are easy to overlook and hard to debug in production.