I have a trigger which executes a callout on the Case object to create a resource in my backend. The callout returns the ID of the created resource, which should then be added to the Case the trigger was called from.

The problem is that future methods don't support return types, so how can I update the record with the newly obtained backend ID?

(Note: the field is not called ID, and it is not meant to replace the Salesforce SObject ID, it's just named something like external_resource_id__c in my org)

Also, since future methods don't accept sObjects either, I can't just pass the object to the method and perform the insert from the inside.

So what could I do in this case?

Trigger code

trigger CreateExtStory on Case (before insert, before update) {
    List<Case> caseList = new List<Case>();
    for(Case c : Trigger.new){
        if (c.Create_Ext_Case__c == true && c.Ext_Resource_ID__c == null){
            String extTitle = c.Subject;
            String extDescription = c.Description;
            Integer projectId = ExtProcessUtilities.determineProjectId(c.Ext_Process_Project__c);
            ExtProcessProject project = new ExtProcessProject(projectId);
            ExtProcessStory story = new ExtProcessStory(extTitle , project, extDescription);
    insert caseList;

You can't get a return value, because future methods are called asynchronously. You would want to pass in the original case Id so you can update the record.

@future public static void createExternalCase(Id caseId) {
  Case record = [SELECT ... FROM Case WHERE Id = :caseId];
  String externalId = Utils.createExternalCase();
  record.external_resource_id__c = externalId;
  update record;
  • And the update statement wouldn't start to recussively call the trigger?
    – ThePorcius
    Apr 12 at 19:32
  • @ThePorcius Depends. After insert->future would be okay, but if you need after update->future, you need to check if you're already in a future method and not continue: if(!System.isFuture()) { Utils.createExternalCase(record.Id); }
    – sfdcfox
    Apr 12 at 19:34
  • I'm actually using before triggers...I'll post the code
    – ThePorcius
    Apr 12 at 19:39
  • @ThePorcius Don't bother; you can't callout anyways, so after insert trigger would be the most appropriate trigger for your situation.
    – sfdcfox
    Apr 12 at 19:43

Be aware that there are limits on future methods that you must consider when setting up such processing.

Note that there are limits on the number of future methods that can be invoked, as per the documentation:

Maximum number of methods with the future annotation allowed per Apex invocation: 50 (synchronous), 0 in batch and future contexts; 1 in queueable context

Recall that (standard SObject) triggers are invoked with up to 200 records at a time and if you have more than 200 records being inserted/updated/deleted/undeleted/etc. in a given DML operation then your trigger is invoked multiple times with different subsets (or "chunks") of the SObjects.

So, if each call to your trigger initiates a single future method then you can handle a maximum of 50 chunks of 200 records (which is 10000 records) which happens to be the maximum number of records you can update in a single session anyway. However, if your trigger is initiated from an async context you either cannot use a future at all (and since you cannot perform a callout in a trigger you cannot perform your callout) or you can support just one chunk of 200 records; any more and a governor limit uncatchable exception will be thrown, ruining your day.

Thus the pattern of using a future from a trigger to perform a callout is essentially broken since it will fail in various key scenarios. Sure, you can work around these by making sure you don't perform DML in async contexts, but you can pretty much guarantee that someone will have a need to do this on your org at some point in the future and then the sh*t will hit the fan.

You are therefore better to either consider using a Queueable or to use platform events to take the processing outside the current transaction. Both have their downsides:

  1. Until Transaction Finalizers GA you cannot reliably recover from a critical error (e.g. governor limit issue or uncaught exception) raised during queueable processing since you have no way to access the queueable's state. Additionally, you can only start a single queueable from the context of async processing.
  2. There can be edge cases where Platform Events are not delivered (even to a trigger-based event consumer). (I have never seen this scenario myself, but other community members have.)

The latter is captured in the documentation:

In rare cases, the event message might not be persisted in the distributed system during the initial or subsequent attempts. This means that the events aren’t delivered to subscribers, and they aren’t recoverable.

As noted, the main problem with queueables is that you are stuck if you have a need to initiate one per trigger chunk, you have more than 200 SObjects being operated on and you are already in an async context.

This basically means that the most flexible and robust solution is to use platform events and keep your fingers crossed you don't get hit by the nasty edge case mentioned above. TBH, it strikes me that if this edge case is gonna hit your org it's probably going to mess up more than just the delivery of platform events.

  • This method is not for doing batch updates or inserts, it's just for the standard case creation flow. Also, the user above said that if(!System.isFuture()) should stop the call from happening recursively. Is this still an issue with what you mentioned?
    – ThePorcius
    Apr 13 at 17:38
  • It is possible that this would not be what you want in the future- someone may add a new process flow that includes case creation from a separate future method. Like I said, you can take this approach but there will be unexpected consequences n the future (pardon the pun) as your solution evolves.
    – Phil W
    Apr 13 at 17:50

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.