Design Managed Apex for Agentforce

As an independent software vendor (ISV) developer, you can build custom agent actions using Apex and distribute them in managed packages. To ensure that subscriber admins can declaratively configure your Apex agent actions and that Agentforce can invoke the actions at run time, follow these requirements and recommendations.

Use Global Apex

For Agentforce to use Apex agent actions in managed packages, these Apex members must have the global access modifier.

  • The Apex class containing the @InvocableMethod that defines the agent action.
  • The input wrapper class that defines the parameters an admin can configure for the action, and the output wrapper class that defines the structured result returned to Agentforce.
  • All @InvocableVariable members within these input and output wrapper classes.
  • Any custom Apex data types used as properties in your wrapper classes.

If any of these Apex members aren’t global, then the Apex agent action can’t be invoked by Agentforce at run time.

Important

These Apex members must be global because Agentforce agents currently can’t be packaged directly, and therefore can’t have a namespace. By definition, this means that Apex agent actions don’t have access to non-global Apex, such as public Apex, that’s part of a managed package and does have a namespace.

Importantly, managed global Apex is subject to stricter manageability rules than managed non-global Apex. See the Global Apex Manageability Rules section of Best Practices for Using Global Apex in Managed Packages.

Although global Apex is required for any direct entry point to an agent action, delegate any business logic or heavy lifting to public classes and methods. See the Delegate from Thin Global Entry Points section of Best Practices for Using Global Apex in Managed Packages.

Use @InvocableMethod to Define the Action

To define an Apex agent action, use the @InvocableMethod annotation and follow these requirements.

  • Your Apex method must be global static.
  • Your Apex method must be annotated with @InvocableMethod (label='Your Action Name' description='Clear, concise description of what the action does' category='Your ISV App Name').
    • Use clear and descriptive label and description modifiers. The Agentforce reasoning engine uses them to determine when to invoke the action. Subscriber admins configuring Agentforce also use them to help decide which agent topics to add the action to.
    • Use the category modifier to help organize actions. We recommend using your ISV app name.

Only one method in a class can have the @InvocableMethod annotation. Create a separate global Apex class for each agent action in your managed package.

Note

Structure Inputs and Outputs with Global Wrapper Classes

In addition to the requirements of using global Apex and the @InvocableMethod annotation, we also recommend using custom global classes to structure input and output parameters. By using parameter objects, you avoid changing the global method signature when you modify the parameters of the agent action. To learn how to use this pattern, see the Use Parameter Objects for Global Method Inputs and Return Types section of Best Practices for Using Global Apex in Managed Packages. Then review these targeted guidelines to implement Apex agent actions using this pattern.

Because you can’t change managed global method signatures, make signatures flexible.

  • Define global inner Apex classes to serve as containers for input and output parameters. These classes can be in the same top-level class as the invocable method.
  • Annotate both input and output classes with @JsonAccess(serializable='always' deserializable='always'). The @JsonAccess annotation governs the serialization and deserialization of managed Apex. Because Agentforce serializes and deserializes complex Apex types from an unmanaged context at run time, both @JsonAccess parameters must be set to 'always'.
  • Within these input and output classes, declare global member variables annotated with @InvocableVariable(label='User-Friendly Name' description='Description of this parameter' required=true/false).
    • Use clear and descriptive label and description modifiers, so that subscribers can configure the inputs declaratively, and Agentforce can understand the output.
    • Set the required modifier as true or false to specify whether the input is required for the agent action to run. This modifier also helps subscriber admins configure your actions.
  • Define the invocable method to accept a List of its input class type, for example List<MyInputAction> requests.
  • Define the invocable method to return a List of its output class type, for example List<MyOutputAction> results.

Example Code for an Apex Agent Action

