SfSkills Domains Docs GitHub

Home / data / field-history-tracking

field-history-tracking

Use when enabling, configuring, or querying Salesforce field history tracking to audit changes to field values over time. Covers enabling tracking on objects and fields, the 20-field-per-object limit, 18-month data retention, querying standard History sObjects (AccountHistory, OpportunityFieldHistory, custom __History), and troubleshooting missing records. NOT for Shield Field Audit Trail, FieldHistoryArchive, or keeping history past 18 months — use security/field-audit-trail. NOT for CreatedDate, LastModifiedDate, or SystemModstamp behavior — use admin/system-field-behavior-and-audit.

Open the skill folder on GitHub


Field History Tracking

This skill activates when a practitioner needs to enable Salesforce Field History Tracking on standard or custom objects, query history sObjects to audit field value changes, or troubleshoot cases where expected history records are absent. It does NOT cover Shield Field Audit Trail (use field-audit-trail) or Event Monitoring (use security skills).


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Concept 1: Enabling Field History Tracking

Field History Tracking is enabled in two steps. First, the feature must be activated on the object. For standard objects this is done in Setup > Object Manager > [Object] > Fields & Relationships > Set History Tracking. For custom objects the same path applies. Enabling history on the object does not automatically track any fields — each field must be individually checked.

Second, within the object's history tracking settings, select up to 20 fields to track. Salesforce stores any change to a tracked field as a new row in the object's History sObject (e.g., AccountHistory for Account, MyObject__History for a custom object MyObject__c). The History related list on page layouts surfaces these rows to end users without any additional code.

The History related list is configured separately in Setup > Page Layouts. It must be added to the layout for users to see it inline on a record. Without the related list, data is still stored and queryable via SOQL — it is just not visible on the page.

Concept 2: History sObjects and Query Structure

Every object with Field History Tracking enabled has a corresponding History sObject. Standard objects use named sObjects: AccountHistory, ContactHistory, LeadHistory, CaseHistory, etc. Custom objects use the pattern ObjectApiName__History (e.g., Project__History for Project__c).

Naming exception — Opportunity: the field-history sObject for Opportunity is OpportunityFieldHistory, not OpportunityHistory. The object named OpportunityHistory is a different object — it stores opportunity stage/pipeline history (StageName, Amount, Probability, CloseDate, ForecastCategory) and has no Field/OldValue/NewValue columns. A field-history query must target OpportunityFieldHistory. (A handful of other standard objects follow the same <Object>FieldHistory pattern — always confirm the exact sObject name in the object reference.)

Key fields on all History sObjects:

FieldDescription
IdUnique row identifier
ParentIdId of the record that was changed (foreign key to the parent object). On standard-object history sObjects this is exposed under the object-specific name — e.g., AccountId on AccountHistory, ContactId on ContactHistory — rather than the literal ParentId. Custom __History objects use ParentId.
FieldAPI name of the field that changed (e.g., "AnnualRevenue", "StageName"). Typed as a restricted picklist.
DataTypeData type of the field that changed (picklist). Present on standard-object history records; useful when interpreting OldValue/NewValue, which are stored as anyType.
OldValueValue before the change (anyType). Null if the record was created (new record) or if the field was previously blank.
NewValueValue after the change.
CreatedByIdId of the user who made the change
CreatedDateTimestamp when the change occurred

For a new record creation event, Field is set to "created", OldValue is null, and NewValue is null. For a field change, Field contains the tracked field API name.

Example SOQL — all history for a single Account record:

SELECT Field, OldValue, NewValue, CreatedById, CreatedDate
FROM AccountHistory
WHERE AccountId = '001XXXXXXXXXXXX'
ORDER BY CreatedDate DESC
LIMIT 200

Example SOQL — who changed StageName across all Opportunities in the last 30 days (note the OpportunityFieldHistory sObject — OpportunityHistory would fail here because it has no Field column):

SELECT OpportunityId, OldValue, NewValue, CreatedById, CreatedDate
FROM OpportunityFieldHistory
WHERE Field = 'StageName'
  AND CreatedDate = LAST_N_DAYS:30
ORDER BY CreatedDate DESC

Example SOQL — history for a custom object field:

SELECT Field, OldValue, NewValue, CreatedById, CreatedDate
FROM MyObject__History
WHERE ParentId = 'a01XXXXXXXXXXXX'
  AND Field = 'Status__c'
ORDER BY CreatedDate ASC

History sObjects are available in Salesforce Reports via the standard [Object] History report type when history is enabled on the object.

Relationship queries against history objects. A history object is the child of its tracked parent, so SOQL can traverse the relationship in either direction and avoid a second round-trip to fetch parent fields.

The child-to-parent relationship name depends on the object type: custom __History objects use Parent (matching their ParentId field), while standard history objects use the object name — SELECT Account.Name FROM AccountHistory (matching the AccountId field). Writing Parent.Name against a standard history object fails to compile; see Gotcha 5.

Scope note. These relationship-query forms work only on standard and custom History objects. Shield's FieldHistoryArchive enforces a restricted SOQL grammar and does not accept them — see Gotcha 6 and the field-audit-trail skill.

Concept 3: 18-Month Retention and Data Loss

Salesforce automatically purges Field History records older than 18 months on a rolling basis. This is a platform-enforced limit and cannot be extended for standard history tracking. There is no setting to disable this purge. If a field change occurred 19 months ago, that history row no longer exists.

