What could be the best strategy in solving:

  • Send and email with various attachements when an Opportunity is Closed-Won and also save (update) the time stamp of the Email Sending in the same opportunity.

  • The email's attachements are to be calcualted based on the Opportunity's items and a PDF filled with Opportunity data.

I've been looking for:

  1. add the logic directly to the current flow of the trigger: issue is the time that can take the creation of the attachments

  2. batch class to manage all the attachment logic but what about the update of the time stamp in the opportunity and the trigger? also, what about BATCH exec when different opportunities are closed let's say during the same hour by different users?

  3. @future notation: Same as the batch, what about the updating in the opportunity time stamp?

  • if the timestamp is the time when email is sent, why do you care how long it takes to assemble the email?
    – cropredy
    Commented Mar 8, 2017 at 14:44
  • Trigger by itself is heavy right now (business logic already implemented) and I'm wondering about the updating during a trigger fired by updateing the same record.
    – joralbert
    Commented Mar 8, 2017 at 15:11

1 Answer 1


I would recommend @future here. As you mentioned, generating attachments might consume quite a bit of CPU/Heap, and it's governor consumption you can avoid.

The timestamp field update can go in either the synchronous or asynchronous code block, but it depends on its intention. If it's supposed to indicate the moment you send the email, go async.

You can update the records from within your @future method without causing your logic to fire again by correctly filtering records to only act on those which change stage to Closed Won.

public static List<Opportunity> getNewlyClosedWon
    (List<Opportunity> newRecords, Map<Id, Opportunity> oldMap)
    List<Opportunity> filtered = new List<Opportunity>();
    for (Opportunity newRecord : newRecords)
        if (newRecord.StageName == '...' && 
            newRecord.StageName != oldMap.get(newRecord.Id).StageName)
    return filtered;

Or with Selector:

public static Select.Filter isNewlyClosedWon()
    return Select.Field.hasChanged(Opportunity.StageName).andX(
        Select.Field.isEqual(Opportunity.StageName, '...')

Then in your trigger/handler code:

List<Opportunity> newlyClosedWon = OpportunityService.getNewlyClosedWon(trigger.new, trigger.oldMap);
// or with selector:
List<Opportunity> newlyClosedWon = OpportunityService.isNewlyClosedWon()
    .filter(trigger.new, trigger.oldMap);

If filters alone don't do the trick, you might need to adopt a pattern similar to the following:

public with sharing class OpportunityService
    public static Boolean shouldCallAsync()
        return !system.isFuture() && !system.isBatch() &&
            Limits.getLimitFutureCalls() > Limits.getFutureCalls();

    public static void sendEmailWithAttachmentsAsync(Set<Id> recordIds)
        sendEmailWithAttachments([SELECT ... FROM Opportunity WHERE Id IN :);
    public static void sendEmailWithAttachments(List<Opportunity> records)
        if (shouldCallAsync())
            sendEmailWithAttachmentsAsync(new Map<Id, SObject>(records).keySet());
            update records;
  • Thanks, but doing the update inside the @future method, and calling it from the beforeUpdate OR afterUpdate throws: caused by: System.DmlException: Update failed. First exception on row 0 with id 006a000001KGMXOBB2; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, OpportunityTrigger: execution of AfterUpdate caused by: System.AsyncException: Future method cannot be called from a future or batch method: OpportunityEmail.EmailManager(List<Id>)
    – joralbert
    Commented Mar 8, 2017 at 16:55

You must log in to answer this question.

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