I'm writing a trigger for new opportunity products to populate a field Product_Category__c (on opportunity lineitem) getting the value from Product_Category_new__c (on Product).

This is my trigger:

trigger SetProductCategory on OpportunityLineItem (after insert) {

    for (OpportunityLineItem opplineitem: Trigger.new) {           
        opplineitem.Product_Category__c =  opplineitem.PricebookEntry.Product2.Product_Category_new__c;

I get this error message:

execution of AfterInsert caused by: System.FinalException: Record is read-only

I have tried with before insert;in this case there aren't errors but the value Product category it's not saved on Opportunity Line item.

I have found that:

In a after insert trigger is not allowed change field using trigger.new

Please can you suggest a solution for this? I would like to copy the value Product category (Product) on Product Category (Opportunity Product) after the insert of the new opportunity products.

2 Answers 2


The record is locked because you are catching the record after the insert DML statement has happened. So if you'd like to modify this record (or records), you will need to query anew, modify, then execute another DML statement.

Here is an example of how you could modify your trigger to work:

trigger SetProductCategory on OpportunityLineItem (after insert) {

    List<OpportunityLineItem> olis = [SELECT Id, PricebookEntry.Product2.Product_Category_new__c FROM OpportunityLineItem WHERE Id IN: Trigger.newMap.keySet()];

    for (OpportunityLineItem opplineitem: olis){
        opplineitem.Product_Category__c= opplineitem.PricebookEntry.Product2.Product_Category_new__c;

    update olis;


And of course, since it's a trigger, we want to be sure to work in batches -- that's why I'm updating a list of olis rather than an individual opp line item.

  • Thanks a lot Tim, but i get again the same error, in your code your are not changing the records in the list olis.Thanks again
    – Enry
    Commented Aug 3, 2013 at 7:31
  • Tim thanks to you i have found a solution.Check it!Thanks a lot for your help!
    – Enry
    Commented Aug 3, 2013 at 7:44
  • Oh sorry, typo on my part. I've corrected it.
    – Tim Smith
    Commented Aug 3, 2013 at 7:47
  • 3
    You could also do this in a before insert trigger if needed by getting all of the PricebookEntryID's and creating a map of PricebookEntryIDs to the PriceBookEntry records with the fields you want. Then updating the product category using that map. This would stop any update trigger from firing.
    – Eric
    Commented Mar 5, 2014 at 4:09

I know this issue is pretty old, but today I stumbled over an alternative solution by accident. As an apex developer for years, I'm avoiding database calls whenever I can, to save the Limits, so this solution might help someone to overcome data related limit exceptions.

Alternative Solution:

The records can be re-initialized as mutable objects, by serializing and deserializing the list. It runs about twice as fast as a database query, doesn't affect the query and row limits, but consumes more heap.

List<Account> accounts = (List<Account>) 
    JSON.deserialize(JSON.serialize(Trigger.new), List<Account>.class);

After some testing I came up with some details about the heap:

  • Deserialized records consum 1/4 more of the heap (don't ask me why, debugging displays two equivalent lists).
  • It also needs a lot of heap during processing (that is released after deserializing).
  • I was able to run it for about 18.000 records with selecting no more fields than their Id, before reaching the heap limit during the serializing process.
  • Fascinating find! What do you mean about "doesn't need query and row limits"? Commented Apr 8, 2019 at 7:18
  • @BrianMiller Total number of SOQL queries issued (= 100 queries) & Total number of records retrieved by SOQL queries (= 50.000 rows). With this solution, both are not affected. (I changed the wording a bit, thanks for your hint :) )
    – itsmebasti
    Commented Apr 8, 2019 at 8:53
  • Thanks for the update. I don't see a SOQL query here, so I'm not sure how I would use your new technique and how it magically doesn't count against the query & row count limits Commented Apr 8, 2019 at 13:06
  • 1
    Just compare it to the other answer. You had to re query the data, now you don't have to anymore.
    – itsmebasti
    Commented Apr 8, 2019 at 15:05
  • 1
    Better to ensure that you do the processing in the before phase if possible, of course. If you HAVE to do this, you can avoid the JSON heap issue (at the cost of more CPU usage) by doing: List<Account> accounts = new List<Account>(); for (Account account : Trigger.new) { accounts.add(new Account(Id = account.Id, TheValueToSet = someValue)); } This even allows you to do the update needed at the time you create the new in-memory instance.
    – Phil W
    Commented Oct 10, 2019 at 16:22

You must log in to answer this question.

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