Organizations that require history beyond 18 months have two documented options:

  1. Shield Field Audit Trail — A paid Salesforce Shield add-on that extends retention up to 10 years and stores archived records in the FieldHistoryArchive sObject. Requires a Shield or standalone FAT license. See the field-audit-trail skill.
  2. Custom logging via Apex or Flow — Capture field changes into a custom object at the time of change using a before-update trigger or Flow. This custom log is not subject to the 18-month purge and can be retained indefinitely. The tradeoff is implementation and maintenance overhead, and it does not backfill historical changes that occurred before the custom logger was deployed.

Mode 1: Enabling Field History Tracking

When to use: An object has no history tracking configured and audit requirements call for capturing future field changes.

Steps:

  1. Go to Setup > Object Manager.
  2. Select the target object (e.g., Account, or a custom object).
  3. Click Fields & Relationships, then Set History Tracking (for standard objects) or Fields & Relationships > Set History Tracking (for custom objects).
  4. Check the Enable [Object] History checkbox at the top.
  5. Select up to 20 fields to track. The platform enforces the 20-field limit — attempting to save with more than 20 checked will produce a validation error.
  6. Click Save.
  7. Add the [Object] History related list to the relevant page layouts in Setup > Page Layouts.

History records begin accumulating from the moment tracking is enabled. No retroactive history is created for changes that occurred before enablement.


Mode 2: Auditing Existing Field History Configuration

When to use: Reviewing which objects and fields currently have history tracking enabled, or confirming configuration correctness before a compliance review.

Steps to audit without code:

  1. Navigate to Setup > Object Manager > [Object] > Fields & Relationships > Set History Tracking. The checkboxes show which fields are currently tracked.
  2. For custom objects, the Salesforce Metadata API returns history configuration in the CustomObject metadata type under enableHistory (boolean) and in CustomField metadata under trackHistory (boolean).

Using the Metadata API to audit history configuration programmatically:

# Retrieve custom object metadata including history settings
sfdx force:source:retrieve -m "CustomObject:MyObject__c"

In the retrieved XML, confirm <enableHistory>true</enableHistory> on the object and <trackHistory>true</trackHistory> on each tracked field.


Mode 3: Troubleshooting Missing History Records

When to use: A practitioner reports that a known field change is not appearing in the history related list or SOQL results.

Diagnosis checklist:

SymptomLikely CauseResolution
No history rows at all for an objectHistory tracking not enabled on the objectEnable tracking in Object Manager
Specific field change missingField not selected in history tracking settingsEnable the specific field (check 20-field limit)
History existed but is now goneChange occurred more than 18 months agoData is purged; use Shield FAT for future retention
History missing for a deleted recordRecord was hard-deletedHistory is deleted with the record
Formula field changes not trackedFormula fields are not eligible for trackingUse a regular field or custom log pattern
Changes by an integration user not visibleIntegration user not the CreatedById — may be a System contextCheck trigger context; history records the running user
New record creation not showing field valuesFor new records, OldValue and NewValue are null for the "created" rowExpected behavior; creation event stores Field = "created" only

Decision Guidance

SituationRecommended ApproachReason
Track field changes for 18 months or lessStandard Field History TrackingNo additional license required; built-in and reportable
Track field changes for more than 18 monthsShield Field Audit Trail (field-audit-trail skill)Standard tracking purges at 18 months — FAT is the only Salesforce-native extension
Need more than 20 tracked fields on one objectShield Field Audit Trail (up to 60 fields) or custom loggingStandard FHT hard-caps at 20 fields per object
Formula, roll-up, or long text area field must be auditedCustom logging via Apex trigger or FlowThese field types are ineligible for standard history tracking
Custom object field history in SOQLQuery ObjectApiName__HistoryPlatform auto-generates this sObject when history is enabled
Pull a record plus its field-change log in one round-tripParent-to-child subquery SELECT ..., (SELECT OldValue, NewValue FROM Object__History) FROM Object__cHistory is a child of its parent; a subquery avoids a second query and keeps rows grouped by record
Report the change alongside parent context (owner, name)Child-to-parent traversal to parent fields — Parent.* on custom __History, the object name (Account.*) on standard historyOne query returns OldValue/NewValue plus parent fields; no separate lookup needed
Build a report of field changesUse the built-in [Object] History report typeHistory sObjects are available natively in Report Builder
Audit field changes after record deletionConsider Shield FAT or custom logging pre-deletionHistory is deleted along with the parent record

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking field history tracking work complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. Checkbox fields store true/false, not the field label — When a checkbox field is tracked, OldValue and NewValue contain the literal strings "true" or "false", not any display label. SOQL comparisons must use these string values. Reports and list views typically display these correctly, but programmatic comparisons that check for "Yes" or "Checked" will fail silently.
  2. New record creation sets OldValue and NewValue to null — When a record is first created, a history row is written with Field = "created", OldValue = null, and NewValue = null. Field values at creation time are NOT captured as individual field-level rows. If the requirement includes capturing initial values at creation, a custom trigger or Flow must store these separately at insert time.
  3. History tracking is not retroactive — Enabling history on a field records changes from that point forward only. No history rows are created for changes that occurred before enablement. Stakeholders who expect to see historical changes from before the feature was turned on will be disappointed; communicate this limitation clearly before enablement.

Output Artifacts

ArtifactDescription
Field history enablement decisionsPer-object table of enabled fields, tracking status, and field type eligibility
SOQL query setParameterized queries for AccountHistory, OpportunityFieldHistory, custom __History objects
Troubleshooting diagnosisRoot cause analysis for missing history records
Retention advisoryDocumentation of the 18-month limit and recommended escalation to Shield FAT if needed

Package files