0

I am having some difficulties to call multiple callouts from a future method which is calling from a Process Builder.

I am getting the below error,

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

My scenario is something different that I am trying to call an external System to send messages to the configured Mobile Number,

Actually the scenario is as follows,

When we update a service appointment status to a specific value we need to send an sms to the configured mobile number,

  1. We have a Process Builder calling an InvocableMethod as follows,

     @InvocableMethod(label='SendSMS' description='Send SMS when Service Appointment is dispatched')
     global static void innerSendingSMS(List<Id> saIds) {
     System.debug(LoggingLevel.INFO, '---------------' + saIds);
    
     List<ServiceAppointment> lstSerApp = [SELECT Id, Contact.MobilePhone FROM ServiceAppointment WHERE Id IN :saIds WITH SECURITY_ENFORCED];
    
     List<String> smsDetails = new List<String>();
    
     String smsDetail = '';
     for(ServiceAppointment sa : lstSerApp) {
         if(String.isNotBlank(sa.Contact.MobilePhone)) {
             smsDetail += sa.Contact.MobilePhone + '?' + sa.Id.getSObjectType().getDescribe().getName() + '?' + sa.Id;
         } else {
             smsDetail += 'empty' + '?' + sa.Id.getSObjectType().getDescribe().getName() + '?' + sa.Id;
         }
    
         smsDetails.add(smsDetail);
     }
    
     sendingSMS(smsDetails);
     }
    

to make a callout to send sms using the external system we implement a future method from the InvocableMethod,

@future(callout = true)
global static void sendingSMS(List<String> smsDetails) {
    SendSMS sa = new SendSMS();
    for(String str : smsDetails) {
        if(str.substringBefore('?').equals('empty')) {
            SendSMS.sendingSMS(str, 'Service Appointment does not have the Contact or Contact Phone!!');
        } else {
            SendSMS.sendingSMS(str, 'Successfully Dispatched the Service Appointment!!');
        }
    }
}

In the sendingSMS method of SendSMS we are getting the backend response and insert and update records as per our need.

public void sendingSMS(String smsDetails, String body) {
    
    TwilioRestClient client = new TwilioRestClient(AccountSid__c, twilioCustomSetting.AuthToken__c);

    Map<String,String> params = new Map<String,String> {
        'To'   => finalFormattedInfo.substringBefore('?'),
        'From' => twilioCustomSetting.From_Number__c,
        'Body' => body,
        'StatusCallback' => Status_Callback_URL__c
    };

    TwilioRestResponse response = client.request('https://api.twilio.com/2010-04-01/Accounts/'+ twilioCustomSetting.AccountSid__c +'/Messages.json', 'POST', params);

    System.debug(LoggingLevel.INFO, '<---The Twilio Rest Response as text--->' + response.getResponseText());

    System.debug(LoggingLevel.INFO, '<---The Twilio Rest Response is--->' + response);

    Map<String, Object> twilioResponse = (Map<String, Object>) JSON.deserializeUntyped(response.getResponseText());

    List<SMS__c> lstSMS = buildSMSRecord(response, twilioResponse, finalFormattedInfo);

    if(lstSMS.size() > 0) {
        SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                                                            AccessType.CREATABLE,
                                                                            lstSMS);                                        
        insert securityDecision.getRecords();
    }
    // if(response.isError()) {
    //     throw new TwilioRestException(String.valueOf(twilioResponse.get('message')));        
    // }
}

RestResource to update the SMS status,

