Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Flow

The Apex action <name> couldn't be invoked because it's not @InvocableMethod or has the wrong signature

A Flow tried to call an Apex method as an action, but the method either lacks the `@InvocableMethod` annotation, doesn't return / accept a `List` (Invocable methods are bulkified by contract), or its parameter type isn't supported. The fix is to refactor the Apex method into invocable shape.

Also seen asApex action couldn't be invoked·InvocableMethod·wrong signature flow apex·The flow tried to call an Apex action

A business analyst builds a record-triggered flow that calls a custom Apex action to score a new Case. The Apex class compiled cleanly in the deploy. When the analyst tests the flow in Flow Builder, the action picker shows the class. When the flow runs against a real Case, the screen flashes red with "The Apex action CaseScoreAction couldn't be invoked because it's not @InvocableMethod or has the wrong signature." The class is decorated with @InvocableMethod. The flow expects to pass a single Case. Something else is off.

What the platform is checking

Flow Builder calls Apex through the Invocable Action mechanism. The platform inspects the target class for a method decorated with the @InvocableMethod annotation. The method must follow a precise signature contract: it must be public static, return a List<T>, and accept a single parameter of type List<T>. The platform binds flow inputs to the parameter list and flow outputs to the return list.

When any part of that contract is violated, the runtime refuses to invoke the method. The error message names the class but does not always name the specific contract violation. The developer needs to inspect the class for each requirement.

The contract is strict because Flow Builder relies on metadata introspection to drive the action picker and parameter mapping. A method that almost matches but not quite would produce confusing UI behavior or runtime data corruption. The platform refuses ambiguous signatures.

Common violations include: returning a single value instead of a List, accepting a single value instead of a List, missing the static modifier, mismatched generic types, multiple methods annotated @InvocableMethod in the same class without distinguishing them, or wrapping classes that do not implement the required serialization contracts.

What actually fails

Five categories cover most production failures.

Missing or misplaced annotation. The @InvocableMethod annotation must sit directly on the method, not the class. Forgetting it entirely produces a method that compiles but is invisible to Flow.

Wrong return shape. The method must return a List<T> where T is a class with @InvocableVariable-annotated fields. Returning a single instance, a Map, or a primitive breaks the contract.

Wrong parameter shape. Same as the return: the method accepts List<T> only. A method that takes individual primitives or an SObject directly does not bind.

Missing static modifier. The method must be static. An instance method with @InvocableMethod produces the invocation error.

Multiple invocable methods in one class. A class can only contain one method decorated with @InvocableMethod. A second annotated method in the same class causes the runtime to refuse all of them.

The broken example

A class intended to score Cases, called from a flow:

public class CaseScoreAction {

    public Integer score(Case c) {
        Integer points = 0;
        if (c.Priority == 'High') points += 10;
        if (c.Subject?.contains('urgent')) points += 5;
        if (c.IsEscalated) points += 8;
        return points;
    }
}

The class compiles. The flow builder shows CaseScoreAction in the action picker because Salesforce surfaces any public Apex class. At runtime, the flow fails with the invocation error.

The problems compound: no annotation, instance method instead of static, single Case instead of List<Case>, integer return instead of List of wrappers. None of the four requirements is met.

A second shape: a class with the annotation but in the wrong place:

@InvocableMethod
public class CaseScoreAction {
    public static List<Integer> score(List<Case> cases) {
        List<Integer> scores = new List<Integer>();
        for (Case c : cases) {
            Integer points = 0;
            if (c.Priority == 'High') points += 10;
            scores.add(points);
        }
        return scores;
    }
}

The annotation is on the class instead of the method. The compiler does not flag this in older API versions. The runtime cannot find an invocable method and reports the same error.

The fix, three paths

Restructure the method signature. The canonical pattern uses a request wrapper and a response wrapper. The wrappers expose @InvocableVariable fields that Flow maps to flow variables.

public class CaseScoreAction {
    public class Request {
        @InvocableVariable(required=true label='Case')
        public Case caseRecord;
    }

    public class Response {
        @InvocableVariable(label='Score')
        public Integer score;
    }

