I've built a lightning component that receives a ServiceAppointment (Field Service Lightning) record from a parent component, and leveraging this component to face a VisualForce page via an <iframe> element. The component in question is also leveraging force:recordData for some additional benefits, such as its built in recordUpdated listener.

Here's a simplified version of this component:

<aura:component implements="force:hasRecordId,flexipage:availableForAllPageTypes" access="global">
    <aura:attribute name="serviceAppointment" type="ServiceAppointment" required="true" />
    <aura:attribute name="simpleRecord" type="ServiceAppointment" required="true" />

    <force:recordData aura:id="forceRecord"
                  fields="Id,AppointmentNumber,FSL__Scheduling_Policy_Used__c,Has_Resource_Assigned__c,Status" />

    <iframe src="{!'<company url>/apex/FSL__GetCandidates?id=' + v.simpleRecord.Id}" />

Once an action happens in the FSL__GetCandidates VisualForce page, the Has_Resource_Assigned__c field has its value updated accordingly through an apex trigger with the idea the force:recordData component will pick up the change. However, the recordUpdated listener never fires, likely because the change happened from outside the scope of the component.

I already looked into lightning:empApi and saw ServiceAppointment isn't supported at this time.

Nothing short of building a listener that pings the server every N seconds, is there an option in Salesforce that is more streamlined?

Note: The VisualForce page FSL__GetCandidates is from the Field Service Lightning managed package so I have no access to its logic. Also, this component does not sit on a record page.

1 Answer 1


I managed to get everything wired up leveraging lightning:empApi and a Platform Event while moving away from the force:recordData concept.

Below is a working example that anyone can leverage with some subtle changes.

Step 1 - Create Platform Event

  • From Setup, search for Platform Events under Integrations. For me the API name is Service_Appointment_Resource_Update__e.
  • Create a new Platform Event and any custom fields you may need.

Step 2 - Publish New Platform Event

This can be done from apex anonymous for testing purposes, but the following is an example in a trigger:

Map<Id, ServiceAppointment> serviceAppointmentMap = new Map<Id, ServiceAppointment>();
Map<Id, Service_Appointment_Resource_Update__e> notificationsToPublish = new Map<Id, Service_Appointment_Resource_Update__e>();
List<AssignedResource> assignedResources = Trigger.isInsert
    ? Trigger.New
    : Trigger.Old;

for (AssignedResource res : assignedResources) {
    serviceAppointmentMap.put(res.ServiceAppointmentId, null);


for (ServiceAppointment sa : [SELECT Id, Has_Resource_Assigned__c FROM ServiceAppointment WHERE Id IN :serviceAppointmentMap.keySet()]) {
    sa.Has_Resource_Assigned__c = Trigger.isInsert;//note: if this trigger becomes expanded upon this will require being more flushed out

    notificationsToPublish.put(sa.Id, new Service_Appointment_Resource_Update__e(Message__c = 'Service Resource Changed', Is_Resource_Assigned__c = sa.Has_Resource_Assigned__c, Service_Appointment_Id__c = sa.Id));

    serviceAppointmentMap.put(sa.Id, sa);

if (!serviceAppointmentMap.isEmpty()) {
    update serviceAppointmentMap.values();

    EventBus.publish(notificationsToPublish.values());//publish all events

Step 3 - Create Lightning Component to Subscribe to Platform Event


<aura:component implements="force:hasRecordId,flexipage:availableForAllPageTypes" access="global">
    <aura:attribute name="channel" type="String" default="/event/Service_Appointment_Resource_Update__e"/>
    <aura:attribute name="subscription" type="Map"/>
    <aura:attribute name="serviceAppointments" type="ServiceAppointment[]" />

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

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


doInit : function(component, event, helper) {
    component.set('v.subscription', null);
    // Get empApi component.
    const empApi = component.find('empApi');
    // Define an error handler function that prints the error to the console.
    const errorHandler = function (message) {
        console.error('Received error ', JSON.stringify(message));
    // Register empApi error listener and pass in the error handler function.
    helper.subscribe(component, event, helper);

    helper.handleInit(component, event, helper);


    // Client-side function that 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 attribute.
        const channel = component.get('v.channel');
        // Subscription option to get only new events.
        const 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. A helper method displays the message
        // in the console app.
        const callback = function (message) {
            console.log('Event Received : ' + JSON.stringify(message));
            helper.onReceiveNotification(component, message);
        // Subscribe to the channel and save the returned subscription object.
        empApi.subscribe(channel, replayId, $A.getCallback(callback)).then($A.getCallback(function (newSubscription) {
            console.log('Subscribed to channel ' + channel);
            component.set('v.subscription', newSubscription);
    // Client-side function that invokes the unsubscribe method on the
    // empApi component.
    unsubscribe: function (component, event, helper) {
        // Get the empApi component.
        const empApi = component.find('empApi');
        // Get the channel from the component attribute.
        const channel = component.get('v.subscription').channel;
        // Callback function to be passed in the unsubscribe call.
        const callback = function (message) {
            console.log('Unsubscribed from channel ' + message.channel);
        // Unsubscribe from the channel using the subscription object.        
        empApi.unsubscribe(component.get('v.subscription'), $A.getCallback(callback));
    // Client-side function that displays the platform event message
    // in the console app and displays a toast if not muted.
    onReceiveNotification: function (component, message) {
        // Extract notification from platform event
        const newNotification = {
            time: $A.localizationService.formatDateTime(
                message.data.payload.CreatedDate, 'HH:mm'),
            message: message.data.payload.Message__c,
            serviceAppointmentId: message.data.payload.Service_Appointment_Id__c,
            isResourceAssigned: message.data.payload.Is_Resource_Assigned__c

        let serviceAppointments = component.get("v.serviceAppointments");

        serviceAppointments.forEach((sa) => {
            if (newNotification.serviceAppointmentId == sa.Id) {
                sa.Has_Resource_Assigned__c = newNotification.isResourceAssigned;

        component.set("v.serviceAppointments", serviceAppointments);

This Trailhead was the walkthrough that finally got me on the right path. The Change Capture Data methodology is not necessary. So everyone knows, the wait time is almost nonexistent and is far more lightweight and reliable than cometd appeared to be.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .