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 ships no error

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

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. The failure only shows up for the one audience you can't easily impersonate: the anonymous public visitor. We hit exactly this building an earlier, Flow-based version of AutoSurvey's guest survey delivery. The flow rendered fine for us and refused to start for guests.

Editorial note (added July 17, 2026): this is how we solved the problem back when AutoSurvey delivered guest surveys through a generated Screen Flow. That Flow-based guest route was retired, and AutoSurvey's guest survey experience today runs on the LWC FormRunner — no Flow interview is created for a guest, so this exact access-denial scenario no longer arises in our own product. The checklist and security architecture below are still accurate and still the right way to build a guest-facing Flow of your own, including one that calls AutoSurvey's current Flow-native Start/Submit Survey invocable actions.

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 step reads backwards, so stay with me. 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>

The setting says restrict, and you're switching it on to grant access. The label describes the old world, not what the toggle does today. Flipping it on moves the flow out of the legacy "anyone with Run Flows" model and into the granular one, where the per-flow grant in step 2 carries real weight. Leave it off and there's no grant to give. Turn it on and the flow starts honoring whatever access you assign.

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 straight 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 made possible. It's deliberately narrow: 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, then re-applies the running user's permissions before handing data back. In Apex that's Security.stripInaccessible(). It's exactly what you want: a guest only ever sees fields they're authorized to see. The catch is that a query which succeeds in system mode comes back stripped to nothing when the guest has no read access to those objects and fields. Your security layer is doing its job, hiding data the guest isn't allowed to read, and an empty screen is what that looks like.

So grant the guest read on every object and field the flow surfaces, and nothing beyond that. 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>

Look at what's switched off: no edit, no delete, and above all no viewAllRecords or modifyAllRecords. The guest can create a submission and read back the question definitions. That's the entire footprint. Don't forget field-level read on the specific fields your screens bind to, since FLS gets stripped the same way object access does.

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.

Public, without handing over the org

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

  • Grant the narrowest thing that works. The guest creates exactly one kind of record (its own submission and answers) and reads only the definition it needs to render. Nothing can be edited, deleted, or viewed across other records.
  • Token-gate every record. After the flow starts, every subsequent call is authorized by an opaque session token plus a 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. The runtime creates records in system mode, then returns data through stripInaccessible so the guest only ever sees what their minimal permissions allow.
  • Fail uniformly. A bad token, an expired one, a wrong secret: all of them return the same generic error, so the endpoint can't be used as an oracle.

That combination is what we took through security review: a fully public endpoint whose only real power is to create one submission record.

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 actually writes.
  4. Site has Public Access enabled in Experience Builder.

This was one of a handful of under-documented problems we hit building AutoSurvey, an AI survey app that runs entirely inside Salesforce, where every response stays a record in your own org. The worst part was that nothing logged an error: I lost most of a day before I realized the setting labeled "restrict" was the one I needed to switch on. If you build guest-facing experiences on the platform, I'd 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.