Loading
Integrointitoimenpiteen kutsu Apexista

Integrointitoimenpiteen kutsu Apexista

Käytä Summer’25 -julkaisusta alkaen IntegrationProcedureService-Apex-luokan Connect API for Integration Procedure -kutsuja ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, ApexInput) -rajapinnasta. Tämä Connect API poistaa riippuvuuden hallitusta paketista ja tarjoaa 21 % paremman suorituskyvyn Apex integraatiotoimenpiteiden kutsuille verrattuna edelliseen menetelmään.

Tärkeää
Tärkeää Tämän asiakirjan suorituskykytiedot on tarkoitettu vain tiedotustarkoituksiin, ja ne perustuvat sisäiseen vahvistukseen ja testaukseen tietyissä olosuhteissa. Todelliset tulokset saattavat vaihdella komponentin rakenteen, tuotantoympäristön ja muiden tekijöiden mukaan. Nämä luvut tarjoavat yleisiä ohjeita eivätkä ne takaa suorituskykyä.

Edellytykset

Ennen kuin käytät Connect API for Integration Procedure -kutsuja, ota parannettu suorituskyky käyttöön Omnistudio-asetukset-sivulta. Kun parannettu suorituskyky ei ole käytössä, pyyntöjä ei reititetä Connect API:n kautta.

Katso ohjeet kohdasta Komponenttien parannetun suorituskyvyn ottaminen käyttöön.

Integrointitoimenpiteen käynnistäminen ja sen tietojen palauttaminen

Kutsuu integrointiprosessia ja palauttaa sen tiedot. Ohittaa jakosäännöt ja mukautetut käyttöoikeudet, mikä varmistaa, että integrointitoimenpide on yksityinen. Lisätietoja on Salesforce-ohjeen aiheessa Jakosäännöt ja mukautetut käyttöoikeudet.

Connect API for Integration Procedure -kutsun Olemassa oleva metodi (korvaa Connect API:lla)
String ipName = 'FindContacts_LastNames';
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
String serializedInputMap = JSON.serialize(inputMap);
stringList.add(serializedInputMap);
apexInput.input = stringList;

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
for (String key: optionsMap.keySet()){
    if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMap.get(key);
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMap.get(key);
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMap.get(key);
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMap.get(key);
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMap.get(key);
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMap.get(key);
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMap.get(key);
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMap.get(key);
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMap.get(key);
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMap.get(key);
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMap.get(key);
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMap.get(key);
    }
}
options.shouldSendLegacyResponse = true;
apexInput.options = options;

ConnectAPI.IntegrationProcedureServiceRunOutputRepresentation output = ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);

List<String> responseList = output.response;
String currentSerResponse = responseList[0];
Map<String, Object> currentResponseObj = (Map<String, Object>)JSON.deserializeUntyped(currentSerResponse);

Object ipOutput = currentResponseObj.get('result');
String ipName = 'FindContacts_LastNames';
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName',
'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

Object ipOutput = devopsimpkg11.Integration ProcedureService.runIntegrationService(ipName, inputMap, optionsMap);

Integrointitoimenpiteen käynnistäminen ja sen datan palauttaminen jakosääntöjen ja mukautettujen käyttöoikeuksien ohittamatta

Kutsuu integrointiprosessia ja palauttaa sen tiedot. Ohittaa jakosäännöt ja mukautetut käyttöoikeudet, mikä varmistaa, että integrointitoimenpide on yksityinen. Lisätietoja on Salesforce-ohjeen aiheessa Jakosäännöt ja mukautetut käyttöoikeudet.

Connect API for IP -kutsut Olemassa oleva metodi (korvaa Connect API:lla)

Käyttäjillä täytyy olla tarvittavat käyttöoikeudet integraatiotoimenpiteen suorittamiseen.


String ipName = 'FindContacts_LastNames';
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
String serializedInputMap = JSON.serialize(inputMap);
stringList.add(serializedInputMap);
apexInput.input = stringList;

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
for (String key: optionsMap.keySet()){
    if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMap.get(key);      
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMap.get(key);  
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMap.get(key);  
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMap.get(key);  
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMap.get(key);  
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMap.get(key);  
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMap.get(key);  
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMap.get(key);  
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMap.get(key);  
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMap.get(key);  
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMap.get(key);  
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMap.get(key);  
    } 
}
options.shouldSendLegacyResponse = true;
apexInput.options = options;

ConnectAPI.IntegrationProcedureServiceRunOutputRepresentation output = ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);

List<String> responseList = output.response;
String currentSerResponse = responseList[0];
Map<String, Object> currentResponseObj = (Map<String, Object>)JSON.deserializeUntyped(currentSerResponse);

Object ipOutput = currentResponseObj.get('result');

String ipName = 'FindContacts_LastNames';
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);
Object ipOutput = devopsimpkg11.IntegrationProcedureService.runIntegrationProcedureServiceFromApex(ipName, inputMap, optionsMap);

