System Audit Field Integrity in SObject Cloning
Overview
Salesforce system audit fields (CreatedDate, CreatedById, LastModifiedDate, LastModifiedById) have an integrity constraint: CreatedDate must always be less than or equal to LastModifiedDate. When cloning records in Apex, clearing only some of these fields causes Salesforce to auto-assign fresh values to the cleared fields while keeping stale values on the others -- which can violate this constraint and throw a FIELD_INTEGRITY_EXCEPTION.
This article explains the root cause, why it commonly surfaces in Flow-to-Apex cloning patterns, and the simple fix: always clear all four audit fields together.
System Audit Field Integrity in SObject Cloning
When cloning SObject records in Apex -- especially records retrieved by Flows with storeOutputAutomatically=true -- system audit field values from the source record can carry over to the clone. Partially clearing these fields causes integrity violations that are difficult to diagnose.
The Problem
Consider cloning a QuoteLineItem retrieved from a Flow. The source record has:
CreatedDate: 2026-03-10 21:29:12 GMT
LastModifiedDate: 2026-03-10 21:29:12 GMT
If your Apex code clears only CreatedDate and CreatedById:
qli.put('CreatedDate', null);
qli.put('CreatedById', null);
insert qli;
Salesforce assigns a fresh CreatedDate on insert (e.g., 2026-03-11 14:26:03 GMT), but LastModifiedDate retains the old value (2026-03-10 21:29:12 GMT). The result:
FIELD_INTEGRITY_EXCEPTION: Last Modified Date (Tue Mar 10 21:29:12 GMT 2026)
before Create Date (Wed Mar 11 14:26:03 GMT 2026)
The new CreatedDate is after the stale LastModifiedDate, violating the invariant that a record cannot be modified before it was created.
Why Audit Fields Carry Over
This commonly occurs when:
-
Flow
storeOutputAutomatically=true: Get Records elements with this setting retrieve ALL fields, including system audit fields. When the Flow passes these records to an Invocable Apex method, the audit field values travel with them. -
SObject.clone() with default parameters:
record.clone(false, false, false, false)does not clear read-only timestamps by default. The fourth parameter (preserveReadonlyTimestamps) must be explicitly set tofalse. -
Manual field-by-field copying: Building a new SObject by copying fields from a source record can inadvertently include audit fields if using dynamic field iteration.
The Fix: Clear All Four Audit Fields
Always clear all four system audit fields before inserting a cloned record:
SObject clone = sourceRecord.clone(false, true, false, false);
// Clear ALL audit fields — partial clearing causes integrity violations
clone.put('CreatedDate', null);
clone.put('CreatedById', null);
clone.put('LastModifiedDate', null);
clone.put('LastModifiedById', null);
insert clone;
When all four fields are null on insert, Salesforce auto-populates them with the current timestamp and running user. The integrity constraint is satisfied because all values are consistent.
Why put() Instead of Dot Notation
System audit fields are read-only in Apex. Direct field assignment does not compile:
// Does NOT compile — read-only field
clone.CreatedDate = null;
The put() method bypasses compile-time field accessibility checks, allowing you to null out these fields before insert:
// Compiles and works — dynamic field access
clone.put('CreatedDate', null);
What About SystemModstamp?
SystemModstamp is a truly read-only system field that cannot be set or cleared even via put(). However, Salesforce ignores it on insert and auto-populates it, so it does not need to be cleared. Do not attempt to include it in your audit field cleanup.
When clone() Parameters Are Sufficient
If you control the clone operation, the SObject.clone() method's fourth parameter handles this:
// clone(preserveId, isDeepClone, preserveReadonlyTimestamps, preserveAutonumber)
SObject clone = sourceRecord.clone(false, true, false, false);
// ^^^^^ — clears audit timestamps
With preserveReadonlyTimestamps=false, Salesforce nulls out all audit fields automatically. The manual put() approach is needed when:
- Records come from a Flow (already instantiated, not cloned via
clone()) - Records are built by copying fields from a source in a loop
- You need to selectively preserve some fields while clearing audit fields
Key Takeaways
- Always clear all four audit fields (
CreatedDate,CreatedById,LastModifiedDate,LastModifiedById) together -- never partially. - Use
put()for system fields since dot-notation assignment does not compile for read-only fields. SystemModstampdoes not need clearing -- Salesforce handles it on insert.- When using
SObject.clone(), setpreserveReadonlyTimestamps=false(fourth parameter). - Watch for Flow
storeOutputAutomatically=trueas a common source of carried-over audit fields.