    @InvocableMethod(label='Score Case' description='Calculates a priority score for a Case')
    public static List<Response> scoreCases(List<Request> requests) {
        List<Response> responses = new List<Response>();
        for (Request req : requests) {
            Case c = req.caseRecord;
            Integer points = 0;
            if (c.Priority == 'High') points += 10;
            if (c.Subject?.contains('urgent')) points += 5;
            if (c.IsEscalated) points += 8;
            Response resp = new Response();
            resp.score = points;
            responses.add(resp);
        }
        return responses;
    }
}

The class now has: a static method with @InvocableMethod, a List parameter (Request wrapper), a List return (Response wrapper), and @InvocableVariable fields on both wrappers. Flow Builder sees the method, presents the inputs and outputs cleanly, and binds flow variables to wrapper fields.

Accept SObjects directly when wrappers add no value. For simple actions on a single SObject type, you can skip wrappers and accept the SObject list directly.

public class CaseScoreAction {
    @InvocableMethod(label='Score Case' description='Calculates a priority score for a Case')
    public static List<Integer> scoreCases(List<Case> cases) {
        List<Integer> scores = new List<Integer>();
        for (Case c : cases) {
            Integer points = 0;
            if (c.Priority == 'High') points += 10;
            if (c.IsEscalated) points += 8;
            scores.add(points);
        }
        return scores;
    }
}

Flow Builder accepts SObject lists and primitive lists directly. The wrapper pattern is preferred when you need multiple input fields or output fields, but for a single SObject in and a single primitive out, the simpler signature works.

Move secondary methods to a separate class. When a class needs more than one invocable method, split them into separate classes. Each class hosts exactly one invocable method.

public class CaseScoreAction {
    @InvocableMethod(label='Score Case')
    public static List<Integer> scoreCases(List<Case> cases) { /* ... */ }
}

public class CaseRouteAction {
    @InvocableMethod(label='Route Case')
    public static List<Id> routeCases(List<Case> cases) { /* ... */ }
}

Each class is independently discoverable in Flow Builder. The user picks the action they want from a clean list.

The fixed example

A complete invocable Apex class with multiple inputs and outputs:

public class CaseScoreAction {
    public class Request {
        @InvocableVariable(required=true label='Case Record')
        public Case caseRecord;

        @InvocableVariable(label='Priority Multiplier' description='Multiplier applied to priority points')
        public Decimal multiplier = 1.0;
    }

    public class Response {
        @InvocableVariable(label='Score')
        public Integer score;

        @InvocableVariable(label='Recommended Action')
        public String recommendation;
    }

    @InvocableMethod(label='Score Case' description='Calculates a score and recommends an action' category='Case Management')
    public static List<Response> scoreCases(List<Request> requests) {
        List<Response> responses = new List<Response>();
        for (Request req : requests) {
            Case c = req.caseRecord;
            Integer points = 0;
            if (c.Priority == 'High') points += 10;
            if (c.IsEscalated) points += 8;
            if (c.Subject != null && c.Subject.toLowerCase().contains('urgent')) points += 5;

            Decimal mult = req.multiplier ?? 1.0;
            Integer finalScore = (Integer)(points * mult);

            Response resp = new Response();
            resp.score = finalScore;
            resp.recommendation = finalScore > 15 ? 'Escalate' : finalScore > 8 ? 'Review' : 'Standard';
            responses.add(resp);
        }
        return responses;
    }
}

The annotation specifies the label, description, and category that Flow Builder uses for the action picker. The Request and Response wrappers each define their own labeled variables. Flow variable binding is now declarative on both sides.

Edge cases and gotchas

Bulk invocation semantics. Flow Builder calls the method with a list of requests when the flow runs in bulk mode (a record-triggered flow that fires for multiple records in the same transaction). The list size is not always one. The method must iterate. Returning the wrong-sized response list mis-aligns the bound output variables.

public static List<Response> scoreCases(List<Request> requests) {
    List<Response> responses = new List<Response>();
    for (Request req : requests) {
        // produce one Response per Request
    }
    return responses;
}

The output list size must match the input list size for flow to map variables correctly. Skipping a request means downstream flow logic gets the wrong response indexed at the wrong position.

Variable types and serialization. @InvocableVariable supports primitive types, SObjects, lists of primitives, lists of SObjects, and Apex classes annotated with @InvocableVariable. Custom classes without the right annotations on their fields are invisible to Flow.

