2

I'm calling a trigger after insert and after update, which is calling an Apex Class which hits an endpoint. The triggers are working as expected and firing to an endpoint when a checkbox is selected after insert and when a checkbox is updated after update on contact creation.

I'm getting an error in the Apex Jobs > status detail section as follows:

First error: Update failed. First exception on row 0 with id 0035E000013uqdCQAQ; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, LIFT_ContactTrigger: execution of AfterUpdate

caused by: System.AsyncException: Database.executeBatch cannot be called > from a batch start, batch execute, or future method. Trigger.LIFT_ContactTrigger: line 9, column 1

trigger LIFT_ContactTrigger on Contact (after insert, after update) {
    list<contact> lstContact = new list<contact> ();
    For(Contact c : trigger.new){
        if(c.Send_to_Onboarding_Portal__c){
            lstContact.add(c);
            LIFT_ContactBatchCreation cbc = new LIFT_ContactBatchCreation(lstContact);
            id batch = DataBase.executeBatch(cbc,1);
        }
       
    }
    
}

global class LIFT_ContactBatchCreation implements Database.Batchable<sObject>, Database.AllowsCallouts {
    private List<Contact> lstContact;
    public LIFT_ContactBatchCreation(List<Contact> lsacc) {
        System.debug('constructor batch class, ' + lsacc.size());
        this.lstContact = lsacc;
    }
    global Database.QueryLocator start(Database.BatchableContext bc) {
        System.debug('start batch class, ' + lstContact.size());
        String soqlQuery = 'SELECT Account.Branch_Id__c, Account.Parent_Account_Name__c, Account.Date_became_Customer__c, Account.Go_Live_Date__c, Account.Name, FirstName, LastName, Email, Id From Contact where Id IN :lstContact';
        return Database.getQueryLocator(soqlQuery);
    }
    global void execute(Database.BatchableContext bc, List<Contact> scope){
        System.debug('execute batch class, ' + scope.size());
        for (Contact contact : scope) {
            try {
                System.debug(contact.Account.Name);
                System.debug(contact.Account.Branch_Id__c);
                String accountName = contact.Account.Name;
                String branchId = contact.Account.Branch_Id__c;
                String firstName = contact.FirstName;
                String lastName = contact.LastName;
                String email = contact.Email;
                Date goLiveDate = contact.Account.Go_Live_Date__c;
                Date startDate = contact.Account.Date_became_Customer__c;
                String parentAccountName = contact.Account.Parent_Account_Name__c;
                //TODO - move to separate class for the callout
                Http http = new Http();
                HttpRequest request = new HttpRequest();
                //TODO - make named credential more generic so can easily be deployed between environments
                request.setEndpoint('callout:Glofox_Portal/api/branches/fromSalesforceDemo');
                request.setMethod('POST');
                request.setHeader('Content-Type', 'application/json;charset=UTF-8');
                // Set the body as a JSON object
                request.setBody('{"branchId":"'+branchId+'", "parentAccountName":"'+parentAccountName+'", "name":"'+accountName+'", "goLiveDate":"'+goLiveDate+'","startDate":"'+startDate+'" ,"admins":[{"firstName":"'+FirstName+'","lastName":"'+LastName+'","email":"'+email+'"}]}');
                HttpResponse response = http.send(request);
                System.debug(response.getBody());
                // Parse the JSON response
                if (response.getStatusCode() != 200) {
                    //TODO - insert error handling and/or retyr logic here
                    //TODO - log exception in Salesforce including error code and reason
                    System.debug('The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getStatus());
                } else {
                    //TODO - update status field in Salesforce on success and update externalId
                    System.debug(response.getBody());
                }
            }
            catch (Exception e) {
                System.debug('Error-' + e.getMessage());
                //Log exception
            }
        }
        update scope;
    }
    global void finish(Database.BatchableContext bc){
    }
}

Any help would be much appreciated.

Thanks

2 Answers 2

4

this error is occurring due to this sequence of operations.

  1. Some user or automation updated record
  2. trigger executes and calls the future method
  3. future method ( or batch from a trigger) executes and updates the record
  4. The trigger runs again in response to DML by the future method and calls the execute batch method
  5. so the trigger is now in the future context and you can not execute batch from here.

You can not call the future from the future and batch from future context. So you need to stop the execution of batch when the trigger is in a future context.

You need to add System.isFuture() and System.isBatch() checks in your trigger. You can stop the execution of the batch job like this.

trigger LIFT_ContactTrigger on Contact (after insert, after update) {
    // skip the trigger logic to execute batch if its in future context
    if(!System.isFuture() && !System.isBatch()){
        list<contact> lstContact = new list<contact> ();
        for(Contact c : trigger.new){
            if(c.Send_to_Onboarding_Portal__c){
                lstContact.add(c);
            }
        }
        if(!lstContact.isEmpty()){
            DataBase.executeBatch(new LIFT_ContactBatchCreation(lstContact),1);
        }
    }
}
8
  • 1
    Thank you very much Rahul! I have been trying to figure a solution all day. thanks for your insight!
    – con
    Commented Jul 1, 2020 at 15:10
  • This is a good and accurate answer, but I do want to call out that it's a risky strategy. You may discover down the road that other functionality you build into your Apex trigger isn't working in future contexts because it's easy to forget about this kind of blocker. It's ideal to scope these kind of escape valves as narrowly as possible.
    – David Reed
    Commented Jul 1, 2020 at 17:56
  • @DavidReed Do you mean I should wrap the code in an if block instead of returning from the trigger, right? So any logic we want to execute in the future/batch scope can be kept out of that block. Commented Jul 1, 2020 at 17:59
  • 1
    @con I just realised that you are calling executeBatch in a for-loop. You need to update that and its a serious issue. Also, I have changed my approach little bit. You need to update the code according to that. Please find the updated code in the answer. Commented Jul 1, 2020 at 18:08
  • 1
    @rahulgawale & David, thank you both for your help and guidance. I have updated the code to include the IF statement and all is working correctly with my API. thanks
    – con
    Commented Jul 2, 2020 at 12:21
4

Your batch class is causing Contact trigger invocation, which then attempts to recursively enqueue the batch class - that's not allowed.

Here's where it happens:

    update scope;

You actually don't need this as written, because you don't mutate the Contacts at all. However, it fires a Contact Update DML, to which your trigger responds:

trigger LIFT_ContactTrigger on Contact (after insert, after update) {
                                                      ^^^^^^^^^^^^

Your batch class doesn't clear Send_to_Onboarding_Portal__c, so your conditional is true, and you end up reinvoking the batch class:

    if(c.Send_to_Onboarding_Portal__c){

The fix appears to me to be to clear that field in your batch class when the callout completes, handling errors as appropriate for your business intent. (You'd then leave the update in place).

1
  • Thanks again for your Help, David! much appreciated
    – con
    Commented Jul 1, 2020 at 15:10

You must log in to answer this question.

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