@RestResource(urlMapping='/smsResponse')
global without sharing class SP_Twilio_SMSManager implements 
SP_Abstract_RestCallbackInterface {

/**
 * @description         The POST method to send updated details by other systems.
 * 
 */
@HttpPost
global static void postCallback() {

    TwilioConfig__c twilioCustomSetting = TwilioConfig__c.getOrgDefaults();
    String expectedSignature = RestContext.request.headers.get('X-Twilio-Signature');
    String url = twilioCustomSetting.Status_Callback_URL__c;
    Map <String, String> params = RestContext.request.params;
    System.debug(LoggingLevel.INFO, '<---Return Response--->' + params);
    System.debug(LoggingLevel.INFO, '<---The Host URL--->' + RestContext.request.headers.get('Host'));
    System.debug(LoggingLevel.INFO, '<---The Callback URL at the Custom setting--->' + twilioCustomSetting.Status_Callback_URL__c);

    //Validate signature.
    if (!TwilioAPI.getDefaultClient().validateRequest(expectedSignature, url, params)) {
        RestContext.response.statusCode = 403;
        RestContext.response.responseBody = Blob.valueOf('Failure! Rcvd '+expectedSignature+'\nURL '+url+'\nHeaders'+RestContext.request.headers);
        return;
    }

    RestContext.response.responseBody = Blob.valueOf('ok');

    List<SMS__c> lstSMSToUpsert = new List<SMS__c>();
    SMS__c smsDetails = new SMS__c();

    smsDetails.Message_SID__c = params.get('MessageSid');
    smsDetails.Status__c = params.get('MessageStatus');

    lstSMSToUpsert.add(smsDetails);

    List<Id> lstUpsertedSMSRecordIds = new List<Id>();
    Schema.SObjectField messageSID = SMS__c.Fields.Message_SID__c;
    Database.UpsertResult[] upsertResult = Database.upsert(lstSMSToUpsert, messageSID, false);
    for (Database.UpsertResult sr : upsertResult) {
        if (sr.isSuccess()) {
            // Operation was successful
            lstUpsertedSMSRecordIds.add(sr.getId());
        }
    }

    System.debug(LoggingLevel.INFO, '<---lstUpsertedSMSRecordIds--->' + lstUpsertedSMSRecordIds);

    if(lstUpsertedSMSRecordIds.size() > 0) {
        SMS__c upsertSMS = [SELECT Id, Status__c, Service_Appointment__r.Id, Service_Appointment__r.SMS_Status__c FROM SMS__c WHERE Id IN :lstUpsertedSMSRecordIds limit 1];

        if(upsertSMS != null && upsertSMS.Service_Appointment__r != null) {
            upsertSMS.Service_Appointment__r.SMS_Status__c = upsertSMS.Status__c;
            update upsertSMS.Service_Appointment__r;
        }
    }
}
}

It's working fine with a record, But it is giving uncommitted work pending error with bulk records.

My question is Can we achieve our need using the above implementation or any other option to sort it out.

1

Your solution would work fine if you were not doing DML after getting response. I have handled these kind of situation using batch class of execution size 1.

There is limitation in Salesforce that you cannot any callout once you do the DML operation in the same transaction.

The batch will make sure that each callout will have their own transaction and you can avoid error :-

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

All you have to do is call the batch class instead of future method.

Note:- You need to also make sure that you are not firing the batch class multiple times as process builder may result in recursive loop. To have better control, you can move to triggers as well instead of using process builder.

1
  • This is the only option I have to do it as I can't wait until all the callouts finish to do DML as per @HungryBeagle suggestion. – Hariprasath Mar 17 at 6:27
0

The issue is that you have a for loop wrapped around the callout and object update. One way to fix it would be to do this:

 @InvocableMethod(label='SendSMS' description='Send SMS when Service Appointment is dispatched')
 global static void innerSendingSMS(List<Id> saIds) {
 System.debug(LoggingLevel.INFO, '---------------' + saIds);

 List<ServiceAppointment> lstSerApp = [SELECT Id, Contact.MobilePhone FROM ServiceAppointment WHERE Id IN :saIds WITH SECURITY_ENFORCED];

 List<String> smsDetails = new List<String>();

 String smsDetail = '';
 for(ServiceAppointment sa : lstSerApp) {
     if(String.isNotBlank(sa.Contact.MobilePhone)) {
         smsDetail += sa.Contact.MobilePhone + '?' + sa.Id.getSObjectType().getDescribe().getName() + '?' + sa.Id;
     } else {
         smsDetail += 'empty' + '?' + sa.Id.getSObjectType().getDescribe().getName() + '?' + sa.Id;
     }

     smsDetails.add(smsDetail);
 }

 for(String curSMSDetails : smsDetails)
     sendingSMS(curSMDetails);
 }

Of course, you will want to remove the List argument to sendingSMS, and change it to just a string for that to work.

One issue that could arise is that you may exceed the callout limit.

In this case, your SendingSMS.SendSMS should do all of the callouts first, and then do all of the updates.

@future(callout = true)
global static void sendingSMS(List<String> smsDetails) {
    SendSMS sa = new SendSMS();
    List<SObject> accessInserts = new List<SObject>();
    for(String str : smsDetails) {
        if(str.substringBefore('?').equals('empty')) {
            accessInserts.addAll(SendSMS.sendingSMS(str, 'Service Appointment does not have the Contact or Contact Phone!!'));
        } else {
            accessInserts.addAll(SendSMS.sendingSMS(str, 'Successfully Dispatched the Service Appointment!!'));
        }
    }
    insert accessInserts;
}

