SfSkills Domains Docs GitHub

Home / flow / flow-runtime-context-and-sharing

flow-runtime-context-and-sharing

Flow runtime sharing context — run in user vs system mode, record access in Flow. Triggers: Flow sharing, run in system mode, Flow user context. NOT for Apex sharing declarations — use apex/apex-with-without-sharing-decision.

Open the skill folder on GitHub


Flow Runtime Context And Sharing

Activate when designing, reviewing, or auditing a Flow whose run mode determines whether it sees records the running user can't, edits fields the running user can't, or trusts merge fields that resolve from the running user's context. This is the canonical Security skill for Flow — every record-triggered, screen, scheduled, or auto-launched flow that touches sensitive data must justify its run mode against this skill before activation.


Before Starting

Gather this context before working on anything in this domain:

The most common wrong assumption: "the flow runs as the user who triggered it, so sharing is enforced." It does not. Since Spring '21, record-triggered flows default to System Context Without Sharing, which bypasses sharing rules even when the triggering user couldn't see the record themselves.

Limits and platform constraints:


Core Concepts

There are three central concepts a practitioner must understand: the three run modes themselves, the per-element override, and the Spring '21 default change that silently rewrote the security model for record-triggered flows.

Concept 1 — The Three Run Modes

Salesforce Flow runs at one of three security boundaries, set on the flow's overall properties:

Run ModeSharing RulesFLSCRUDValidation RulesWho is "Running User"?
System Context Without SharingBypassedBypassedBypassedFiredAutomated Process or invoking user (depends on context)
System Context With SharingEnforcedBypassedBypassedFiredSame as above
User ContextEnforcedEnforcedEnforcedFiredThe interactive user

Key points:

Defaults by flow type:

The principle of least privilege says: start at User Context and escalate only with justification. The Salesforce default does the opposite — it starts at without-sharing and forces you to opt in to safety. This is why every record-triggered flow needs an explicit run-mode review.

Concept 2 — Per-Element Override (runInMode)

The Flow XML supports a runInMode attribute on individual Get Records, Create Records, Update Records, and Delete Records elements. Valid values:

There is no per-element "User Context" override — you can only escalate from User Context, not de-escalate from System Context. This asymmetry is intentional: it lets a User-Context Screen Flow read a single lookup the user can't see (e.g., the assigned Queue's owner) without escalating the entire flow.

Use the per-element override sparingly and document each one. An audit script should flag every element with runInMode != DefaultMode for human review.

In Flow Builder, the setting appears on Get Records as Filter Records → How to Find Records and on DML elements as Run in System Context (without sharing) / Run in System Context (with sharing) toggles. The XML representation is what audit scripts and CI lint should target.

Concept 3 — The Spring '21 Default Change

Before Spring '21 (API 51.0):

In Spring '21 (API 52.0):

Implications for audit:

Concept 4 — $User, $Profile, $Permission Merge Fields

These global merge fields always resolve against the running user, but the definition of "running user" depends on the flow context:

Flow contextRunning user resolved as
Screen FlowThe interactive session user
Record-triggered after-save (System Context)The user whose DML triggered the flow
Scheduled flowAutomated Process user (NOT the flow creator)
Platform-event-triggered flowAutomated Process user (or the user defined on the PE subscriber)
Auto-launched from ApexThe Apex caller's running user (subject to System.runAs in tests)
Auto-launched from REST APIThe API caller
Orchestration stage assigned to a queueThe user who claims the work item

A common bug: a scheduled flow checks $Permission.Edit_Sensitive_Data to decide a branch — but Automated Process user has no permission set assignments, so the check always returns false. The flow silently takes the wrong branch.

Do not use $Permission checks as a substitute for an explicit run-mode decision. Run mode controls what the flow can do at the platform level; $Permission is just a feature toggle that the flow logic happens to read.


Common Patterns

Pattern 1 — User Context UI + System Context Subflow

When to use: A Screen Flow needs to update a record the user can't see (e.g., assigning a Case to a queue the user is not a member of, or updating a parent Account's last-touched date when the user has read-only on the parent).

How it works:

