You are using an older browser that might negatively affect how this site is displayed. Please update to a modern browser to have a better experience. Sorry for the inconvenience!

Change Data Capture


  • Change Data Capture enables you to integrate your Salesforce data with external systems. It is powerful feature that will send notification every time a record is created, updated, deleted, or undeleted.  
  • It will capture all field changes for all records with regardless of sharing rules. 
  • Using the publisher/subscriber model, CDC sends notifications to subscribers whenever a data change occurs in Salesforce. Notification messages are sent to the event bus to which clients can subscribe through a channel. 
  • Once you subscribe to these events, you will get a notification whenever a new event is generated, the notification will include a payload in JSON format which will have all the details about the event like: 
  • SObject name – where the changed happened ex: Account, opportunity 
  • Record ID’s – List of record id’s which are changed 
  • Change Type – Create/Update/Delete/Undelete 
  • Changed Fields – Fields which are modified 
  • User – The User who performed the change 
  • Timestamp – When the change happened 
  • You can use this payload to perform your operation.  

SAMPLE PAYLOAD 

{ 

  “schema”: “-pszPCNGMHqUPU1ftkjxEA”, 

  “payload”: { 

    “ChangeEventHeader“: { 

  “commitNumber“: 10751862227089, 

      “commitUser“: “0052v00000d9NRxAAM”, 

      “sequenceNumber“: 1, 

      “entityName“: “Leave_Application__c“, 

      “changeType“: “UPDATE”, 

      “changedFields“: [“LastModifiedDate“,”Rejected__c“], 

      “changeOrigin“: “com/salesforce/api/soap/47.0;client=SfdcInternalAPI/”, 

      “transactionKey“: “0002696a-07b8-4267-785e-0107141f9ed3”, 

      “commitTimestamp“: 1569436136000, 

      “recordIds“: [ 

        “a052v00000mwQnPAAU” 

      ] 

    }, 

    “LastModifiedDate”:”2019-12-30T10:36:45Z”, 

  “Rejected__c”:false, 

    “OwnerId”: “005RM000001vI4mYAE”, 

    “CreatedById“: “005RM000001vI4mYAE”, 

    “LastModifiedById“: “005RM000001vI4mYAE”, 

  }, 

  “event”: { “replayId“: 1} } 

Definition of Payload Characteristic:  

Keyword Definition 
ChangeEventHeader Contains information about the event like created user, type of update, modified fields etc., 
EntityName Contains the name of the standard or custom object. In our example, it is Opportunity. 
changeType Contains the operation that caused the change like CREATE, UPDATE, DELETE, UNDELETE 
changedFields Contains which fields were changed in that operation. Available only at the time of updating a record. It will be null when the record is created for the first time. 
changeOrigin Contains the client id (origin) which initiated this change 

Possible way to subscribe Change Event as follows 

  • Asynchronous Trigger 
  • Lightning Component 
  • Lightning Web Component 
  • EMP Connector 

Asynchronous Trigger 

  • Asynchronous Apex Triggers help to reduce the Apex Transaction time. 
  • These triggers run asynchronously after the database transaction is completed. The trigger fires after the change event message is published. 

After enabling the object for Change Data Capture, you can create an After Insert trigger in the Developer Console in the same way a Standard/Custom Object trigger is created. 

Pre-requisite: 

To Enable Change Data Capture 

  • Go to: Setup-> Integrations -> Change Data Capture 
  • Choose object which you want CDC and save it. 

To reduce transaction times and limit constraints, move your complex logic or non-transactional logic to asynchronous processing using Change Data Capture and Async Apex Triggers. 

Asynchronous Apex Trigger Example 

  • Create a change event trigger that captures changes on Opportunity and create follow-up task for opportunity whose stage is closed won  
  • Enable CDC in Opportunity object 
  • Create Apex Trigger in OpportunityChangeEvent Subject (After Insert) 

Trigger : 

