Q:

Trigger Recursion after API Callout to Run Territory Assignment Rules

I am trying to trigger territory assignment rules on accounts via the API (the only way you can, apparently), and then change the account owner based on the territories that are assigned. It’s an interesting process - a managed app first makes an API call in a future method, which updates the account, which triggers my class, which makes another queueable API call to run the assignment rules, which ideally I can then use to update the account owner.
The problem is that my API call apparently updates the account when it runs the assignment rules, which recursively hits the account trigger, which makes the API call, which hits the account trigger, etc. I am able to stop recursion in future or queueable contexts, but how can I stop the API from firing the trigger again?
Note 1: there are no errors given when the code runs. It just fires infinitely.
Note 2: I cannot assign any static variable, because the API transaction is occurring in its own context. For the same reason, !System.isQueueable is not preventing the recursion either.
Trigger looks like this:
if (Trigger.isUpdate) {
    AssignAcctOwnerFromTerritory.AssignOwnerFromTerritory(trigger.new);  //must run in after trigger so that it can grab the territory ID assigned by ETM assignment rules        
}

Class that calls the queueable callout:
public class AssignAcctOwnerFromTerritory {

    public static void AssignOwnerFromTerritory(List<Account> AccsToUpdate){

        //run ETM assignment rules
        Map<ID, Account> AcctMap = new Map<ID, Account>(AccsToUpdate);
        Set<String> AccIDs = new Set<String>((List<String>) New List<ID>(AcctMap.keyset()));

        if(!System.isQueueable()){              
            RunTerritoryRules Job = new RunTerritoryRules();
                Job.AcctIDs = AccIDs;                
                    System.debug('Queuing ETM assignments');
                System.enqueueJob(job);
        }       
}   

The callout:
public class RunTerritoryRules implements Queueable, Database.AllowsCallouts  {

    public Set<String> AcctIDs = null;
    public String SessionID = null;

    public void execute(QueueableContext context) {

        System.debug('AcctIDs queued for ETM rules API call  = '+AcctIDs);      

        SessionID = Page.TerritoriesSessionID.getContent().toString();     
        List<String> AccIDStrings = new List<String>();
        if(AcctIDs != null){
            for(String AccID : AcctIDs){
                AccIDStrings.add(AccountTag.replace('{ACCID}', AccID)); 
            }
        }

        requestTemplate = requestTemplate.replace('{ACCLISTS}', String.join(AccIDStrings, ' '));
        requestTemplate = requestTemplate.replace('{SESSID}', SessionID); 

        HttpRequest request = new HttpRequest();
            request.setEndpoint(System.URL.getSalesforceBaseUrl().toExternalForm()+'/services/Soap/u/41.0/'+UserInfo.getOrganizationId());
            request.setMethod('POST');
            request.setHeader('Content-Type', 'text/xml;charset=UTF-8');
            request.setHeader('SOAPAction', '""');
            request.setBody(requestTemplate);

        String s = String.valueOf(new Http().send(request).getBodyDocument());
            System.debug(s);
        }

        String accountTag = '<urn:sObjects> '+
                            '<urn1:type>Account</urn1:type>  '+
                    '<urn1:Id>{ACCID}</urn1:Id>   '+
                        '</urn:sObjects> ' ;    
        String requestTemplate = '<soapenv:Envelope '+
                'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'+
                ' xmlns:urn="urn:partner.soap.sforce.com"'+
                ' xmlns:urn1="urn:sobject.partner.soap.sforce.com">'+
                                '<soapenv:Header> '+
                                  '<urn:AssignmentRuleHeader> '+ 
                                     '<urn:useDefaultRule>true</urn:useDefaultRule> '+
                                     '<urn:assignmentRuleId></urn:assignmentRuleId> '+
                                  '</urn:AssignmentRuleHeader>  '+
                                  '<urn:SessionHeader> '+
                        '<urn:sessionId>{SESSID}</urn:sessionId> '+
                                  '</urn:SessionHeader> '+
                               '</soapenv:Header> '+
                               '<soapenv:Body> '+
                      '<urn:update> '+
                       ' {ACCLISTS}'+ 
                                  '</urn:update> '+
                               '</soapenv:Body> '+
                            '</soapenv:Envelope>';

Relatedly, in the logs, the API call appears as a different transaction from the queueable handler in which it is called, which is different from what I’ve seen making API calls to external web services. Why is that? If the territories will not update within the same transaction as the queueable class in which it is called, I don’t know how I could make the final account owner update. In other integrations I’ve written, the result of the API call (a JSON string for example) is returned within the context of the running class, so it can be used for processing thereafter. 

Many thanks 

A:

There are several ways to unravel trigger recursion.
Seeing as in your case, one of the steps is a managed app that you cannot touch I suggest the following:
Make sure that your trigger fires only when a relevant field is modified.  
You don't need your trigger to fire every time someone changes a phone number (or whatever field is being changed by the managed app) on an account, only when the territory changes.  
Here's a simple sample function of how to get only records that have been modified in a particular field(s).  
private static List<SObject> findRelevantModifiedRecords(List<SObject> newList, Map<Id, SObject> oldMap, List<String> relevantFields){
    Set<SObject> retSet = new Set<SObject>();
    SObject tempObj;
    for (SObject obj : newList){
        tempObj = oldMap != null ? oldMap.get(obj.Id) : null;
        for (String field : relevantFields){
            if ((tempObj == null && obj.get(field) != null)  || (tempObj != null && obj.get(field) != tempObj.get(field))){
                retSet.add(obj);
            }
        }
    }
    return new List<SObject>(retSet);
}

This code checks for a created record that has a value in the specified field(s), or a record that received a new value on specified field(s), even if the old value was null.

