[readonly] markdown buffer
Bulk-Safe Invocable Apex
The easiest invocable Apex action to write is often the most dangerous one to keep.
@InvocableMethod
public static void recalculate(List<Request> requests) {
Id accountId = requests[0].accountId;
// Do the work for one record.
}
It works in a demo because Flow sends one item. Then the action is reused and quietly retains an assumption that was never part of its contract.
An invocable method receives a list because bulk is part of the interface. Its design must explain every element in that list.
- 00Account Avalid
- 01Account Bmissing access
- 02Account Aduplicate input
- 03Account Cvalid
- 00Account Asuccess
- 01Account Bpermission denied
- 02Account Asuccess
- 03Account Csuccess
Define the contract first
Use request and result types that describe the operation.
public class RecalculateAccountHealthAction {
public class Request {
@InvocableVariable(required=true)
public Id accountId;
}
public class Result {
@InvocableVariable public Id accountId;
@InvocableVariable public Boolean success;
@InvocableVariable public String errorCode;
@InvocableVariable public String message;
}
@InvocableMethod(
label='Recalculate Account Health'
description='Recalculates health for each supplied account.'
)
public static List<Result> run(List<Request> requests) {
return execute(requests);
}
}
Labels, descriptions, and field names are part of the admin-facing API. They should explain themselves in Flow Builder.
Decide whether one invalid request should fail the whole collection. Independent requests often deserve one result each; one atomic command does not.
Work as a collection
Validate requests while collecting identifiers, then query once.
Map<Id, Account> accountById = new Map<Id, Account>([
SELECT Id, Health_Score__c
FROM Account
WHERE Id IN :accountIds
WITH USER_MODE
]);
The same rule applies to children: query once, then group them in memory.
Preserve input-to-output correlation. A common contract is positional, where output at index n belongs to input at index n. Build lookup maps for the work, then make a final pass over the original request list to construct ordered results.
Including the input identifier in each result makes failures easier to inspect.
Duplicates need an explicit meaning. An idempotent calculation can deduplicate expensive work while still returning one outcome per request. A command such as creating a shipment must not silently collapse two intentional requests.
Do not let a Set<Id> accidentally define business semantics.
Keep Flow at the edge
The invocable class should adapt Flow's contract to reusable domain logic.
@InvocableMethod(label='Recalculate Account Health')
public static List<Result> run(List<Request> requests) {
List<AccountHealthService.Outcome> outcomes =
AccountHealthService.recalculate(toServiceRequests(requests));
return toFlowResults(outcomes);
}
The service can then be reused by Apex, an LWC, scheduled work, or a repair script.
Test one request, a realistic full collection, missing and inaccessible records, duplicates, mixed outcomes, database failure, and output order. A one-item test proves very little about a list-based API.
Flow-friendly Apex is not code that merely appears in the action picker. It has understandable configuration, predictable bulk behaviour, and results that let Flow decide what happens next without parsing exception prose.