Best Practices for Creating Invocable Methods: Using Inner Classes and Invocable Variables
I’ve long been a fan of using invocable methods with flows. Today I’ll use a real-life scenario to explain the importance of using invocable variables as well. We’ll start by looking at the anti-pattern of a standalone invocable method, then we’ll follow best practices and refactor the solution into an invocable method plus invocable variables.
The use case comes from a pro bono project for Playworks. Playworks is the leading national nonprofit leveraging the power of play to transform children’s social and emotional health. They’re a great organization, and they could definitely use some support.
Playworks tracks each school partnership with an opportunity record. A school might purchase 5 trainings, represented by opportunity product records. For financial purposes, the service completion date is tracked for each training, so each training needs its own opportunity product record. Manual entry can get cumbersome, with some opportunity records having 10 or more opportunity products associated with them.
To address this need, we created the Add Products Wizard. Built using flow, the wizard starts by presenting a user with a list of products to select from.

Note: The handy table above is made possible by a free LWC called the Welkin Data Table for Flows.
The user selects the products they want, then enters the quantity and sales price of each product they selected.

So far, so good, but things get tricky if you build an invocable method without invocable variables. Let’s take a look at what not to do, then we’ll refactor our solution to follow best practices.
First up, the anti-pattern. Here’s what the flow loop element looks like behind the scenes, designed for a standalone invocable method. After the user selects their products, the loop begins. The loop runs once for each product selected. The pattern is as follows:
- Get the quantity and sales price for each product from the user.
- Gather up all needed inputs for the invocable method into a flow text collection variable.
- Pass the flow text collection variable into the invocable method.
- The invocable method then creates the desired number of opportunity product records.
- Clear the values of the text collection variable so the loop can start over.
Then comes the messy part. The text collection variable holds 4 values.

The four values are added to the text collection variable in the following order:
- First: Product Id
- Second: Opportunity Id
- Third: Sales Price
- Fourth: Quantity
Imagine having to come in and explain what the opportunityProductInputs variable is holding. There is no way to know, without explicitly inspecting the assignment element that populates the opportunityProductInputs variable. Frustrating, to say the least. The code gets even more wonky. Take a look at the invocable method.
@InvocableMethod(label='Create New Opportunity Products')
public static void createNewOpportunityProducts(List<List<String>> inputs){
List<OpportunityLineItem> oppProds = new List<OpportunityLineItem>();
PriceBook2 stdPriceBook = [SELECT Id
FROM Pricebook2
WHERE isStandard = True
LIMIT 1];
PriceBookEntry pbe = [SELECT Id
FROM PriceBookEntry
WHERE Product2Id =: inputs.get(0).get(0)
AND Pricebook2Id =: stdPriceBook.Id
LIMIT 1];
for(Integer i = 0; i < Integer.valueOf(inputs.get(0).get(3)); i++){
OpportunityLineItem oppProd = new OpportunityLineItem();
oppProd.Product2Id = inputs.get(0).get(0);
oppProd.PricebookEntryId = pbe.Id;
oppProd.OpportunityId = inputs.get(0).get(1);
oppProd.UnitPrice = Integer.valueOf(inputs.get(0).get(2));
oppProd.Quantity = 1;
oppProds.add(oppProd);
}
insert oppProds;
}
Notice that the data type of the input parameter is a nested list of strings. A standalone invocable method requires that the input parameter be a list. A single string becomes a list of strings, and a list of strings becomes a nested list of strings. Not to mention, you have to use nondescript indexes to reference the inputs within the method. The whole thing gets unwieldy fast. This has major downstream implications for code readability.
Now for a best practice. Let’s refactor this code by pivoting away from a standalone invocable method, and adding some invocable variables to hold the inputs. The invocable variables are defined in an inner class called Requests.
public class Requests{ @invocableVariable(label='Product Id' required=true) public String productId; @invocableVariable(label='Opportunity Id' required=true) public String opportunityId; @invocableVariable(label='Sales Price' required=true) public Double salesPrice; @invocableVariable(label='Quantity' required=true) public Integer quantity; }
Next, let’s change the input parameter of the invocable method to accept the data type of the inner class.
@InvocableMethod(label='Create New Opportunity Products')
public static void createNewOpportunityProducts(List<Requests> requests)
Here’s what the refactored method looks like, using the new input parameter.
@InvocableMethod(label='Create New Opportunity Products') public static void createNewOpportunityProducts(List<Requests> requests){ Requests request = requests[0]; List<OpportunityLineItem> oppProds = new List<OpportunityLineItem>(); Opportunity oppty = [SELECT Playworks_Fiscal_Date__c FROM Opportunity WHERE Id =: request.opportunityId]; PriceBook2 stdPriceBook = [SELECT Id FROM Pricebook2 WHERE isStandard = True LIMIT 1]; PriceBookEntry pbe = [SELECT Id FROM PriceBookEntry WHERE Product2Id =: request.productId AND Pricebook2Id =: stdPriceBook.Id LIMIT 1]; for(Integer i = 0; i < request.quantity ; i++){ OpportunityLineItem oppProd = new OpportunityLineItem(); oppProd.Product2Id = request.productId; oppProd.PricebookEntryId = pbe.Id; oppProd.OpportunityId = request.opportunityId; oppProd.UnitPrice = request.salesPrice; oppProd.Quantity = 1; oppProds.add(oppProd); } insert oppProds; }
Notice how much more readable the Apex is. Gone are the nondescript indexes, replaced by logical variable names. The Apex action element is also much more readable inside the flow. It is no longer necessary to gather up the inputs with an assignment element. The invocable variable values are set and the invocable method is called in the same place, creating a much more intuitive flow.

When comparing a standalone invocable method to an invocable method plus invocable variables, the difference is clear. The code becomes much more readable, and the flow is much easier to understand in the latter.
There is one more advantage of using an invocable method plus invocable variables, not shown in this example. While a standalone invocable method can only accept a collection of primitive data types, an invocable variable makes it possible to indirectly pass sObjects into the invocable method. An inner class with variables to hold accounts and opportunities would look something like this.
public class Requests{ @invocableVariable(label='Accounts' required=true) public List<Account> accountsToProcess; @invocableVariable(label='Opportunities' required=true) public List<Opportunity> opportunitiesToProcess; }
Hopefully this post has convinced you not to build a standalone invocable method again, opting instead for an invocable method plus invocable variables. Thanks for reading, and feel free to send me any questions or feedback.