trigger OpportunityChangeTrigger on OpportunityChangeEvent (after insert) { 

    List<Task> tasks = new List<Task>(); 

    boolean won = false; 

    for (OpportunityChangeEvent event : Trigger.new) { 

system.debug(‘>>Received Change Event<<‘); 

system.debug(‘trigger.New’+trigger. New); 

system.debug(‘>>EntityName<<‘+event.ChangeEventHeader.getEntityName()); 

system.debug(‘>>ChangeOrigin<<‘+event.ChangeEventHeader.getChangeOrigin()); 

system.debug(‘>>ChangeType<<‘+event.ChangeEventHeader.getChangeType()); 

system.debug(‘>>RecordIds<<‘+event.ChangeEventHeader.getRecordIds()); 

system.debug(‘>>TransactionKey<<‘+event.ChangeEventHeader.getTransactionKey()); 

system.debug(‘>>IsTransactionEnd<<‘+event.ChangeEventHeader.getIsTransactionEnd()); 

system.debug(‘>>SequenceNumber<<‘+event.ChangeEventHeader.getSequenceNumber()); 

system.debug(‘>>CommitTimestamp<<‘+event.ChangeEventHeader.getCommitTimestamp()); 

system.debug(‘>>CommitUser<<‘+event.ChangeEventHeader.getCommitUser()); 

system.debug(‘>>CommitNumber<<‘+event.ChangeEventHeader.getCommitNumber()); 

system.debug(‘>>NulledFields<<‘+event.ChangeEventHeader.getnulledFields()); 

system.debug(‘>>Difffields<<‘+event.ChangeEventHeader.getdifffields()); 

system.debug(‘>>Opportunity Name<<‘+event.Name);         

EventBus.ChangeEventHeader header = event.ChangeEventHeader; 

        if (header.changetype == ‘UPDATE’ && event.IsWon) { 

            Task t = new Task( 

                Subject = ‘Follow up on won opportunities: ‘ + header.recordIds, 

                OwnerId = header.commituser, 

                WhatId = header.recordIds[0] 

            );              

            Tasks.add(t); 

            won = true; 

        }          

        if (won) { 

            insert Tasks; 

        } 

    } 

}  

Normally in Asynchronous trigger, one cannot track debug log in developer console. Because debug logs are created under the Automated Process entity, enable debug logs in Setup for this entity to be collected. 

  • Open Setup in a new tab 
  • From Setup, enter Debug Logs in the Quick Find box, then select Debug Logs. 
  • Click New. 
  • For Traced Entity Type, select Automated Process. 
  • Select the time period to collect logs for and the debug level. Click Save 

Lightning Component : 

<!– EMP API component –> 

Use Case: 

Get live notifications when the record that you are viewing gets modified  

LeaveApplication: 

<aura:component implements=”force:appHostable,flexipage:availableForAllPageTypes, 

                            flexipage:availableForRecordHome,force:hasRecordId, 

                            forceCommunity:availableForAllPageTypes,force:lightningQuickAction” access=”global” > 

<!– EMP API component –> 

<lightning:empApi aura:id=”empApi” /> 

<!– Calls the onInit controller function on component initalization –> 

    <aura:handler name=”init”  value=”{!this}” action=”{!c.onInit}”/> 

<!– aura subscription –> 

    <aura:attribute name=”subscription” type=”Map” />      

<aura:attribute name=”response”  type=”String”   default=”Change Event reponses will be displayed here!”/> 

    <lightning:card title=”empApi” iconName=”action:log_event”> 

    <!– Lightning Input –> 

        <div class=”slds-m-left_xx-small”> 

    <lightning:input aura:id=”channel”  label=”Enter Subscription Channel” name=”channel”  type=”text”/> 

        </div> 

    <!– Lightning Button –> 

      <div class=”slds-m-top_x-small slds-m-left_xx-small”>   

    <lightning:button label=”Subscribe”  onclick=”{! c.subscribe }” variant =”brand”/> 

    <lightning:button label=”Unsubscribe”  onclick=”{! c.unsubscribe }”   disabled=”{!empty(v.subscription)}” variant=”brand”/>    

        </div> 

        </lightning:card> 