In this example, the getCoordinates method is defined as an @InvocableMethod so it can be invoked by Agentforce. The method accepts a list of GeocodingRequest objects and returns a corresponding list of GeocodingResponse objects. The input and output wrapper classes are both annotated with @JsonAccess(serializable='always' deserializable='always') so Agentforce can serialize and deserialize the objects from an unmanaged context. The properties of both wrapper classes are defined as @InvocableVariable so an admin can configure them declaratively. The label and description modifiers in both @InvocableMethod and @InvocableVariable are important because they help the Agentforce reasoning engine to understand how to use the action.

1// --- ISV's Managed Package Code ---
2// --- 1. The Global Entrypoint Class ---
3// This class contains the @InvocableMethod and the input/output wrapper classes.
4global with sharing class GeocodingAction {
5    // This method is the thin, global entry point that delegates any business logic to a separate public class.
6        @InvocableMethod(
7            label='Get Coordinates for Address'
8            description='Retrieves the latitude and longitude for a given street address.'
9            category='My ISV App Name'
10        )
11        global static List<GeocodingResponse> getCoordinates(List<GeocodingRequest> requests) {
12            // Delegate the entire list to the internal logic class to ensure
13            // any callouts or DML can be performed in bulk.
14                GeocodingLogic logic = new GeocodingLogic();
15                return logic.performGeocoding(requests);
16        }
17    
18        // --- Input Wrapper Inner Class ---
19        // Defines the parameters an admin can configure for this action.
20        @JsonAccess(serializable='always' deserializable='always')
21        global with sharing class GeocodingRequest {
22            @InvocableVariable(label='Street' required=true)
23            global String street;
24    
25            @InvocableVariable(label='City' required=true)
26            global String city;
27    
28            @InvocableVariable(label='State/Province' required=true)
29            global String state;
30    
31            @InvocableVariable(label='Postal Code' required=true)
32            global String postalCode;
33        }
34    
35        // --- Output Wrapper Inner Class ---
36        // Defines the structured result returned to Agentforce.
37        @JsonAccess(serializable='always' deserializable='always')
38        global with sharing class GeocodingResponse {
39            @InvocableVariable(label='Was Successful')
40            global Boolean isSuccess;
41    
42            @InvocableVariable(label='Latitude')
43            global Decimal latitude;
44    
45            @InvocableVariable(label='Longitude')
46            global Decimal longitude;
47    
48            @InvocableVariable(label='Error Message')
49            global String errorMessage;
50        }
51    
52    // Static factory methods for creating consistent results.
53        public static GeocodingResponse success(Decimal lat, Decimal lon) {
54            GeocodingResponse result = new GeocodingResponse();
55            result.isSuccess = true;
56            result.latitude = lat;
57            result.longitude = lon;
58            return result;
59        }
60
61        public static GeocodingResponse error(String message) {
62            GeocodingResponse result = new GeocodingResponse();
63            result.isSuccess = false;
64            result.errorMessage = message;
65            return result;
66        }
67}
68
69
70// --- 2. The Internal Logic Class (Public, not Global) ---
71// This is where the actual business logic lives.
72// It's separate from the global entry point class for better organization and testing.
73public with sharing class GeocodingLogic {
74    // Since we defined the inputs and outputs as inner classes, we use dot notation to reference them.
75    public List<GeocodingAction.GeocodingResponse> performGeocoding(List<GeocodingAction.GeocodingRequest> requests) {
76        List<GeocodingAction.GeocodingResponse> results = new List<GeocodingAction.GeocodingResponse>();
77
78        // This method would contain your complex, bulkified business logic.
79        // For example, you can aggregate all requests into a single callout
80        // to an external geocoding service.
81
82        // For this simplified example, we loop and return mock results.
83        for (GeocodingAction.GeocodingRequest req : requests) {
84            // In a real implementation, you would perform a callout and handle errors.
85            if (req.postalCode == '94105') {
86                results.add(GeocodingAction.success(37.7749, -122.4194));
87            } else {
88                results.add(GeocodingAction.error('Address could not be found.'));
89            }
90        }
91        return results;
92    }
93}