Agent Action Error Handling
Agentforce agents receive the return value of an Invocable action as-is and feed it back into the LLM loop. An unhandled exception becomes a Flow or framework error — the agent cannot reason about it, often re-invokes the action with the same inputs, and occasionally confabulates success. This skill defines a Response type that always returns (status, reason_code, user_message) so the subagent instructions can route deterministically.
Terminology. Salesforce renamed agent topics to subagents in April 2026.
Nothing about behaviour changed, and the API surface kept the old word —
GenAiPlugin is still the metadata type behind a subagent, and GenAiFunction
is still the action. This skill says subagent instructions for what older
docs and your org's metadata call topic instructions.
The Platform Rule Everything Follows From
An @InvocableMethod must return one result per input, in the same order,
even when errors occur — the inputs and outputs "must match on both the size
and the order"
(InvocableMethod Annotation).
So an error is not an exception you throw. It is a value you return in the
slot belonging to the failing request. A thrown exception breaks the contract
for the whole batch, not just the request that failed.
Two further platform rules shape the class: the method must be static,
public or global, and live in an outer class; and there is one
invocable method per class. One action is one verb — see
templates/agentforce/AgentActionSkeleton.cls.
The Envelope
| Field |
Type |
Purpose |
status |
OK | USER_ERROR | SYSTEM_ERROR |
Locus of control — whose problem is this |
reasonCode |
stable enum string |
What the subagent instructions branch on |
userMessage |
≤140 chars, authored |
Safe to read aloud verbatim |
retryable |
Boolean |
Would the same inputs plausibly succeed |
status and retryable are independent axes. A row lock is a retryable system
error; a validation failure is a terminal user error; a 503 is a retryable
system error. Collapsing them into one Boolean success is the single most
common cause of agent retry loops.
Recommended Workflow
- Design the Response class with the four fields above. Register every
reasonCode in Agent_Reason_Code__mdt so Apex, subagent instructions, and
dashboards share one contract.
- Structure the method in two phases — validate and collect, then one bulkified
Database.<dml>(records, false, AccessLevel.USER_MODE) — and assign results
back by index into a pre-sized output list. Per-row DML raises an
uncatchable LimitException under bulk invocation, which defeats every
catch you write.
- Classify by locus of control, branching on
Database.Error.getStatusCode()
or the HTTP status code — never on message text, which is localised and
reworded between releases. Log the raw exception to
templates/apex/ApplicationLogger.cls; never put getMessage() in
userMessage.
- Write one test per reason code asserting the code, the
retryable flag, and
that no raw platform error text reached userMessage. Coverage percentage is
not the signal; assertions-per-reason-code is.
- Add one subagent instruction per outcome class, including an explicit "do not
call the action again" for terminal codes — the planner's default on an
unsatisfied goal is to retry.
- Run the Testing Center /
sf agent test run scenarios that exercise each
branch and confirm the agent's wording matches the instruction for each code.
Key Considerations
userMessage is user-visible and lands in a stored transcript. Validation-rule
error text can interpolate field values, so getMessage() is a PII channel as
well as an implementation-detail leak.
- Distinguishing
NOT_FOUND from NO_ACCESS helps internal users and leaks
record existence on a guest channel. Decide per channel.
retryable = true on a mutating action requires idempotency (a caller-supplied
key upserted on an External Id). Otherwise a retried timeout is a duplicate
write.
- The
@InvocableMethod and @InvocableVariable description values are read
by the planner to decide whether to call the action, and pre-fill the
instruction fields in Agentforce Builder — they are functional, not
documentation.
- Watch the
SYSTEM_ERROR/UNKNOWN ratio in production. It is the direct
measure of classification debt; a jump after a release means an upstream
contract changed.
Worked Examples (see references/examples.md)
- Typed Response envelope for a Case-update action — Agentforce service agent closes Cases on user request.
- Stable reason_code enum across releases — A new DML error type appears after a managed-package install.
Common Gotchas (see references/gotchas.md)
- Throwing AuraHandledException from an Invocable — The agent receives an opaque framework message and loops.
- Governor-limit exceptions pre-empt your catch — LimitException (uncatchable) kills the transaction — agent sees raw Flow failure.
- Empty list returned when input list is empty — Agent receives an empty array and hallucinates a success message.
Top LLM Anti-Patterns (full list in references/llm-anti-patterns.md)
- Rethrowing exceptions from an @InvocableMethod — the LLM cannot reason about framework errors.
- Putting
ex.getMessage() directly into user_message — leaks internals and breaks deterministic subagent routing.
- Using boolean
success instead of a reason_code enum — the agent cannot distinguish retryable from terminal failures.
Official Sources Used
1---2name: agent-action-error-handling3description: Design Invocable Apex actions that return deterministic, agent-friendly errors instead of surfacing raw exceptions to the LLM. NOT for authoring the action itself — @InvocableMethod schema, labels, security context — use agentforce/custom-agent-actions-apex. NOT for the Apex tests that force each error branch — use agentforce/agent-action-unit-tests.4---56# Agent Action Error Handling78Agentforce agents receive the return value of an Invocable action as-is and feed it back into the LLM loop. An unhandled exception becomes a Flow or framework error — the agent cannot reason about it, often re-invokes the action with the same inputs, and occasionally confabulates success. This skill defines a Response type that always returns (status, reason_code, user_message) so the subagent instructions can route deterministically.910> **Terminology.** Salesforce renamed agent *topics* to *subagents* in April 2026.11> Nothing about behaviour changed, and the API surface kept the old word —12> `GenAiPlugin` is still the metadata type behind a subagent, and `GenAiFunction`13> is still the action. This skill says *subagent instructions* for what older14> docs and your org's metadata call *topic instructions*.1516## The Platform Rule Everything Follows From1718An `@InvocableMethod` must return one result per input, **in the same order,19even when errors occur** — the inputs and outputs *"must match on both the size20and the order"*21([InvocableMethod Annotation](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_InvocableMethod.htm)).2223So an error is not an exception you throw. It is a **value you return in the24slot belonging to the failing request**. A thrown exception breaks the contract25for the whole batch, not just the request that failed.2627Two further platform rules shape the class: the method must be `static`,28`public` or `global`, and live in an **outer** class; and there is **one29invocable method per class**. One action is one verb — see30`templates/agentforce/AgentActionSkeleton.cls`.3132## The Envelope3334| Field | Type | Purpose |35|---|---|---|36| `status` | `OK` \| `USER_ERROR` \| `SYSTEM_ERROR` | Locus of control — whose problem is this |37| `reasonCode` | stable enum string | What the subagent instructions branch on |38| `userMessage` | ≤140 chars, authored | Safe to read aloud verbatim |39| `retryable` | Boolean | Would the *same* inputs plausibly succeed |4041`status` and `retryable` are independent axes. A row lock is a retryable system42error; a validation failure is a terminal user error; a 503 is a retryable43system error. Collapsing them into one `Boolean success` is the single most44common cause of agent retry loops.4546## Recommended Workflow47481. Design the Response class with the four fields above. Register every49 `reasonCode` in `Agent_Reason_Code__mdt` so Apex, subagent instructions, and50 dashboards share one contract.512. Structure the method in two phases — validate and collect, then one bulkified52 `Database.<dml>(records, false, AccessLevel.USER_MODE)` — and assign results53 back by index into a pre-sized output list. Per-row DML raises an54 *uncatchable* `LimitException` under bulk invocation, which defeats every55 `catch` you write.563. Classify by locus of control, branching on `Database.Error.getStatusCode()`57 or the HTTP status code — never on message text, which is localised and58 reworded between releases. Log the raw exception to59 `templates/apex/ApplicationLogger.cls`; never put `getMessage()` in60 `userMessage`.614. Write one test per reason code asserting the code, the `retryable` flag, and62 that no raw platform error text reached `userMessage`. Coverage percentage is63 not the signal; assertions-per-reason-code is.645. Add one subagent instruction per outcome class, including an explicit *"do not65 call the action again"* for terminal codes — the planner's default on an66 unsatisfied goal is to retry.676. Run the Testing Center / `sf agent test run` scenarios that exercise each68 branch and confirm the agent's wording matches the instruction for each code.6970## Key Considerations7172- `userMessage` is user-visible and lands in a stored transcript. Validation-rule73 error text can interpolate field values, so `getMessage()` is a PII channel as74 well as an implementation-detail leak.75- Distinguishing `NOT_FOUND` from `NO_ACCESS` helps internal users and leaks76 record existence on a guest channel. Decide per channel.77- `retryable = true` on a mutating action requires idempotency (a caller-supplied78 key upserted on an External Id). Otherwise a retried timeout is a duplicate79 write.80- The `@InvocableMethod` and `@InvocableVariable` `description` values are read81 by the planner to decide whether to call the action, and pre-fill the82 instruction fields in Agentforce Builder — they are functional, not83 documentation.84- Watch the `SYSTEM_ERROR/UNKNOWN` **ratio** in production. It is the direct85 measure of classification debt; a jump after a release means an upstream86 contract changed.8788## Worked Examples (see `references/examples.md`)8990- *Typed Response envelope for a Case-update action* — Agentforce service agent closes Cases on user request.91- *Stable reason_code enum across releases* — A new DML error type appears after a managed-package install.9293## Common Gotchas (see `references/gotchas.md`)9495- **Throwing AuraHandledException from an Invocable** — The agent receives an opaque framework message and loops.96- **Governor-limit exceptions pre-empt your catch** — LimitException (uncatchable) kills the transaction — agent sees raw Flow failure.97- **Empty list returned when input list is empty** — Agent receives an empty array and hallucinates a success message.9899## Top LLM Anti-Patterns (full list in `references/llm-anti-patterns.md`)100101- Rethrowing exceptions from an @InvocableMethod — the LLM cannot reason about framework errors.102- Putting `ex.getMessage()` directly into user_message — leaks internals and breaks deterministic subagent routing.103- Using boolean `success` instead of a reason_code enum — the agent cannot distinguish retryable from terminal failures.104105## Official Sources Used106107- InvocableMethod Annotation (Apex Developer Guide) — https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_InvocableMethod.htm108- Create Custom Actions Using Apex InvocableMethod — https://developer.salesforce.com/docs/ai/agentforce/guide/agent-invocablemethod.html109- Agentforce Actions — https://developer.salesforce.com/docs/ai/agentforce/guide/get-started-actions.html110- Agentforce Metadata Types — https://developer.salesforce.com/docs/ai/agentforce/references/agents-metadata-tooling/agents-metadata.html111- Agentforce Testing Center — https://help.salesforce.com/s/articleView?id=ai.agent_testing_center.htm&type=5