SfSkills Domains Docs GitHub

Home / apex / apex-callable-interface

apex-callable-interface

Use when building Apex classes meant to be invoked dynamically — from Flow, external packages, managed-package extensions, or loose-coupling code that cannot directly reference the concrete class. Trigger keywords: Callable, call method, dynamic Apex, action registry, plugin pattern, managed package extension point. NOT for an @InvocableMethod action exposed to Flow — use apex/invocable-methods. NOT for exposing the class as a REST endpoint — use apex/apex-rest-services.

Open the skill folder on GitHub


Apex Callable Interface

Activate this skill when Apex must be invoked dynamically without the caller having a compile-time reference. The System.Callable interface provides a single-method contract (call(String action, Map<String, Object> args)) that lets Flow, managed package consumers, and service registries address any implementing class by type name and action string.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The System.Callable Interface

One method: Object call(String action, Map<String, Object> args).

Dynamic Instantiation Via Type.forName + Cast

A caller typically looks like:

Type t = Type.forName(namespace, className);
if (t == null) throw new HandlerNotFoundException(className);
Object instance = t.newInstance();
if (!(instance instanceof Callable)) {
    throw new NotCallableException(className);
}
Object result = ((Callable) instance).call(action, args);

The indirection is the whole point — the caller has zero compile-time coupling to the implementation.

Extension-Point Pattern (Managed Package)

Managed packages can ship a Callable with public action strings. Subscribers implement the same Callable in their org with custom logic, and the package looks up the subscriber's class via a custom metadata record or custom setting.

Flow Compatibility

Apex Callable is NOT directly invokable from Flow. Flow needs @InvocableMethod. Callable is for code-to-code dispatch — often behind an @InvocableMethod facade when Flow is a consumer.


Common Patterns

Plugin Action Registry

When to use: You have a fixed set of "hook" points where admins or subscribers should be able to inject logic.

How it works:

public with sharing class PluginRegistry {
    public static Object invoke(String pluginApiName, String action, Map<String, Object> args) {
        Plugin__mdt config = Plugin__mdt.getInstance(pluginApiName);
        if (config == null) return null;
        Type t = Type.forName(config.Namespace__c, config.ClassName__c);
        if (t == null || !Callable.class.isAssignableFrom(t)) {
            throw new PluginException('Plugin not found or not Callable: ' + pluginApiName);
        }
        return ((Callable) t.newInstance()).call(action, args);
    }
}

Why not the alternative: Hardcoded if (pluginName == 'X') new X() requires redeployment for every new plugin.

Documented Action Contract

When to use: Every Callable class where you expect multiple actions.

How it works:

global with sharing class OrderFulfillmentActions implements Callable {
    // Actions:
    //   'reserveInventory': args { 'orderId': Id } -> Id (reservation id)
    //   'cancelReservation': args { 'reservationId': Id } -> Boolean
    //   'quote': args { 'productIds': Set<Id>, 'qty': Map<Id, Integer> } -> Decimal
    global Object call(String action, Map<String, Object> args) {
        switch on action {
            when 'reserveInventory' { return reserveInventory((Id) args.get('orderId')); }
            when 'cancelReservation' { return cancelReservation((Id) args.get('reservationId')); }
            when 'quote' { return quote(args); }
            when else { throw new CalloutException('Unknown action: ' + action); }
        }
    }
    // ...
}

Why not the alternative: Undocumented Map<String, Object> contracts lead to runtime casts that fail silently.


Decision Guidance

SituationRecommended ApproachReason
Flow needs to invoke Apex@InvocableMethodCallable is not wired to Flow directly
Managed package extension pointCallable via metadataLoose coupling survives package updates
In-repo dispatch by configCallable via metadataRemoves hardcoded if/else branches
REST client calling Apex@RestResourceCallable is not a REST endpoint
Scheduled or async jobQueueable / SchedulableCallable runs in caller's transaction
Type-safe helper classRegular Apex classCallable is for dynamic dispatch only

  1. Confirm the caller actually needs dynamic dispatch (most don't — direct class reference is simpler).
  2. Define the action vocabulary as comments at the top of the class — name, expected keys, return type.
  3. Implement call with a switch on action and throw on unknown actions.
  4. Add TypeException-safe casts on every args.get(...) call.
  5. Write tests: a happy-path test per action plus an "unknown action" test that asserts the expected exception.
  6. If the class is a managed-package extension point, ship a reference implementation and document the contract in the package's help.

Review Checklist


Salesforce-Specific Gotchas

  1. Callable is synchronous — calls run in the caller's transaction, share governor limits, and cannot be enqueued by the interface alone.
  2. Type.forName(null, 'X') searches the caller's namespace — pass the correct namespace explicitly in a managed-package context.
  3. Callable.call return is Object — callers must cast; a typo in the action string yields a runtime exception, not a compile error.
  4. Args map is not validated — missing keys return null from .get(), casts to primitives may NullPointerException or TypeException.
  5. No Aura/LWC direct access — Callable is Apex-to-Apex; UI layers should go through @AuraEnabled facades.
  6. Removing an action is a breaking change — downstream consumers have no compile-time contract, so silent regressions are common.

Output Artifacts

ArtifactDescription
scripts/check_apex_callable_interface.pyScans for unguarded args.get casts, missing when else, and Callable used where @InvocableMethod is correct
templates/apex-callable-interface-template.mdWork template for defining a Callable class with a documented action contract

Package files