</aura:component>  

LeaveApplication.Js 

({ 

 onInit : function(component, event, helper) { 

        // Get the empApi component 

        const empApi = component.find(’empApi’);           

        // Register error listener and pass in the error handler function 

        empApi.onError($A.getCallback(error => { 

            // Error can be any type of error (subscribe, unsubscribe…) 

            console.error(‘EMP API error: ‘, error); 

        })); 

    },  

    // Invokes the subscribe method on the empApi component 

    subscribe : function(component, event, helper) { 

        // Get the empApi component 

        const empApi = component.find(’empApi’); 

        // Get the channel from the input box 

        const channel = component.find(‘channel’).get(‘v.value’); 

        console.log(‘Channel name’+channel); 

        // Replay option to get new events 

        const replayId = -1;  

            // Subscribe to an event 

        empApi.subscribe(channel, replayId, $A.getCallback(eventReceived => { 

            // Process event (this is called each time we receive an event) 

            console.log(‘Received event: ‘, JSON.stringify(eventReceived, null, 2)); 

            var myResponse = component.get(‘v.response’); 

            console.log(‘myResponse’+myResponse); 

            component.set(‘v.response’, myResponse + ‘<br/><br/><pre>’+ JSON.stringify(eventReceived, null, ‘  ‘)+'</pre>’);          

            // Show Pop-Up Message 

            console.log(‘toast msg’); 

            var toastEvent = $A.get(“e.force:showToast”); 

            toastEvent.setParams({ 

            “title”: “Success”, 

                “message”: “Message! “+JSON.stringify(eventReceived), 

                “type”: “success”, 

                “mode”: “sticky” 

            }); 

            toastEvent.fire();           

            })) 

        .then(subscription => { 

            // Confirm that we have subscribed to the event channel. 

            // We haven’t received an event yet. 

            console.log(‘Subscribed to channel: ‘, subscription.channel); 

            // Save subscription to unsubscribe later 

            component.set(‘v.subscription’, subscription); 

        }); 

    }, 

    // Invokes the unsubscribe method on the empApi component 

    unsubscribe : function(component, event, helper) { 

        // Get the empApi component 

        const empApi = component.find(’empApi’); 

        // Get the subscription that we saved when subscribing 

        const subscription = component.get(‘v.subscription’);  

        // Unsubscribe from event 

        empApi.unsubscribe(subscription, $A.getCallback(unsubscribed => { 

          // Confirm that we have unsubscribed from the event channel 

          console.log(‘Unsubscribed from channel: ‘+ unsubscribed.subscription); 

          component.set(‘v.subscription’, null); 

        })); 

    } 

})  

SubLeaveApplication.cmp  

<aura:component implements=”force:appHostable,flexipage:availableForAllPageTypes, 

                            flexipage:availableForRecordHome,force:hasRecordId, 

                            forceCommunity:availableForAllPageTypes,force:lightningQuickAction,force:hasSObjectName” access=”global” > 

<!– This will get the current page recordId –> 

    <aura:attribute name = “recordId” type = “String” /> 

    <aura:attribute name = “sObjectName” type = “String” /> 

    <!– Notification Library –> 

    <lightning:notificationsLibrary aura:id = “notifLib”/> 

    <!– Calls the onInit controller function on component initalization –> 

    <aura:handler name = “init” value = “{!this}” action = “{!c.doInit}”/> 

<!– EMP API component –> 

    <lightning:empApi aura:id=”empApi”/> 

</aura:component>  

SubLeaveApplication.Js 

({ 

    doInit : function(component, event, helper) {           

        // Get the empApi component. 

        var empApi = component.find(“empApi”); 

        // Get the channel from the input box. 

        var channel = ‘/data/’; 

        var sObjectName = component.get(‘v.sObjectName’); 

        console.log(‘sObjectName’+sObjectName); 

        if (sObjectName.endsWith(‘__c’)) { 

            // Custom object 

            channel = channel + sObjectName.substring(‘0’, sObjectName.length-3) + ‘__ChangeEvent’; 

            console.log(‘channel@@@’+channel); 

        } 

        else { 

            // Standard object 

            channel = channel + sObjectName + ‘ChangeEvent’; 

            console.log(‘channel@@@’+channel); 

        } 

        console.log(‘channel@@@’+channel);            

        var replayId = ‘-1’;          

        // Callback function to be passed in the subscribe call. 

        // After an event is received, this callback prints the event 

        // payload to the console. 

        var callback = function (message) { 

            console.log(“Received [” + message.channel + 

                        ” : ” + message.data.event.replayId + “] payload=” + 

                        JSON.stringify(message.data.payload));              

            // ChangeEventHeader fields 

            var entityName = message.data.payload.ChangeEventHeader.entityName; 

            var modifiedRecords = message.data.payload.ChangeEventHeader.recordIds; 

            var changeType = message.data.payload.ChangeEventHeader.changeType; 

            var changeOrigin = message.data.payload.ChangeEventHeader.changeOrigin; 

            var transactionKey = message.data.payload.ChangeEventHeader.transactionKey; 

            var sequenceNumber = message.data.payload.ChangeEventHeader.sequenceNumber; 

            var isTransactionEnd = message.data.payload.ChangeEventHeader.isTransactionEnd; 

            var commitTimestamp = message.data.payload.ChangeEventHeader.commitTimestamp; 

            var commitUser = message.data.payload.ChangeEventHeader.commitUser; 

            var commitNumber = message.data.payload.ChangeEventHeader.commitNumber;              

            // SObject Modified fields 

            /* var employeeRecordID = message.data.payload.ChangeEventHeader.recordIds[0]; 

            var employeeName = message.data.payload.Name; 

            var First_Name__c = message.data.payload.First_Name__c; 

            var Last_Name__c = message.data.payload.Last_Name__c; 

            var Tenure__c = message.data.payload.Tenure__c; 

            var CreatedDate = message.data.payload.CreatedDate; 

            var CreatedById = message.data.payload.CreatedById; 

            var LastModifiedDate = message.data.payload.LastModifiedDate; 

            var OwnerId = message.data.payload.OwnerId;*/ 

            var result = ‘neither approved nor rejected’;              

            if(message.data.payload.Accepted__c == true && message.data.payload.Accepted__c != undefined 

               && message.data.payload.Accepted__c != null){ 

                result = ‘Ápproved‘; 

            } 

            else if(message.data.payload.Accepted__c == false && message.data.payload.Accepted__c != undefined 

                    && message.data.payload.Accepted__c != null) 

            { 

                result = ‘Rejected’; 

            } 

            var messagedatas = ‘This Record is already ‘+result;              

            var currentRecordId = component.get(‘v.recordId‘); 

            var userId = $A.get(“$SObjectType.CurrentUser.Id“) 

            console.log(‘not user’+message.data.payload.LastModifiedById != userId); 

            if(message.data.payload.LastModifiedById != userId){               

                component.find(‘notifLib’).showNotice({ 

                    “variant”: “info”, 

                    “header”: “Updated Record!”, 

 “message”: messagedatas, 

                }); 

            }  

        }.bind(this); 

        // Error handler function that prints the error to the console. 

        var errorHandler = function (message) { 

            console.log(“Received error “, message); 

        }.bind(this);          

        // Register error listener and pass in the error handler function. 

        empApi.onError(errorHandler);          

        // Subscribe to the channel and save the returned subscription object. 

        empApi.subscribe(channel, replayId, callback).then(function(value) { 

            console.log(“Subscribed to channel ” + channel); 

        }); 

    } 

})  

Log-In as different user and viewing record at a time, I will receive notifications

  

Demo: