Apex, Flow & Enterprise Architecture
The smallest Invocable Apex method can create the largest coupling
The annotation changes the architecture
A developer adds @InvocableMethod to a small Apex utility so one Flow can call it. Months later, the same action appears in several record-triggered flows, a screen flow and an Agentforce configuration. The class still looks tiny, but its signature has become shared infrastructure. A rename, permission change or altered error can now break several consumers.
Salesforce documents that @InvocableMethod exposes Apex as an invocable action and that @InvocableVariable defines its input and output schema. The Actions API can also discover and invoke custom Apex actions. The JSBC Labs position is therefore simple: adding the annotation is an interface-design decision. Review it with the discipline applied to a public API, even if the first consumer is only one diagram.
Put a domain contract at the boundary
Do not pass a large sObject merely because Flow already has one. That exposes the action to every field and encourages callers to assume that populated values are current, authorised and complete. Prefer request and response classes containing the minimum durable concepts the operation needs: recordId, effectiveDate, requestedOutcome and a caller-supplied correlation key, for example.
Name variables for business meaning rather than implementation. createInvoiceRequest is clearer than inputRecord, and eligibilityResult is more durable than booleanValue. Treat required inputs, accepted ranges, null behaviour and output meanings as a published schema. Reject ambiguity at the edge instead of manufacturing defaults deep inside the service.
Design for collections on the first day
An invocable action receives work as a collection because automation can aggregate interviews across records. Code that reads only the first request, queries inside a loop or performs one DML statement per item may pass a single-record demonstration and fail when a data load, API update or scheduled path produces volume. The boundary must be collection-native even when today's Flow sends one item.
Gather identifiers, query once, calculate with maps and sets, and write records in grouped operations. Return one outcome per request in a predictable order when the consumer needs record-level results. If some inputs can succeed while others fail, define that explicitly; do not alternate unpredictably between partial success and a transaction-wide exception. Salesforce governor limits apply to the shared transaction, so bulk behaviour is part of correctness, not merely performance tuning.
Keep orchestration visible and rules testable
The strongest hybrid design gives Flow the orchestration that administrators need to see while Apex owns logic that benefits from code: complex calculation, efficient collection processing, reusable validation or specialised integration handling. Salesforce's record-triggered automation guidance describes this hybrid model as powerful but cautions that it is not suitable for every case and carries runtime and context trade-offs.
Keep the annotated class thin. It should validate requests, establish security and transaction policy, call a domain service, then translate results into the public response. The domain service should not know that Flow invoked it. This separation lets Apex tests exercise business rules directly and permits another entry point to reuse the capability without pretending that an automation-specific wrapper is the domain model.
Declare the security contract
A Flow label such as 'Approve rebate' does not grant permission to approve rebates. The action must know whose authority applies, which records that identity may access and whether any elevated operation is intentional. Salesforce's secure Apex guidance distinguishes record sharing from object- and field-level access and recommends explicit access modes so behaviour remains maintainable across API versions.
Define class sharing, query mode and DML mode rather than inheriting accidental defaults. Validate input IDs instead of trusting that a caller only supplies records visible in the Flow screen. Put privileged work in a narrowly scoped service, document the justification and test an authorised persona alongside a denied persona.
Return outcomes, not surprises
Throwing a raw exception may stop a transaction, but it gives the Flow little useful control and can expose implementation detail. For expected business outcomes, return a structured result such as success, statusCode, recordId, message and retryable. Reserve exceptions for conditions where the transaction truly cannot continue, and ensure the Flow has a fault path for that possibility.
Separate validation failure, authorisation failure, business rejection, dependency failure and unexpected defect. They need different user messages, telemetry and recovery actions. A payment decline is not an Apex fault; a timeout may be retryable; a missing required input should never enter the domain service. Stable status codes let consumers branch safely while human-readable messages can improve without forcing every Flow to change its logic.
Callouts require a transaction decision
Salesforce documents a callout modifier for invocable methods that contact external systems. Setting it is necessary, but it does not answer the architectural questions: is the user waiting, can the remote operation be repeated, what happens after a timeout, and which system owns reconciliation? A synchronous action couples the Flow's completion to the dependency's latency and availability.
For short, user-facing calls, set an explicit timeout and return a controlled outcome. For long-running or high-volume work, consider recording an intent and handing processing to an asynchronous mechanism with idempotency, retry policy and monitoring. Do not enqueue work and report 'success' when the business operation is only accepted. Return a state such as queued with a tracking identifier so the caller and support team understand what has—and has not—completed.
Version by addition, not silent mutation
Salesforce notes that removing the invocable annotation from an action used by a Flow causes a runtime error. Less dramatic changes can still break meaning: changing a default, reinterpreting an enum or returning a different ordering may leave every Flow technically valid while producing the wrong business result. Dependency tools can show references, but they cannot prove semantic compatibility.
Classify changes before deployment. Add optional fields with safe behaviour when the contract can evolve compatibly. For a breaking change, publish a new action with an explicit version or purpose, migrate consumers deliberately, observe both versions and retire the old one only after dependency and runtime evidence agree. Keep a small contract document beside the code recording inputs, outputs, security mode, failure codes, limits and owners.
Test the interface as consumers experience it
Unit-test the domain service, then test the invocable wrapper with multiple requests, duplicates, missing values, mixed authorised and unauthorised records, dependency failures and limit-sensitive volume. Assert correlation between each request and response, not only aggregate success. If the action performs a callout, mock success, timeout, malformed payload and remote rejection while verifying that each maps to the promised outcome.
Include Flow-level tests or deployment checks for critical consumers, because Apex tests cannot prove that variables are mapped correctly in every diagram. Monitor invocation count, latency, failure class and queued-work completion in production. The durable rule is that code size does not measure architectural reach: the smallest invocable method can become the most widely coupled interface in the org.
Official references
- Salesforce Developers: InvocableMethod Annotation
- Salesforce Developers: InvocableVariable Annotation
- Salesforce Developers: Apex Actions
- Salesforce Architects: Record-Triggered Automation Decision Guide
- Salesforce Developers: Making Callouts from Invocable Actions
- Salesforce Developers: Secure Apex Classes
- Salesforce Developers: Execution Governors and Limits
- Salesforce Developers: Get Custom Invocable Actions