Bridging the Gap: Mastering Invocable Apex in Salesforce Flows

Salesforce encourages administrators and developers to leverage no-code and low-code tools to build complex applications and automations quickly. Among these, Salesforce Flow has become the definitive tool for creating diverse automations and business processes. While Flow is now a robust tool — offering solutions ranging from user-guided Screen Flows to Record-Triggered automations and Scheduled Jobs — certain complex processes cannot be implemented within the Flow builder alone. For instance, processing large volumes of records, executing complex mathematical calculations, or integrating with external APIs often necessitates the use of Apex.

It may seem that teams must choose between the simplicity of Flow and the power of Apex. However, Salesforce provides a mechanism to merge the benefits of both: Invocable Apex. This allows you to encapsulate complex logic in code and expose it as a custom action within the Flow Builder — combining the power of Apex with the declarative ease of Flow.

What is Invocable Apex?

Invocable Apex is a specific method within an Apex class that is annotated to be reachable by Flow, Process Builder, or the REST API. To make a method invocable, it must meet several criteria:

  • It must be annotated with @InvocableMethod.
  • The method must be public and static.
  • It must accept exactly one input parameter, which must be a list of a supported type (primitives, collections, or SObjects).
  • It may return an output to the calling Flow, which also must be a list of a supported type.

Simple Implementation: The Single Parameter

The following example demonstrates a basic implementation. When defining an invocable method, we use the label and category modifiers to control how the action appears in the Flow Builder’s action list.

The getRiskCategory method accepts a List<Integer> (years in business) and returns a List<String>. We use lists for both inputs and outputs to support bulkification. If 100 records trigger a flow simultaneously, Salesforce batches those inputs into a single list of 100 integers rather than making 100 individual calls to the method, significantly improving performance and respecting governor limits.

public class LoanRiskAnalyzer {

@InvocableMethod(label='Get Risk Category (Basic)' category='Risk')

public static List<String> getRiskCategory(List<Integer> yearsInBusiness) {

List<String> results = new List<String>();

for (Integer years : yearsInBusiness) {

if (years >= 9) results.add('Low Risk');

else if (years >= 7) results.add('Moderate Risk');

else if (years >= 4) results.add('Watch/Pass');

else results.add('High Risk');

}

return results;

}

}

Handling Multiple Inputs with Wrapper Classes

Real-world business processes rarely rely on a single data point. Since an @InvocableMethod only allows one input parameter, how do we pass multiple variables? The solution is to create a "Wrapper Class" using the @InvocableVariable annotation. This allows the Flow to pass a complex object containing multiple fields to the Apex method.

public class LoanRiskAnalyzer {

@InvocableMethod(label='Calculate Composite Risk' category='Risk')

public static List<String> calculateCompositeRisk(List<RiskRequest> requests) {

List<String> results = new List<String>();

for (RiskRequest req : requests) {

// Formula: Loan / (Revenue * Terms * ln(Years + 1))

Decimal riskFactor = req.annualRevenue * req.loanTerms *

Math.log(req.yearsInBusiness + 1);

Decimal score = req.loanAmount / (riskFactor > 0 ? riskFactor : 1);

if (score < 0.2) results.add('Low Risk');

else if (score < 0.5) results.add('Moderate Risk');

else if (score < 0.7) results.add('Watch/Pass');

else results.add('High Risk');

}

return results;

}

public class RiskRequest {

@InvocableVariable(label='Years in Business' required=true)

public Integer yearsInBusiness;

@InvocableVariable(label='Annual Revenue' required=true)

public Decimal annualRevenue;

@InvocableVariable(label='Loan Amount Requested' required=true)

public Decimal loanAmount;

@InvocableVariable(label='Loan Terms' required=true)

public Integer loanTerms;

}

}

Returning Multiple Values to Flow