Required vs optional inputs. A flow variable bound to a required InvocableVariable must be populated at runtime. A null or missing input causes the flow to fail with a different error. Mark variables required only when the action genuinely cannot function without them.

Method overloading is not supported. Multiple invocable methods with different signatures in the same class break the contract. The annotation must be on a single method.

SObject generics. A method that accepts List<SObject> (generic) is allowed but Flow Builder presents the input as "any record" with no type-specific picklists. For ergonomic flow building, type the parameter to the specific SObject (List<Case>, List<Opportunity>).

Namespace prefix on package classes. When the class is in a managed package, Flow Builder shows the namespaced class name. References from outside the package use Namespace.ClassName. Within the package, the unqualified name works.

Defensive habits

Always wrap inputs and outputs in named classes. Even for single-field actions, the wrapper pattern makes the API explicit and forward-compatible. Adding a second input later is a non-breaking change when there is already a wrapper.

Validate inputs at the top of the method. Null record references, missing required fields, and inputs that violate business rules should produce a clear exception rather than silently producing garbage output.

Use the category parameter on @InvocableMethod to group related actions. Flow Builder's action picker organizes by category, and consistent grouping helps flow builders find what they need.

Document the action with the description parameter. The description appears in Flow Builder as hover help. A clear one-line description prevents misuse.

Write unit tests that call the method directly. The method is callable from any Apex context, not just Flow. Direct unit tests verify the contract without the overhead of running a flow.

Test patterns

A unit test for the invocable method:

@IsTest
static void scoringHandlesHighPriorityCase() {
    Case c = new Case(Priority = 'High', Subject = 'Urgent: cannot log in', IsEscalated = true);
    insert c;

    CaseScoreAction.Request req = new CaseScoreAction.Request();
    req.caseRecord = c;
    req.multiplier = 1.5;

    Test.startTest();
    List<CaseScoreAction.Response> resp = CaseScoreAction.scoreCases(new List<CaseScoreAction.Request>{req});
    Test.stopTest();

    System.assertEquals(1, resp.size());
    System.assert(resp[0].score >= 20);
    System.assertEquals('Escalate', resp[0].recommendation);
}

The test exercises the action without involving a flow. Issues with the wrapper shape, the score logic, or the recommendation thresholds surface here before the flow is even built.

A flow test using the Test framework:

@IsTest
static void flowRunsInvocableAction() {
    Map<String, Object> params = new Map<String, Object>{
        'inputCase' => new Case(Priority = 'High')
    };
    Flow.Interview.MyFlow flow = new Flow.Interview.MyFlow(params);
    flow.start();
    Integer outScore = (Integer) flow.getVariableValue('outputScore');
    System.assert(outScore > 0);
}

The flow test verifies the end-to-end binding from flow variable to wrapper field to method parameter and back.

Diagnosing in production

When the invocation error fires:

  1. Open the Apex class in Setup or the IDE.
  2. Verify the annotation is on the method, not the class.
  3. Verify the method is public static and returns List<T>.
  4. Verify the parameter is List<T> where T has @InvocableVariable fields.
  5. Verify there is exactly one @InvocableMethod annotation in the class.
  6. Recompile, deploy, and re-test the flow.

If the action does not appear in Flow Builder at all, the annotation is missing or misplaced. If it appears but fails at runtime, the signature is incomplete.

Why the contract is strict

Flow Builder is a low-code tool used by administrators, not just developers. The strict contract is what makes the action picker reliable: every method that conforms shows up the same way, with the same variable mapping behavior. A loose contract would mean some methods show up and bind correctly while others fail in mysterious ways. The strictness is a feature, not a limitation.

The List-in, List-out signature reflects the bulk-execution semantics of flows. When a record-triggered flow runs for many records in the same transaction, the platform batches the invocations. The Apex method receives one list with all the requests, processes them together, and returns one list of responses. The platform aligns the responses with the originating records by position. The contract guarantees this alignment works correctly.

Quick recovery checklist

  1. Add the @InvocableMethod annotation to the method.
  2. Change the method to public static.
  3. Change the parameter to a List type.
  4. Change the return to a List type.
  5. Add @InvocableVariable annotations to wrapper fields.
  6. Redeploy and retest the flow.

The fix is usually a small structural change to the class. Once corrected, the action runs reliably and the flow integration is stable.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Flow errors