And this method would be changed as well:

public List<SObject> sendingSMS(String smsDetails, String body) {
    
    TwilioRestClient client = new TwilioRestClient(AccountSid__c, twilioCustomSetting.AuthToken__c);

    Map<String,String> params = new Map<String,String> {
        'To'   => finalFormattedInfo.substringBefore('?'),
        'From' => twilioCustomSetting.From_Number__c,
        'Body' => body,
        'StatusCallback' => Status_Callback_URL__c
    };

    TwilioRestResponse response = client.request('https://api.twilio.com/2010-04-01/Accounts/'+ twilioCustomSetting.AccountSid__c +'/Messages.json', 'POST', params);

    System.debug(LoggingLevel.INFO, '<---The Twilio Rest Response as text--->' + response.getResponseText());

    System.debug(LoggingLevel.INFO, '<---The Twilio Rest Response is--->' + response);

    Map<String, Object> twilioResponse = (Map<String, Object>) JSON.deserializeUntyped(response.getResponseText());

    List<SMS__c> lstSMS = buildSMSRecord(response, twilioResponse, finalFormattedInfo);

    if(lstSMS.size() > 0) {
        SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                                                            AccessType.CREATABLE,
                                                                            lstSMS);                                        
        return securityDecision.getRecords();
    }
    // if(response.isError()) {
    //     throw new TwilioRestException(String.valueOf(twilioResponse.get('message')));        
    // }
}

I had a similar process to this, and I had to reorganize my code so that all of the callouts were done, I saved the results from all, and then I updated all of the records.

I could not see from the code you provided how each call is dependent on each other, but if neither of these work, your last option is to reformat your code so that it implements schedulable, and schedule the first call, complete everything, and then schedule the next call, etc. This requires quite a bit in ensuring the limits are maintained, and you'd have to change how you query and process each record.

4
  • In our case we can’t wait till all the call outs to finish to insert/ update records, because the callout is callback to a RestResource to update the status of the created record, so it required to insert/ update before the next callout. The main issue is the External service accepts one request to send sms, if we need to send sms to multiple mobile numbers then we need to request multiple times. – Hariprasath Mar 12 at 16:50
  • Could you post the code for the sendingSMS method? – HungryBeagle Mar 12 at 17:59
  • Added the code. In buildSMSRecord we build the SMS record from the external systems response. – Hariprasath Mar 15 at 10:30
  • I can understand what you are saying here, But you can see in the sendingSMS method we are calling the external system with a callback URL, If you have insert an SMS record only the Callback request will upsert the SMS via SP_Twilio_SMSManager RestResource, otherwise it will insert two SMS record for one ServiceAppointment. That's the reason why we can't wait until all the callout to finish to insert SMS. – Hariprasath Mar 16 at 7:51
0

I have modified my code as below working perfectly fine to do the multiple callouts from the Process Builder,

The Invocable method,

@InvocableMethod(label='SendSMS' description='Send SMS when Service Appointment is dispatched')
global static void innerSendingSMS(SMSRequest[] strInput) {
    System.debug(LoggingLevel.INFO, '---------------' + strInput);

    if(strInput.size() > 0) {
        for(SMSRequest smsReq : strInput) {
            if(String.isNotBlank(smsReq.toNum)) {
                String smsDetails = smsReq.toNum + '?' + Id.valueOf(smsReq.recordId).getSObjectType().getDescribe().getName() + '?' + smsReq.recordId;
                sendingSMS(smsDetails, smsReq.body);
            }
        }
    }
}

The Wrapper class to get multiple inputs from Process Builder

global class SMSRequest {

    @InvocableVariable(required=true)
    global String toNum;    
    
    @InvocableVariable(required=true)
    global String body;

    @InvocableVariable(required=true)
    global String recordId;
}

The future method to do the callout,

@future(callout = true)
global static void sendingSMS(String smsDetails, String body) { 
    SendSMS.sendingSMS(smsDetails, body);
}
1
  • @sanket, Is there any issue that we can encounter in the above approach compare to the Trigger and Batch (In this solution we have a limitation in concurrent batch count). – Hariprasath Mar 25 at 4:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.