Tracking field history in Salesforce is crucial for maintaining data compliance and auditing record updates over time. While standard related lists show field updates, building a custom Lightning Web Component (LWC) allows you to filter, format, and present old versus new values dynamically directly on record pages.
AccountHistory or OpportunityFieldHistory) via Apex, returning a clear audit table of modified fields, previous values, new values, user names, and timestamps.
Prerequisites
- Basic understanding of LWC JavaScript structure and Apex wire service patterns.
- Field History Tracking enabled on target objects in Salesforce Setup.
- Salesforce DX CLI configured or Developer Console access.
Step 1: Create the LWC Bundle
Generate a new Lightning Web Component named fieldHistoryComponent using your IDE or terminal:
sf force lightning component create --type lwc --componentname fieldHistoryComponent --outputdir force-app/main/default/lwc
Step 2: Build the HTML Template Layout
In fieldHistoryComponent.html, combine a lightning-combobox for field selection with a lightning-datatable to show history records:
<!-- fieldHistoryComponent.html -->
<template>
<lightning-card title="Field History Tracking" icon-name="standard:history">
<div class="slds-p-around_medium">
<lightning-combobox
name="fieldSelect"
label="Select Field to Audit"
value={selectedField}
options={fieldOptions}
onchange={handleFieldChange}>
</lightning-combobox>
<div class="slds-m-top_medium">
<template if:true={historyData}>
<lightning-datatable
key-field="id"
data={historyData}
columns={columns}
hide-checkbox-column>
</lightning-datatable>
</template>
<template if:true={error}>
<p class="slds-text-color_error">Unable to load field history.</p>
</template>
</div>
</div>
</lightning-card>
</template>
Step 3: Define JavaScript Controller Logic
In fieldHistoryComponent.js, declare field options, column definitions, and wire the Apex method reactively based on user interaction:
// fieldHistoryComponent.js
import { LightningElement, api, wire, track } from 'lwc';
import getFieldHistory from '@salesforce/apex/FieldHistoryController.getFieldHistory';
const COLUMNS = [
{ label: 'Old Value', fieldName: 'OldValue', type: 'text' },
{ label: 'New Value', fieldName: 'NewValue', type: 'text' },
{ label: 'Modified By', fieldName: 'CreatedByName', type: 'text' },
{ label: 'Modified Date', fieldName: 'CreatedDate', type: 'date', typeAttributes: {
year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit'
}}
];
export default class FieldHistoryComponent extends LightningElement {
@api recordId;
@track selectedField = 'Account.Name';
@track historyData;
@track error;
columns = COLUMNS;
fieldOptions = [
{ label: 'Account Name', value: 'Account.Name' },
{ label: 'Type', value: 'Account.Type' },
{ label: 'Annual Revenue', value: 'Account.AnnualRevenue' }
];
@wire(getFieldHistory, { recordId: '$recordId', fieldName: '$selectedField' })
wiredHistory({ data, error }) {
if (data) {
// Flatten parent metadata relationship fields for datatable binding
this.historyData = data.map(record => ({
id: record.Id,
OldValue: record.OldValue,
NewValue: record.NewValue,
CreatedByName: record.CreatedBy ? record.CreatedBy.Name : '',
CreatedDate: record.CreatedDate
}));
this.error = undefined;
} else if (error) {
this.error = error;
this.historyData = undefined;
}
}
handleFieldChange(event) {
this.selectedField = event.detail.value;
}
}
CreatedBy.Name. Always flatten parent relationships during array transformation (e.g. mapping record.CreatedBy.Name to a top-level key like CreatedByName) before passing data to lightning-datatable!
Step 4: Create the Apex Controller
Create an Apex class named FieldHistoryController.cls to query historical object tracking tables securely:
AccountHistory) using Field, OldValue, and NewValue fields.
// FieldHistoryController.cls
public with sharing class FieldHistoryController {
@AuraEnabled(cacheable=true)
public static List<AccountHistory> getFieldHistory(Id recordId, String fieldName) {
if (String.isBlank(recordId)) {
return new List<AccountHistory>();
}
// Extract field name without object prefix if passed
String fieldApi = fieldName.contains('.') ? fieldName.substringAfter('.') : fieldName;
return [
SELECT Id, Field, OldValue, NewValue, CreatedBy.Name, CreatedDate
FROM AccountHistory
WHERE AccountId = :recordId AND Field = :fieldApi
WITH USER_MODE
ORDER BY CreatedDate DESC
LIMIT 100
];
}
}
Step 5: Expose Target Metadata and Test
Update fieldHistoryComponent.js-meta.xml to expose the component for Record Page deployment:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
- Data Source: Queries object history tables (e.g.,
AccountHistory,ContactHistory). - Prerequisites: Field History Tracking must be enabled on target object fields in Setup.
- Apex Best Practice: Secure queries using
WITH USER_MODEand mark methods@AuraEnabled(cacheable=true)for wire optimization. - UI Component: Uses
lightning-datatablewith flattened relationship properties for seamless rendering.
Conclusion
Building a custom Field History LWC gives developers granular control over auditing presentation within Salesforce. By pairing reactive wire handlers with efficient Apex SOQL adapters, you deliver a clean, interactive history tracking experience directly on Lightning record pages.