Designing Apex actions for Agentforce: input validation an agent can’t talk its way around

Written by

in

An Apex action built for a Flow has one source of input: a developer who read the spec, or a user filling in a screen with picklists and masks already doing half the validation for them. An Apex action built for Agentforce has a different source: a language model deciding, turn by turn, what values belong in each parameter. It reads your method’s label and description, forms an intent, and fills in the blanks. Most of the time it gets close enough. The failures show up in the ten percent where “close enough” produces a record with the wrong status, a case reassigned to the wrong queue, or a refund approved against an account that doesn’t exist.

This is the part of Agentforce work that doesn’t get much attention next to the demo-friendly stuff like topic design and grounding. But if you’re the one writing the Apex actions an agent calls, input validation is where most of your actual engineering time should go.

The description is a suggestion, not a contract

An @InvocableMethod annotation lets you set a label and description, and those strings do real work: they’re what the agent reads to decide whether this action fits the user’s request and how to populate its parameters. It’s tempting to treat a well-written description as validation. It isn’t. “Account Id of the customer, required” tells the model what you want. It doesn’t stop the model from passing a contact Id instead, a made-up string that looks like an Id, or an empty value when it couldn’t resolve one and decided to guess rather than ask.

@InvocableVariable(required=true) is similarly softer than it looks. It affects what the agent is nudged to supply, not what Apex is guaranteed to receive. I’ve seen teams ship an action, watch it work fine in testing with a cooperative tester, then get a null reference exception three weeks later because a real conversation took a path nobody scripted. The agent didn’t do anything wrong by its own logic. It just didn’t have a value, and the action didn’t check.

Validate inside the method, every time

Treat the parameters arriving from an invocable Apex action exactly like you’d treat a public API payload: untrusted until proven otherwise. Concretely, that means checking for null and blank before anything else runs, confirming record Ids actually resolve to the object type you expect (an Id typed variable doesn’t guarantee the right sObject, just a valid Id format), and re-checking any business rule the description implied rather than assumed. If the action only makes sense for open cases, check the case status in Apex. Don’t rely on the agent having read that constraint correctly off a sentence in the description.

A pattern that’s worked well for me: wrap the actual logic in a private method, and have the invocable method itself do nothing but validate and translate failures into a response the agent can act on. Something like this shape:

  • Check required fields are present and non-blank, collecting every violation rather than stopping at the first.
  • Resolve any Id to a real record with a targeted SOQL query, not a bare Id.valueOf() check.
  • Confirm the record is in a state the action is allowed to act on, inside Apex, not in the prompt.
  • Only then call the private method that does the actual work.

That last point matters more than it sounds. An agent’s reasoning happens in natural language, which means it can be talked into contradicting its own earlier state, repeating a step it already completed, or applying an instruction from three messages ago that no longer applies. Apex doesn’t have that problem, as long as you don’t let the prompt do the gatekeeping on your behalf.

Give the agent a response it can actually use

When validation fails, resist the urge to throw a generic exception and let the Flow or Apex runtime error bubble up as a stack trace. The agent can’t do anything useful with that, and neither can the person on the other end of the conversation. Return a structured, readable failure: which field was the problem, and ideally what a valid value looks like. “Case Id did not match an open case” lets the agent re-ask the user or try a different lookup. A raw System.NullPointerException just ends the conversation badly, usually with the agent telling the customer something isn’t working rather than something wasn’t provided.

Don’t let the action run with more power than the conversation deserves

A separate failure mode: actions that run in system context doing things no standard user in that seat would be allowed to do. It’s a fair question to ask whether a given action should run without sharing at all. If an agent conversation shouldn’t be able to see or touch a record a human agent couldn’t, the Apex action shouldn’t either. I’d default to with sharing and only step outside it with a specific, documented reason, the same discipline you’d apply to any other integration user. An LLM making the call doesn’t change the sharing model; it just makes it easier to forget you’re still subject to one.

Test it like someone’s trying to break it

Standard Apex test classes covering happy-path invocations won’t catch what actually goes wrong in production, because the thing generating bad input isn’t a developer who skipped a null check. It’s a model improvising around a gap in context. Write test methods that call your invocable method directly with the inputs a confused or under-specified conversation would plausibly produce: blank strings instead of nulls, an Id from the wrong object, a valid Id for a record in the wrong status, duplicate calls for the same intent. If you can, run a handful of adversarial conversations against the actual agent in a sandbox before go-live and watch what it tries to pass. You’ll usually find at least one gap that no amount of prompt engineering would have caught, because the fix was never going to live in the prompt.

None of this is exotic. It’s the same input-validation discipline you’d apply to any Apex method exposed to something you don’t fully control, whether that’s a REST callout, a Lightning component, or an integration user with a service account. Agentforce just removes the one thing that used to quietly cover for sloppy validation: a human reading the screen before they clicked submit.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *