An admin asked me a fair question about my HanseWatt agent — HanseWatt is a fictional DACH energy retailer I built out in a demo org, so the company is invented, but the agent and its data model are real. She wanted to know who the agent would be, from the org’s point of view, at the moment it read a customer’s record.

I gave her the answer I had given a dozen times. It runs as the authenticated user. There is no anonymous service account hiding behind it. Every action carries a real person’s name.

All of that is true, and I still say it. What I had not noticed is that I had let it quietly stand in for a second sentence, one I never actually checked: and therefore the agent sees exactly what that person sees. Some weeks later, sweeping my own work, I found a field sitting in a prompt template that the running user had no permission to read. Same org. Same agent. Same true sentence.

This post is about the distance between those two claims, because for an agent that distance is where the surprises live.

Identity and visibility are two different facts

When we say an action “runs as” someone, we are answering a question about identity: whose name is on the request, whose session, whose entry in the audit trail. Salesforce answers that one beautifully. If your agent creates a record, the record shows a person, not “the AI”.

Visibility is a different question: of all the data in the org, how much can the code that runs on this person’s behalf actually pull back? That question is not settled by the login. It is settled inside your Apex, statement by statement, and it is entirely possible for the answer to be more than the person themselves could reach through the user interface.

A name on the request is not a boundary around the data. Identity tells you who asked. Only the code tells you what came back.

Why the code can out-reach the person who triggered it

Here is the mechanism, and it is not exotic. In Apex written below API version 67.0, a plain SOQL query runs in system mode by default. In system mode, field-level security and object CRUD permissions are simply not enforced by the Apex layer. The query runs as though the permission model were not there.

So a field the running user cannot see — hidden from their profile, stripped out of their page layouts, invisible in every report they can build — can still be read by code that same user triggered. Nothing throws. Nothing warns. The value arrives in your list of records like any other.

That default is not a mistake — audit stamps and integration code genuinely need it. It is simply a feature that has been on by default for most of Apex’s history, and most of the Apex in most orgs was written under it.

An agent action is Apex with a much wider mouth

None of this is new. What is new is where the output goes.

An Agentforce action is an Apex method with declared inputs and outputs. In a classic screen, an over-broad query is usually harmless in practice: you queried more than you needed, but the page only renders three fields, so the extra columns die quietly inside the transaction. The sloppiness is real and invisible.

An agent action has no such hiding place. Its outputs are the entire point — they are handed to a model, which will summarise them, paraphrase them, carry them into the next turn of the conversation and, if asked nicely, restate them. There is no rendering step where an unused field falls away. Whatever your method returns is material the model may use.

That is the whole shift. The same query, moved from a controller to an invocable action, changes from untidy to consequential.

The path I actually traced

I did not find my stray field by reading code over coffee. I found it with a static analyser I wrote for exactly this — a tool that reads Apex and agent metadata as data, without running anything.

It followed one field along a complete path: out of a SOQL column, into an Apex @InvocableVariable output, and from there into the prompt template. The field carried a compliance label. And the report did not say “this could reach the model” — it said the path existed, and gave me the line number: line 125.

Two things about that finding stayed with me. First, the path was static. It existed whether or not any real conversation had ever walked it, which means no amount of well-behaved testing would have proven its absence. Second, it was in my own code, written by someone who talks about permissions for a living. That is not a comfortable sentence to publish. It is also the reason I trust the method: it caught the author.

The label came from somewhere ordinary, by the way: Salesforce orgs already carry compliance classification in metadata — the ComplianceGroup value on a field, with entries like PII, GDPR or HIPAA. Somebody fills these in during a data-governance exercise and then, in my experience, nobody ever reads them back. They are worth reading back, because a label turns a vague finding into a sentence a business colleague can act on.

Two dials, and they are not the same dial

One more distinction, because it catches experienced developers too. Salesforce security answers two separate questions, and Apex has a separate control for each:

  • Sharing decides which records come back — org-wide defaults, role hierarchy, sharing rules. That is what a class-level declaration such as with sharing governs.
  • Field-level security and CRUD decide which fields and objects you may touch at all.

They are independent axes. with sharing on your action class is genuinely useful — and it does nothing whatsoever about a field the user was never allowed to read. If you set one dial and assumed the other followed, you are in good company, and you should go and look.

What to do on Monday

The fix is small, and it belongs on the statement, not in a comment:

  • WITH USER_MODE on the SOQL, so the query itself enforces the user’s object and field permissions.
  • AccessLevel.USER_MODE on the Database methods, for the same reason.

A third tool, Security.stripInaccessible, is worth knowing for how it differs: it removes inaccessible fields from records you have already read, rather than refusing the read. On a path that feeds a model, refusing is the better shape — a loud failure in a sandbox is a gift.

Your next step

Open one agent action in your own org this week. Write down every field in its SELECT. For each one, ask two plain questions: would the person triggering this be able to see this field in a report? and does this field leave the method as an output?

Any field where the answers are “no” and “yes” is worth an hour of your attention. You do not need a tool to find your first one. You need the list, and the willingness to read it honestly.

Mustafa Aksu

Salesforce developer & ISV builder focused on Revenue Cloud, Agentforce, and Data Cloud. I write from real, shipped work.