[SalesForce] PB + Queueable System.FinalException: Record is read-only

I've set up a Process (in Process Builder) which calls an invocable method, but cannot figure out why I'm getting this error:

System.FinalException: Record is read-only

I'm familiar with the concept that in a Trigger "after" context, records are read-only. But the method being called from the Process ultimately enqueues a job, which is supposed to asynchronously update the same record which triggered the Process.

Has anyone else run into this? I have no idea why it would be happening, it shouldn't even be the same transaction.

I could post code, but I'm not sure which part is relevant here. The exception is thrown when I do [SObject].put(fieldName, value); within the queueable's execute() method. It's almost as if the system is validating the record's read/write access at the time the queueable is instantiated rather than when it would ultimately execute the DML.

  1. Edit 1: more context (below Edit 2)
  2. Edit 2: I should mention that
    calling the invocable method anonymously works as expected.
  3. Edit 3: I tried calling a Flow, which started with a Wait element, then invoked the method. SAME error. It's as if the transaction is staying open and all future DML associated to the records which triggered the transaction is prevented. Is that documented somewhere? How can I achieve an asynch update to the same records?

The Process is:

  • Lead Object [field is changed]
  • Invocable Method, single param for Lead sObject

Class with invocable method:

public without sharing class UpdateAccountFieldsOnPeople_Invocable {

public class UpdateRequest {
    @InvocableVariable(Label = 'Account')
    public Account account;

    @InvocableVariable(Label = 'Lead')
    public Lead lead;

    @InvocableVariable(Label = 'Leads')
    public List<Lead> leads;

    @InvocableVariable(Label = 'Contact')
    public Contact contact;

    @InvocableVariable(Label = 'Contacts')
    public List<Contact> contacts;
}

@InvocableMethod(Label = 'Update Account-based Fields on Leads or Contacts')
public static List<Id> updateRequests(List<UpdateRequest> requests) {

    List<Id> jobIds = new List<Id>();

    for(UpdateRequest request : requests) {
        jobIds.add(System.enqueueJob(new UpdateFieldValuesQueueable(request)));
    }

    return jobIds;
}

private static List<SObject> getLeadsAndContacts(Id accountId) {
    //some code
}

public class UpdateFieldValuesQueueable implements Queueable {

    private UpdateRequest vRequest;

    public UpdateFieldValuesQueueable(UpdateRequest request) {
        //some code
    }

    public void execute(QueueableContext context) {

        List<SObject> sObjectList = new List<SObject>();
        if(vRequest.leads == null && vRequest.contacts == null && vRequest.account != null) {
            sObjectList = getLeadsAndContacts(vRequest.account.Id);
            update UpdateAccountFieldsOnPeople.updateFields(sObjectList, vRequest.account);
        }
        else if(vRequest.leads != null || vRequest.contacts != null) {
            sObjectList = SObjectUtils.mergeLists(vRequest.leads, vRequest.contacts);
            //SUCCESSFULLY GETS HERE & CALLS THIS METHOD
            update UpdateAccountFieldsOnPeople.updateFields(sObjectList, vRequest.account);
        }
        else {}
    }
}
}

Method which ultimately causes the exception:

public List<SObject> setFieldValues(List<SObject> sobjList, Map<String,Object> fieldNameValueMap, IfInvalid invalidAction) {

    List<SObject> updatedList = new List<SObject>();

    for(SObject sobj : sobjList) {

        String typeString = sobj.getSObjectType().getDescribe().name;

        for(String fieldName : fieldNameValueMap.keySet()) {
            try {
                //EXCEPTION THROWN HERE
                sobj.put(fieldName, fieldNameValueMap.get(fieldName));
            }
            catch(Exception e) {
                if(invalidAction == IfInvalid.LOG) {
                    System.debug(typeString + ' does not have a field called ' + fieldName + '. ' + typeString + ' with Id: ' + sobj.Id + ' was not updated.');
                }
                else {
                    throw new TypeException('Error: ' + typeString + ' does not have a field called ' + fieldName, e);
                }
            }
        }

        updatedList.add(sobj);
    }

    return updatedList;
}

Best Answer

I suspect that you've unwittingly copied the "isReadOnly" flag on the sObject records. This flag, which can't be accessed or modified directly, is responsible for causing the FinalException that you're getting. You should be able to get around this by deep-cloning the list first:

public List<SObject> setFieldValues(List<SObject> sobjList, Map<String,Object> fieldNameValueMap, IfInvalid invalidAction) {
  sobjList = sobjList.deepClone(true, false, false);
  ...
Related Topic