In Salesforce, flows are bulkified. The documentation explains it best, so I'll copy it here:
When multiple interviews for the same flow run in one transaction, each interview runs until it reaches a bulkifiable element. Salesforce takes all the interviews that stopped at the same element and intelligently executes those operations together. If other interviews are at a different element, Salesforce then intelligently executes those operations together. Salesforce repeats this process until all the interviews finish.
For clarity, a Flow Interview is a running instance of a flow.
In regards to InvocableMethod calls, this is why there is a restriction on the types of data you can use. The flow runtime will wait until all interviews are at the same element, then call the Apex code with the parameters being all the inputs from all the interviews, and the output will be returned, in order, to the interview the input came from.
In order to facilitate this behavior in Apex, an extra List
is wrapped around the input parameter and the return value. That means if you want to return a single value, like Id
, you must put it in a List
, and if you want to return a List<Id>
, you need to return a List<List<Id>>
.
In order to make sure that your code will behave correctly, you must make certain that, if you return a List
, it has exactly the same number of elements as the input parameter, and in the same order as they were received. If you don't, you may experience errors or even erroneous logic, such as the wrong data reaching the wrong interview.
Fixed Examples
In the first example, the order of the output values may not match the expected size, or may return the accounts in the wrong order, meaning that you can either receive an exception, or some interviews might get the wrong value.
@InvocableMethod
public static List<Account> getLeadConvertedAccount(List<Lead> leads) {
List<Id> accountIds = new List<Id>();
for(Lead record: leads) {
accountIds.add(record.ConvertedAccountId);
}
Map<Id, Account> accountsById = new Map<Id, Account>([
SELECT Name FROM Account WHERE Id IN :accountIds
]);
List<Account> accounts = new List<Account>();
for(Lead record: leads) {
accounts.add(accountsById.get(record.ConvertedAccountId);
}
return accounts;
}
In the second example, the action is not properly bulkified, and so will get an exception if it executes in bulk. The code should always be bulkified, even if only used in a screen flow. A future developer might want to use it in a different context, and it won't correctly if it runs in a bulkified context.
@InvocableMethod
public static List<List<String>> splitTextByLine(List<String> source) {
List<List<String>> results = new List<List<String>>();
for(String value: source) {
results.add(value?.split('\n'));
}
}
In the third example, the original code failed to add a value in certain contexts, so it would "randomly" fail. Always ensure that the number of inputs and outputs match, even if it's just to return a null value.
@InvocableMethod
public static List<Account> processAccountData(List<String> data) {
List<Account> results = new List<Account>();
for(String accountJson: data) {
// Never fail to add a value for each input
if(String.isNotBlank(accountJson)) {
results.add((Account)JSON.deserialize(accountJson, Account.class));
} else {
results.add(null);
}
}
return results;
}