Integrointitoimenpiteen kutsuminen @future-annotaatiolla

Connect API for Integration Procedure -kutsun Olemassa oleva metodi (korvaa Connect API:lla)

String ipName = 'FindContacts_LastNames';
String inputMap = '{\"ContactLastName\": \"Smith\"}';
String optionsMap = '{\"isDebug\": false}';

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
stringList.add(inputMap);
apexInput.input = stringList;

Map<String, Object> optionsMapDeserialized = (Map<String, Object>)JSON.deserializeUntyped(optionsMap);

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
 
 for (String key: optionsMapDeserialized.keySet()){
    if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMapDeserialized.get(key);  
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMapDeserialized.get(key);  
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMapDeserialized.get(key);  
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMapDeserialized.get(key);  
    } 
}
  
options.shouldSendLegacyResponse = true;
options.useFuture = true;
apexInput.options = options;

ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);
String ipName = 'FindContacts_LastNames';
String inputMap = '{\"ContactLastName\": \"Smith\"}';
String optionsMap = '{\"isDebug\": false}';

devopsimpkg11.IntegrationProcedureService.runIntegrationServiceFuture(ipName, inputMap, optionsMap);

Integrointitoimenpiteen kutsuminen jonoitettavana Apex asynkronista käsittelyä varten

Palauttaa Queueable-työn tunnuksen. Lisätietoja on Salesforce-ohjeen aiheessa Queueable Apex.

Connect API for Integration Procedure -kutsun Olemassa oleva metodi (korvaa Connect API:lla)

String ipName = 'FindContacts_LastNames';

Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
String serializedInputMap = JSON.serialize(inputMap);
stringList.add(serializedInputMap);
apexInput.input = stringList;

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
for (String key: optionsMap.keySet()){
    if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMap.get(key);  
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMap.get(key);  
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMap.get(key);  
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMap.get(key);  
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMap.get(key);  
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMap.get(key);  
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMap.get(key);      
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMap.get(key);  
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMap.get(key);  
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMap.get(key);  
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMap.get(key);  
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMap.get(key);  
    } 
}    
options.shouldSendLegacyResponse = true;
options.useQueueable = true;
apexInput.options = options;

ConnectAPI.IntegrationProcedureServiceRunOutputRepresentation output = ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);

List<String> responseList = output.response;
String currentSerResponse = responseList[0];
Map<String, Object> currentResponseObj = (Map<String, Object>)JSON.deserializeUntyped(currentSerResponse);

Object currentResult = currentResponseObj.get('result');
Map<String, Object> mapCurrentResult = (Map<String, Object>)currentResult;

Object ipResultObj = mapCurrentResult.get('IPResult');
Map<String, Object> IpResultMap = (Map<String, Object>)ipResultObj;
Id ipOutput = (Id)IpResultMap.get('queueableJobId');

String ipName = 'FindContacts_LastNames';
Map inputMap = new Map();
inputMap.put('ContactLastName', 'Smith');
Map optionsMap = new Map();
optionsMap.put('isDebug', false);


Id ipOutput = devopsimpkg11.IntegrationProcedureService.runIntegrationProcedureQueueable(ipName, inputMap, optionsMap);

Kutsuu integraatiotoimenpiteen, joka välittää syöttöparametrien kartan ja vastaanottaa tuloksen toisessa kartassa

Connect API for Integration Procedure -kutsun Olemassa oleva metodi (korvaa Connect API:lla)

String ipName = 'FindContacts_LastNames';
    
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');
   Map<String, Object> outputMap = new Map<String, Object>();


Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
String serializedInputMap = JSON.serialize(inputMap);
stringList.add(serializedInputMap);
apexInput.input = stringList;

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
for (String key: optionsMap.keySet()){
    if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMap.get(key);  
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMap.get(key);      
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMap.get(key);  
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMap.get(key);  
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMap.get(key);  
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMap.get(key);  
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMap.get(key);  
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMap.get(key);  
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMap.get(key);  
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMap.get(key);  
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMap.get(key);  
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMap.get(key);  
    } 

}

options.shouldSendLegacyResponse = true;
apexInput.options = options;

ConnectAPI.IntegrationProcedureServiceRunOutputRepresentation output = ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);

List<String> responseList = output.response;
String currentSerResponse = responseList[0];
Map<String, Object> currentResponseObj = (Map<String, Object>)JSON.deserializeUntyped(currentSerResponse);
Object ipOutput = currentResponseObj.get('result');

if (options.useQueueable == true) {
    Map<String, Object> mapCurrentResult = (Map<String, Object>)ipOutput;
    Object ipResultObj = mapCurrentResult.get('IPResult');
    ipOutput = ipResultObj;
}
else if (options.useFuture == true) {
    ipOutput = new Map<String, Object>();
}    

outputMap.put('IPResult', ipOutput);

