-1

I have an SMS sending requirement, which involves a mix of DML and callouts. I know I can't do DML insert/update before a callout, and I can't do the callout first in my case. Explanation below:

SMS Sequence required:

  1. Callout to external system to get a link for verification purposes
  2. Create/insert a record that will receive the verification details from the link the sms recipient clicks on
  3. Create sms body that includes the link from 1 to send to the recipient who needs to do the verification - this is just a text body and does no query
  4. Callout to send the sms generated in step 3
  5. Create a Task to store the sms info sent/returned to show in the Activities timeline on the account (whoId) and the custom verification object (whatId)
  6. Check the status of the sms (via callout get)
  7. Update the Task with the sms status

LWC: In our Salesforce org I achieve the above via an LWC which

  1. Does SMS sequence 1 and 2 in one handleclick function
  2. If record create in LWC 1 returns success, then does SMS sequence 3, 4, 5
  3. If LWC 2 above returns success, then does SMS sequence 6 and 7

So the DML and callout mixed error (System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out) doesn't occur when using LWC.

Our portal is built on Heroku platform. A user might be required to do a verification and requests to do so via being sent an SMS or email. The Heroku portal does a callout to the Salesforce org using a class to run steps 1 to 7, but throws the mixed DML/Callout error at SMS 4.

If I make the class Heroku uses do point 4 as an @future(callout=true), then

  1. the mixed error doesn't happen, but there is no status returned to indicate whether it sent successfully or not so the portal user doesn't know whether the sms was sent or not (unless sent to himself)
  2. and the Task is not updated as the check doesn't run,
  3. and I can't run the check because the TaskId (or sms message id) was never returned to the portal so there's no clear id to run a query with. I could potentially use the account (recipient) id and the verification record id though.

Is there any way I can make the SMS sequence run 1 to 7 from Heroku in one call without getting the error? Somehow make 1 and 2 commit before doing the callout at 4, so not having to do my current fix using @future?

UPDATE: I've made the callout to send the SMS an @future method, which means it all works fine. The only thing is the Heroku portal needs to create another callout to retrieve the status of the SMS.

I have a background job running to check Tasks with an SMS status that have not been updated and update them (with a limit of 100 for callout limits).

1 Answer 1

1

The short answer is...

No, you can't run all of that in a single transaction. You need to adjust how you're having Heroku work, or go async in Apex

The longer answer

It's not really clear how Heroku is involved here. About all I've been able to gather is that:

  • You're using Heroku
  • To call an Apex class in Salesforce
  • To perform all of these individual steps in a single, monolithic call

I've no experience with Heroku (the costs are prohibitive), but assuming that you can't just create multiple classes for Heroku to call in sequence and that you must delegate all the work to a single class/single call into Salesforce...

Any DML (or DML-like operation, including stuff like calls to System.enqueueJob()) effectively taints the entire transaction after that point. There's no way for us to "commit" the data as the error wants us to.

It looks like Salesforce gave us Database.releaseSavepoint() in Spring '24 so that the "or rollback" part of the error message is at least possible to do, but I don't imagine it's going to be of much help to you.

If you want to make subsequent callouts, then you need to do that from a different transaction. That includes stuff like:

  • @future methods (can't be chained)
  • Queueable and/or Batchable Apex classes (can be chained)
    • Schedulable classes too
  • Platform Events (but delivery isn't guaranteed, and platform events are still rather half-baked in my opinion)
  • Using LWC to make calls to Apex or standard APIs that Salesforce provides (simplified way of thinking... every event handled in LWC/Aura can lead to a separate transaction)

In the end, you need to break things up.

  • Steps 1 and 2 can be done together (in the transaction of the initial call from Heroku)
    • DML in Step 2 taints that transaction
  • Steps 3-5 need to be a separate transaction (assuming "creating the sms body" is just creating a string in memory and doesn't involve DML).
    • DML in Step 5 taints that transaction
  • Steps 6 and 7 sound like some time needs to pass (minutes/hours/days?)

@future methods can't be chained, and you can't call Database.executeBatch() from a @future method. So I'd approach this by

  • Handling Steps 1 and 2 as you currently are, then System.enqueueJob()
  • A Queueable class you'll make to handle Steps 3 - 5
  • Completely removed from the flow of the previous steps, make a Schedulable Batch class to find records that still need to check the status of an SMS, and perform steps 6 and 7. Schedule it to run every few hours, at the end of business each day, or whatever interval is appropriate

You won't call the Schedulable Batch class directly. Having it run on an interval should prevent records from being processed multiple times at once without any additional work (assuming your volume is low enough and your interval long enough so that all of the batches finish before the next run starts).

If your volume is low enough and the verification happens quick enough, you may be able to get away with chaining another queueable for steps 6 and 7 (specifying a delay between 0 - 10 minutes via the second argument to System.enqueueJob()) instead of making a Schedulable Batch class, but be aware that that approach can chew through the 250k async call per day limit (24-hour rolling window for that limit).

Using another Queueable would probably be easier, but a scheduled Batchable would put less pressure on that async call limit. I'm not sure if each invocation of a batchable's execute() takes from that limit, but even if your batch size is limited to 100 (max number of callouts, and I assume each call for a check on the SMS status can only take a single SMS) that's still 100x more volume you'd be able to handle compared to queueables.

1
  • Thanks for the detailed answer. I don't know much about how Heroku does the call to Salesforce as I'm not part of that dev, but it's a portal for people to log in and view their data, and one of the actions is to request a verification. All I know is it calls a Salesforce class and returns a status to the portal user. I have asked the developer to split the job into creating the verification record first and then do the SMS call. I already have a queueable job that checks any SMS where the status has not been updated - it runs hourly. The volumes are very low, like one or 2 a day. Commented Aug 14 at 22:30

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.