XSCREENSAVER / 3D PIPES

[readonly] markdown buffer

Bulk-Safe Invocable Apex

Jun 8, 2026 · 6 min read

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.

traceview://invocable-bulk
Flow interviewsList<Request>
  1. 00Account Avalid
  2. 01Account Bmissing access
  3. 02Account Aduplicate input
  4. 03Account Cvalid
collection work 1 SOQL 1 calculation pass final pass over original order
Flow resultsList<Result>
  1. 00Account Asuccess
  2. 01Account Bpermission denied
  3. 02Account Asuccess
  4. 03Account Csuccess
Four Flow inputs are handled as one collection and returned in their original order. Duplicate work may be deduplicated internally, but no input loses its corresponding result.

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.

traceview://invocable-duplicates
idempotent calculationdeduplicate work
AAcalculate A onceresult Aresult A
Two inputs still receive two outputs.
business commandpreserve intent
ship Aship Atwo commandsshipment 1shipment 2
A Set must not silently decide these were duplicates.
Repeating an idempotent calculation may share internal work while still producing two results. Repeating a shipment command may represent two intentional business operations and must not be collapsed accidentally.

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.