Experience Cloud Flows Guest user Security

How to run a Salesforce Flow as an Experience Cloud guest user (the safe way)

June 25, 2026 · ~7 min read

If you've tried to put a Screen Flow in front of unauthenticated visitors on an Experience Cloud site lately, you've probably hit a wall that didn't used to be there. The flow works perfectly for logged-in users and dies for guests with a vague access error. Here's why that happens — and the complete, least-privilege fix.

The regression that hides in plain sight

In Spring '23, Salesforce removed the blanket "Run Flows" permission from guest user profiles and replaced it with a granular, per-flow access model. There was no error in your code and nothing to migrate — flows that guests could run yesterday simply started failing.

The symptom is a denial at flow interview creation:

"You do not have the level of access necessary to perform the operation you requested."

What makes this nasty is that authenticated users are unaffected. Your own testing looks green, your QA looks green, and the failure only appears for the one audience you can't easily impersonate — the anonymous public visitor. We hit exactly this building AutoSurvey's guest survey runtime; the flow rendered for us and refused to start for guests.

TL;DR — the four parts

  1. Set isAdditionalPermissionRequiredToRun = true on the flow.
  2. Grant the guest profile/permission set explicit flow access to that flow.
  3. Give the guest least-privilege object + field read on every object the flow touches.
  4. Turn on Public Access for the site in Experience Builder.

Miss any one of the four and the flow still fails. They are AND, not OR.

1. Flip the flow into the granular access model

This is the counterintuitive one. The flow setting you need is "Override default behavior and restrict access to enabled profiles or permission sets." In metadata it's a single element:

<Flow>
    ...
    <interviewLabel>My Survey {!$Flow.CurrentDateTime}</interviewLabel>
    <isAdditionalPermissionRequiredToRun>true</isAdditionalPermissionRequiredToRun>
    <label>My Survey</label>
    ...
</Flow>

Read that setting name again: it says restrict access. So why turn it on to grant guest access? Because it's what switches the flow out of the old "anyone with Run Flows" model and into the new granular one — the model where an explicit per-flow grant (step 2) actually means something. With it off, there is no granular grant to give; with it on, the flow honors the access you assign. Turning the "restrict" toggle on is precisely what unlocks the door.

Managed-package note: for a packaged flow, only the package provider can set this element — a subscriber admin can't toggle it after install. If you're an ISV, it has to be baked into the flow you ship. (We emit it directly from our flow serializer.)

2. Grant the guest explicit flow access

Now that the flow is in the granular model, give the site's guest user access to it. You can do this through the guest user profile (Experience Builder → Settings → General → the guest profile → Flow Access) or via a permission set, granting access to each flow the guest needs to run.

This is the per-flow grant that step 1 enabled. It's deliberately explicit: the guest can run this flow, not every flow in the org.

3. Give the guest least-privilege data access — and watch for stripInaccessible

The flow now starts, but screens come up empty or the interview errors mid-run. This is the subtlest part, and it's worth understanding rather than brute-forcing.

A well-built runtime queries in system mode and then re-applies the running user's permissions before returning data — in Apex, Security.stripInaccessible(). That's the right thing to do: it means a guest only ever sees fields they're authorized to see. But it also means a query that succeeds in system mode comes back stripped to nothing if the guest has no read access to those objects and fields. The query isn't failing — your security layer is correctly hiding data the guest can't read.

So grant the guest read on every object and field the flow surfaces — and nothing more. Here's the shape we use: create + read on the two objects the guest legitimately writes (the submission and its answers), and read-only on the definition objects the runtime needs to render:

<!-- The guest writes its own submission + answers -->
<objectPermissions>
    <object>Survey_Submission__c</object>
    <allowRead>true</allowRead>
    <allowCreate>true</allowCreate>
    <allowEdit>false</allowEdit>
    <allowDelete>false</allowDelete>
    <viewAllRecords>false</viewAllRecords>
    <modifyAllRecords>false</modifyAllRecords>
</objectPermissions>

<!-- The guest only reads the survey definition -->
<objectPermissions>
    <object>Form_Question__c</object>
    <allowRead>true</allowRead>
    <allowCreate>false</allowCreate>
    <allowEdit>false</allowEdit>
    <allowDelete>false</allowDelete>
    <viewAllRecords>false</viewAllRecords>
    <modifyAllRecords>false</modifyAllRecords>
</objectPermissions>

Note what's off: no edit, no delete, and crucially no viewAllRecords or modifyAllRecords. The guest can create a submission and read back the question definitions, and that's the whole of it. Don't forget field-level read on the specific fields your screens bind to — FLS is stripped the same way object access is.

4. Turn on Public Access for the site

The last gate is the one people forget because it lives in a different place than everything else. In Experience Builder → Settings → General, enable "Public can access the site." Without it, none of the above matters — the guest never gets far enough to be denied by a flow.

This is the Experience Builder setting, not the Network object field in the Metadata API. If you're scripting site config, make sure you're flipping the one Builder actually reads.

Doing it without opening the barn door

"Let the public run a flow that writes records" sounds alarming, and done carelessly it should. The point of the granular model is that you can be precise. A few principles that kept our guest endpoint tight enough to take into AppExchange security review:

  • Create where you must, read where you can, nothing else. The guest creates exactly one kind of record (its own submission and answers) and reads only the definition it needs to render. No edit, no delete, no view-all.
  • Token-gate every record. After the flow starts, every subsequent call is authorized by an opaque session token + resume secret — never by a Salesforce record Id in the URL. A guest can't enumerate or reopen someone else's submission.
  • Keep writes in system mode, reads under the user. Let the runtime create records in system mode, but return data through stripInaccessible so the guest only ever sees what their (minimal) permissions allow.
  • Fail uniformly. Bad token, expired token, wrong secret — all return the same generic error, so the endpoint isn't an oracle.

Granular access plus a token-gated runtime is what lets a fully public survey collect responses without becoming an open door into the org.

The whole checklist

When a guest can't run your Experience Cloud flow, walk these in order:

  1. Flow has isAdditionalPermissionRequiredToRun = true.
  2. Guest profile/permission set has explicit flow access to that flow.
  3. Guest has object + field read on everything the flow renders (remember stripInaccessible), and create only where it genuinely writes.
  4. Site has Public Access enabled in Experience Builder.

This is one of a handful of under-documented Salesforce problems we solved building AutoSurvey — an AI-powered survey app that's 100% native to Salesforce, where every response stays as a record in your own org. If you build guest-facing experiences on the platform, I'd genuinely love to compare notes.

I'm Erik, the founder. Say hello at erik@mindsharestudio.com, grab 30 minutes on my calendar, or take a look at autosurvey.app.