Just as we can pass multiple inputs, we can return multiple outputs by defining a result wrapper class. This is useful when a single Apex calculation produces several values — such as a numeric score, a text category, and a specific recommendation — that the Flow needs to use in subsequent steps.

public class LoanRiskAnalyzer {

@InvocableMethod(label='Calculate Detailed Risk' category='Risk')

public static List<RiskResult> calculateDetailedRisk(List<RiskRequest> requests) {

List<RiskResult> results = new List<RiskResult>();

for (RiskRequest req : requests) {

RiskResult res = new RiskResult();

Decimal riskFactor = req.annualRevenue * req.loanTerms *

Math.log(req.yearsInBusiness + 1);

res.numericScore = req.loanAmount / (riskFactor > 0 ? riskFactor : 1);

if (res.numericScore < 0.2) {

res.category = 'Low Risk';

res.managementNote = 'Strong revenue-to-loan ratio.';

} else {

if (res.numericScore < 0.5) res.category = 'Moderate Risk';

else if (res.numericScore < 0.7) res.category = 'Watch/Pass';

else res.category = 'High Risk';

res.managementNote = (req.yearsInBusiness < 5)

? 'Review: New business entity.'

: 'Review: High debt-to-income ratio.';

}

results.add(res);

}

return results;

}

public class RiskRequest {

@InvocableVariable(label='Years in Business' required=true)

public Integer yearsInBusiness;

@InvocableVariable(label='Annual Revenue' required=true)

public Decimal annualRevenue;

@InvocableVariable(label='Loan Amount Requested' required=true)

public Decimal loanAmount;

@InvocableVariable(label='Loan Terms' required=true)

public Integer loanTerms;

}

public class RiskResult {

@InvocableVariable(label='Numeric Risk Score')

public Decimal numericScore;

@InvocableVariable(label='Risk Category')

public String category;

@InvocableVariable(label='Management Highlight')

public String managementNote;

}

}

Real-World Use Case: API Integrations and Asynchronous Processing

To see the versatility of Invocable Apex in action, consider a common financial requirement: Automatic Currency Conversion for Invoices. In this scenario, we use a Record-Triggered Flow on the Invoice object. To maintain data integrity and respect Salesforce’s transaction limits, the flow is split into two distinct paths:

  • Synchronous Path: Immediately upon record creation, the flow locks the Invoice to prevent further edits and publishes a Platform Event. This ensures the system knows an invoice is pending conversion.
  • Asynchronous Path: Using the "Run Asynchronously" path, the flow calls an Invocable Apex method that performs a callout to an external Exchange Rate API. Once the rate is retrieved, the Apex logic calculates the foreign currency value, updates the Invoice, and sets the status to "Ready" — effectively unlocking the record.

This architecture solves a classic Salesforce challenge: you cannot perform a web service callout if there is uncommitted work (like a record insert) in the same transaction. By moving the Invocable Apex to the asynchronous path, we provide a seamless, automated experience without hitting governor limits.

View the Full Implementation: You can find the complete Apex class and Flow configuration for this example on my GitHub Gist.

Architect's Note: Why Platform Events?

While a standard record-triggered flow could handle the internal currency conversion, I chose a Platform Event-driven architecture to future-proof the system. By publishing an event when an invoice is created, we create a "Pub/Sub" model. This allows not only our internal Apex to react, but also enables external systems (like an ERP or financial reporting tool) to subscribe to these events and trigger their own processes simultaneously without additional configuration inside Salesforce.

Summary: When to Use Invocable Apex

By utilizing wrapper classes, Invocable Apex becomes a versatile bridge between declarative and programmatic development. You should consider implementing Invocable Apex when:

  • Complexity: The logic is too intricate for standard Flow elements (loops, assignments).
  • Bulkification: You need to process large collections while staying within governor limits.
  • Reusability: You want to write logic once in Apex and use it across multiple Flows.
  • Integrations: You need to call external systems via Apex callouts.
  • Performance: High-performance processing is required for critical business paths.