Salesforce gives you two tools for automation: point-and-click Flow Builder, and code-first Apex. Most real projects need both. The problem is they don't naturally talk to each other — until you use Invocable Methods.
An Invocable Method is a static Apex method annotated with @InvocableMethod. Once annotated, it shows up as a callable action inside Flow Builder. Admins can invoke your Apex logic from any Flow without writing a single line of code themselves.
Flow Builder handles most automation. It creates records, sends emails, updates fields, routes approvals. But it cannot make HTTP callouts. It cannot run complex calculations. It cannot process data in ways that require real programmatic logic.
Invocable Methods close that gap — cleanly, officially, and without hacks. They're the right architectural answer when Flow needs something only code can do.
public class SendWelcomeEmail {
@InvocableMethod(label='Send Welcome Email'
description='Sends a welcome email to a new contact')
public static void sendEmail(List<Id> contactIds) {
for (Id contactId : contactIds) {
// email logic here
}
}
}
The label parameter is what appears in Flow Builder's action search. The description helps admins understand what it does without reading your code. Save the class — your method is immediately available in every Flow in the org.
When you need to pass more than one piece of data, use wrapper classes with @InvocableVariable:
public class AccountUpgradeAction {
public class Request {
@InvocableVariable(label='Account Id' required=true)
public Id accountId;
@InvocableVariable(label='New Tier')
public String newTier;
}
public class Result {
@InvocableVariable(label='Success')
public Boolean success;
@InvocableVariable(label='Message')
public String message;
}
@InvocableMethod(label='Upgrade Account Tier')
public static List<Result> upgradeAccount(List<Request> requests) {
List<Result> results = new List<Result>();
for (Request req : requests) {
Result res = new Result();
try {
Account acc = [SELECT Id, Tier__c FROM Account WHERE Id = :req.accountId];
acc.Tier__c = req.newTier;
update acc;
res.success = true;
res.message = 'Account upgraded to ' + req.newTier;
} catch (Exception e) {
res.success = false;
res.message = e.getMessage();
}
results.add(res);
}
return results;
}
}
Each field in your wrapper class needs @InvocableVariable — that's what makes it visible to Flow Builder. The required=true flag tells Flow Builder that the field must be mapped before the action can be saved.
Invocable Methods always receive a List<> and always return a List<> (or void). This is intentional. Flows can process hundreds of records at once — Salesforce batches them and calls your method once with all items. Write your logic to handle the entire list, not just one record.
After saving your class, open any Flow. Add an element → Action → Apex Action. Search by the label you gave your method. It appears in the results — click it, and Flow Builder shows a configuration screen where you map Flow variables to your @InvocableVariable fields.
Primitives all work: String, Integer, Long, Double, Boolean, Date, DateTime, Id. sObject types work — you can pass an Account or Contact object directly. Collections of those work too: List<String>, List<Id>. What doesn't work: Map types, generic Object, and non-sObject classes.
Don't let exceptions bubble up uncaught. If your method throws an uncaught exception, the entire Flow fails with a cryptic system error. Instead, wrap each record's logic in a try/catch, set success = false in your Result, and return normally. The Flow can then check the success field and route to an error path.
The same @InvocableMethod works across all Flow types:
Messaging.SingleEmailMessage with custom templates@future method or Queueable from a FlowBulkify. Never SOQL inside a loop. Collect all IDs from the input list first, run one query, build a Map, then loop:
// ❌ WRONG — 1 query per record
for (Request req : requests) {
Account a = [SELECT Id FROM Account WHERE Id = :req.accountId];
}
// ✅ CORRECT — 1 query for all records
Set<Id> ids = new Set<Id>();
for (Request req : requests) {
ids.add(req.accountId);
}
Map<Id, Account> accMap = new Map<Id, Account>(
[SELECT Id, Tier__c FROM Account WHERE Id IN :ids]
);
for (Request req : requests) {
Account acc = accMap.get(req.accountId);
// process acc
}
One @InvocableMethod per class. Salesforce enforces this. Create a separate class for each distinct action.
Label and description. Treat your method as a public API. Admins find it by label. If you rename it after deployment, existing Flows break silently.
@isTest
private class AccountUpgradeActionTest {
@isTest
static void testUpgrade() {
Account acc = new Account(Name='Test Co', Tier__c='Silver');
insert acc;
AccountUpgradeAction.Request req = new AccountUpgradeAction.Request();
req.accountId = acc.Id;
req.newTier = 'Gold';
Test.startTest();
List<AccountUpgradeAction.Result> results =
AccountUpgradeAction.upgradeAccount(
new List<AccountUpgradeAction.Request>{ req }
);
Test.stopTest();
System.assertEquals(true, results[0].success);
Account updated = [SELECT Tier__c FROM Account WHERE Id = :acc.Id];
System.assertEquals('Gold', updated.Tier__c);
}
}
Write two test methods: one for the happy path and one that simulates an exception, verifying that success = false and message is populated.
Invocable Methods are the right answer exactly once: when Flow needs something only Apex can do. They keep your architecture clean — declarative automation handles the orchestration, Apex handles the complexity. That division of responsibility is exactly what Salesforce intends.
Learn to identify the boundary. When Flow is enough, don't write code. When Flow isn't enough, write a clean invocable method with a wrapper class, bulkified logic, exception handling, and a test class. That's the complete pattern.