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);
            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);


    return updatedList;

2 Answers 2


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);

I had a similar issue and there are similar questions, like these ones:

https://developer.salesforce.com/forums/?id=906F00000008v2hIAA https://developer.salesforce.com/forums/?id=906F0000000MNEOIA4

The idea is to use Queueable to edit a record after it's been updated, like so:

  • Record is updated
  • After update trigger is fired
  • Queueable apex fires on a separate transaction, makes a web service calls
  • when the web service calls returns, you update the original record with some values

This was resulting in the read-only error mentioned here. This surprised me because the Queueable context in theory should not know about the after trigger context, as they run in separate transactions.

The solution to this problem is as documented by sfdcfox and in one of the links I mentioned above:

  • if you are working with a list, deep clone the list with the first parameter set to true, and use the new list in your DML statements
  • i you are working with a single record, just create a new instance of that object passing the original record id, ie:

    Account cloneAccount = new Account(Id=originalAccount.Id); cloneAccount.Name = 'new name'; update cloneAccount;

You must log in to answer this question.

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