String ipName = 'FindContacts_LastNames';

Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> outputMap = new Map<String, Object>();

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

devopsimpkg11.IntegrationProcedureService ipService = new devopsimpkg11.IntegrationProcedureService();

ipService.invokeMethod(ipName, inputMap, outputMap, optionsMap); // Returns Boolean

Object ipOutput = outputMap.get('IPResult');

Integrointitoimenpiteiden puheluiden esimerkkejä

Voit kutsua integrointiprosessia Apex-koodista kahdella tavalla käyttämällä IntegrationProcedureService -luokkaa.

Esimerkki 1


class IntegrationProcedureService {
/*
procedureAPIName - the Type_SubType of Procedure
input - The payload / initial data in the Map<String, Object> format for the Procedure
*/

String ipName = 'FindContacts_LastNames';
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
String serializedInputMap = JSON.serialize(inputMap);
stringList.add(serializedInputMap);
apexInput.input = stringList;

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
for (String key: optionsMap.keySet()){
if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMap.get(key);
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMap.get(key);
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMap.get(key);
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMap.get(key);
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMap.get(key);
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMap.get(key);
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMap.get(key);
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMap.get(key);
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMap.get(key);
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMap.get(key);
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMap.get(key);
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMap.get(key);
    }
}
options.shouldSendLegacyResponse = true;
apexInput.options = options;

ConnectAPI.IntegrationProcedureServiceRunOutputRepresentation output = ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);

List<String> responseList = output.response;
String currentSerResponse = responseList[0];
Map<String, Object> currentResponseObj = (Map<String, Object>)JSON.deserializeUntyped(currentSerResponse);

Object ipOutput = currentResponseObj.get('result');

Esimerkki 2

/* Initialize variables */
String procedureName = 'Type_SubType';
Map<String, Object> ipInput = new Map<String, Object> ();
Map<String, Object> ipOutput = new Map<String, Object> ();
Map<String, Object> ipOptions = new Map<String, Object> ();

/* Populating input map for an Integration Procedure. 
Follow whatever structure your VIP expects */
String orderId = '80100000000abcd';
ipInput.put('orderId', orderId);

/* Call the Integration Procedure via runIntegrationService, 
and save the output to ipOutput */
ipOutput = (Map<String, Object>) 

String ipName = 'FindContacts_LastNames';
Map<String, Object> inputMap = new Map<String, Object>();
inputMap.put('ContactLastName', 'Smith');

Map<String, Object> optionsMap = new Map<String, Object>();
optionsMap.put('isDebug', false);

ConnectAPI.IntegrationProcedureServiceRunInputRepresentation apexInput = new ConnectAPI.IntegrationProcedureServiceRunInputRepresentation();
List<String> stringList = new List<String>();
String serializedInputMap = JSON.serialize(inputMap);
stringList.add(serializedInputMap);
apexInput.input = stringList;

ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation options = new ConnectAPI.IntegrationProcedureServiceRunOptionsRepresentation();
for (String key: optionsMap.keySet()){
    if (key == 'isDebug') {
        options.isDebug = (Boolean)optionsMap.get(key);
    } else if (key == 'chainable') {
        options.chainable = (Boolean)optionsMap.get(key);
    } else if (key == 'resetCache') {
        options.resetCache = (Boolean)optionsMap.get(key);
    } else if (key == 'ignoreCache') {
        options.ignoreCache = (Boolean)optionsMap.get(key);
    } else if (key == 'queueableChainable') {
        options.queueableChainable = (Boolean)optionsMap.get(key);
    } else if (key == 'useQueueableApexRemoting') {
        options.useQueueableApexRemoting = (Boolean)optionsMap.get(key);
    } else if (key == 'vlcApexResponse') {
        options.vlcApexResponse = (Boolean)optionsMap.get(key);
    } else if (key == 'useFuture') {
        options.useFuture = (Boolean)optionsMap.get(key);
    } else if (key == 'useQueueable') {
        options.useQueueable = (Boolean)optionsMap.get(key);
    } else if (key == 'vlcIPData') {
        options.vlcIPData = (String)optionsMap.get(key);
    } else if (key == 'vlcStatus') {
        options.vlcStatus = (String)optionsMap.get(key);
    } else if (key == 'vlcMessage') {
        options.vlcMessage = (String)optionsMap.get(key);
    }
}
options.shouldSendLegacyResponse = true;
apexInput.options = options;

ConnectAPI.IntegrationProcedureServiceRunOutputRepresentation output = ConnectAPI.OmniDesignerConnect.integrationProcedureExecute(ipName, apexInput);

List<String> responseList = output.response;
String currentSerResponse = responseList[0];
Map<String, Object> currentResponseObj = (Map<String, Object>)JSON.deserializeUntyped(currentSerResponse);

Object ipOutput = currentResponseObj.get('result');

System.debug('IP Output: ' + ipOutput);
 
Ladataan
Salesforce Help | Article