[Screen Flow — User Context]                  [Auto-launched Subflow — System Context Without Sharing]
   ┌────────────────────┐                          ┌────────────────────────────┐
   │ Capture user input │ ── invoke subflow ──>    │ Update parent Account      │
   │ (in user's context)│                          │ Insert audit log record    │
   └────────────────────┘                          │ Return success/failure flag│
              │                                    └────────────────────────────┘
              ▼
   ┌────────────────────┐
   │ Show confirmation  │
   │ to user            │
   └────────────────────┘

The Screen Flow stays in User Context — the user only sees fields they're entitled to. The subflow is configured as System Context Without Sharing and does the privileged write. Pass only the minimum fields needed (record Id + the new value) — never let the subflow re-read user-visible data and round-trip it back to the UI.

Why not the alternative: Setting the entire Screen Flow to System Context Without Sharing exposes every Get Records and Display Field to bypass FLS — meaning a screen that should show only the user's allowed fields suddenly shows everything. The bug surfaces months later when an auditor asks why an intern saw the CEO's compensation.

Pattern 2 — Without Sharing for Bulk Reparenting

When to use: A scheduled or batch-replacement flow needs to reassign ownership of Cases from a departing rep to a queue, even when the running Automated Process user has no specific share grants.

How it works:

Why not the alternative: Trying to enforce sharing on a bulk reparenting flow forces the Automated Process user to be granted explicit shares to every record — operationally infeasible. The right answer is to escalate run mode AND add compensating audit controls.

Pattern 3 — FLS-Bypass Audit

When to use: Periodic security review of all active flows; triage list for agents/flow-analyzer/AGENT.md audit-mode runs.

How it works:

# Pull active Flow metadata from the org
sf project retrieve start --metadata Flow

# Inventory run-mode settings
grep -l "<runInMode>SystemModeWithoutSharing</runInMode>" force-app/main/default/flows/

# Inventory flows that depend on the Spring '21 default
for f in force-app/main/default/flows/*.flow-meta.xml; do
  api=$(grep -oP '(?<=<apiVersion>)[^<]+' "$f")
  mode=$(grep -oP '(?<=<runInMode>)[^<]+' "$f")
  if [[ -z "$mode" && $(echo "$api >= 52.0" | bc) -eq 1 ]]; then
    echo "IMPLICIT WITHOUT-SHARING: $f (api=$api)"
  fi
done

Every flow returned should be classified:

Why not the alternative: Trusting flow authors to remember the security implications of the default does not scale. A repeatable audit script is the only defensible posture under SOX / HIPAA / FedRAMP.


Decision Guidance

Flow Type + Use CaseRecommended Run ModeReason
Screen Flow used by sales reps to update their own OpportunitiesUser ContextFLS + sharing must hold; user can only see their own data anyway.
Screen Flow that needs to read 1 lookup the user can't see (queue owner)User Context + per-element SystemModeWithSharing on that one Get RecordsMinimum escalation; document why.
Screen Flow on Experience Cloud (guest or community user)User Context (mandatory for guest); never escalate without security reviewGuest user can leak data org-wide if escalated.
Record-triggered after-save flow updating a related child recordSystem Context With SharingAllows automated work without bypassing sharing rules; safer than the without-sharing default.
Record-triggered after-save flow reparenting / bulk-fixing dataSystem Context Without SharingRequired when the triggering user can't see all related records; audit log mandatory.
Record-triggered before-save flowSystem Context (forced — setting is ignored)Platform constraint; document for clarity.
Scheduled flow doing nightly maintenanceSystem Context Without SharingAutomated Process user has no shares; escalation required.
Platform-event-triggered flow consuming integration eventsSystem Context Without SharingSame rationale as scheduled.
Auto-launched flow invoked from ApexInherit Apex caller's mode OR explicit System Context With SharingMake the boundary explicit; avoid surprises when Apex changes from with sharing to without sharing.
Orchestration stage assigned to a human queueUser ContextStage runs as the queue claimant; standard user permissions apply.
Approval-process replacement (Orchestrator) using $Permission for routingUser Context$Permission only resolves correctly for human users.

  1. Identify the flow's trigger and default mode. Record-triggered after-save? Screen? Scheduled? Look up the default from the table in Concept 1. Note the API version — anything below 52.0 has different defaults.
  2. Classify data sensitivity. What objects does the flow read and write? Are any classified HR, Comp, Health, Financial, or Personal? Sensitive data raises the bar — User Context is the default unless escalation is justified in writing.
  3. Map the running user. Who is the running user under each invocation path? For scheduled and PE-triggered flows, this is Automated Process — flag any $User/$Permission references that won't resolve correctly.
  4. Choose the overall run mode. Use the Decision Guidance table. Default to User Context for screen flows; default to System Context With Sharing for record-triggered flows that don't need to bypass sharing; only choose Without Sharing with a justification block in the flow description.
  5. Identify per-element overrides. If the flow is User Context but needs to read one specific record the user can't see, add runInMode=SystemModeWithSharing (or Without Sharing if necessary) on that single Get Records and document why.
  6. Add an audit-log step for any without-sharing path. Every System Context Without Sharing write should record source flow + record Id + running user to an audit table. This is the compensating control auditors require.
  7. Test with a low-privileged persona, then pin the API version. Run the flow as a user with the minimum profile — confirm User Context paths fail closed (record not found / insufficient access) and System Context paths succeed for the documented reason. Then set <apiVersion> explicitly in the flow XML so a later "Save" in Flow Builder does not silently flip the default.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. Before-save record-triggered flows ignore the run mode setting. They always run in System Context as part of the triggering DML transaction. Configuring User Context on a before-save flow is silently ignored — leading to a false sense of security in code review.
  2. $User returns the triggering user, not the flow author or the Automated Process user, for after-save record-triggered flows. A flow that emails $User.Email to confirm a record change will email the user who saved the record — not the system or the original record owner. This often surfaces during cross-tenant data migrations where the running user is a sysadmin doing bulk loads.
  3. System Context Without Sharing inherits to subflows unless the subflow explicitly overrides. Calling a subflow from a without-sharing parent does not "reset" the security boundary. To escalate-then-de-escalate, the subflow must set its own runInMode to SystemModeWithSharing or be a Screen subflow (which always asserts User Context for its screens — but its DML elements still inherit).
  4. FLS still enforces in some Action elements regardless of run mode. Send Email Alert respects FLS on the merge fields it pulls into the email body. Post to Chatter respects FLS. Apex actions enforce record access per the class's own with sharing declaration and field access per the class's query idiom — two separate controls, and the field-access one is gated by the apiVersion in the class's .cls-meta.xml (see references/gotchas.md Gotcha 4). Setting the flow to System Context Without Sharing does NOT bypass these.
  5. Saving a legacy flow in Flow Builder bumps apiVersion and silently changes default run mode. A flow created at API 50.0 (User Context default) opened and saved in current Flow Builder gets bumped to the latest API version (System Context Without Sharing default) if runInMode was never explicitly set. The flow now bypasses sharing — and no diff in source control will warn you, because no element changed.
  6. The Profile of the user activating the flow can affect what gets deployed. Activating a flow whose embedded Apex action class is private or whose referenced object is hidden by FLS for the activator can silently strip metadata in some legacy edge cases. Always activate flows as a sysadmin with full data + metadata access.
  7. Orchestration stage assigned to a queue resolves $User to the claimant, but only after claim. Before the work item is claimed, $User references in the assigned stage are unresolved — formula evaluation defers. Test with both pre-claim and post-claim states.
  8. Platform-event-triggered flows run as Automated Process for standard PEs, but as the PE-defined "Run As" user for high-volume PEs. The $User resolution differs. Check the PE's "Subscriber Run As User" setting before assuming Automated Process.

Output Artifacts

ArtifactDescription
Run-mode recommendationOne of: User Context / System With Sharing / System Without Sharing — with one-paragraph justification citing data sensitivity and least-privilege.
Per-element override listTable of Get Records / DML elements where runInMode != DefaultMode, with reason.
Audit findingsList of flows in scope whose current runInMode does not match the recommendation; classified as P0 (sensitive data + without-sharing default), P1 (justified but undocumented), P2 (defensible but should be tightened).
Test planPersona-based test cases: low-privileged user, peer user, sysadmin, Automated Process. Expected pass/fail for each.
Audit log schemaIf recommending System Context Without Sharing, the schema for the per-record audit table (source flow, record Id, running user, timestamp).


Official Sources Used

Package files