SfSkills Domains Docs GitHub

Home / admin / scoping-rules

scoping-rules

Use when setting the default filtered set of records a user sees in list views, reports and SOQL without removing their access. Trigger keywords: scoping rule, Filter by scope, default record scope, RestrictionRule enforcementType Scoping, Object Manager scoping rule, recordFilter. NOT for the SOQL USING SCOPE clause - use apex/soql-using-scope-clause. NOT for blocking record access - use admin/restriction-rules.

Open the skill folder on GitHub


Scoping Rules

This skill activates when the requirement is "these users should land on a smaller set of records by default" — a focus and productivity requirement, not an access requirement. A scoping rule narrows what a user sees first in list views, reports and SOQL; it removes nothing. The user can switch scope and reach every record their sharing model already gave them.

Two names collide around this feature and both cause real misroutes. The Setup feature is a scoping rule. The SOQL USING SCOPE clause is a query modifier — see apex/soql-using-scope-clause. They meet at exactly one point (USING SCOPE scopingRule) and are otherwise unrelated. The sibling feature that actually blocks access is a restriction rule — see admin/restriction-rules.


Before Starting

Gather this context before touching anything:


Core Concepts

There is no ScopingRule metadata type

This is the single most common authoring error. A scoping rule is a RestrictionRule with enforcementType set to Scoping. There is no separate metadata type, no separate Tooling API object, and no scopingRules/ folder.

<!-- force-app/main/default/restrictionRules/SR_Department_A_Contacts.rule-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<RestrictionRule xmlns="http://soap.sforce.com/2006/04/metadata">
    <active>true</active>
    <description>View contacts from Department A.</description>
    <enforcementType>Scoping</enforcementType>
    <masterLabel>SR for Department A contacts</masterLabel>
    <recordFilter>Department=$User.Department</recordFilter>
    <targetEntity>Contact</targetEntity>
    <userCriteria>$User.UserRoleId = '00Exxxxxxxxxxxx'</userCriteria>
    <version>1</version>
</RestrictionRule>
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
  <types>
    <members>*</members>
    <name>RestrictionRule</name>
  </types>
  <version>67.0</version>
</Package>

The file suffix is .rule and the directory is restrictionRules. RestrictionRule is available in Metadata API and Tooling API version 52.0 and later.

FieldRequiredNotes
enforcementTypeYesScoping for a scoping rule. The other documented values are Restrict (a restriction rule — a different feature) and FieldRestrict, which the reference marks "Don't use".
targetEntityYesThe object being scoped. Salesforce advises against editing this after creation; delete and recreate instead.
recordFilterYesThe record-side criterion. Supports boolean, date, dateTime, double, int, reference, string, time, and single picklist values.
userCriteriaYesThe user-side criterion. Determines who the rule applies to.
masterLabelYesRule label.
descriptionYesDocumented as required, unlike on most metadata types. Generators trim it for brevity; do not.
activeNoDefaults to false. Omitting it and then wondering why nothing filtered is one of the two standard "my rule does nothing" causes; the other is the list-view wiring below.
versionYesInteger rule version.

The equivalent Tooling API payload wraps the same fields under Metadata, with FullName alongside:

{
    "FullName":"Department A contact scoping rule",
    "Metadata": {
         "active":true,
         "description":"View contacts from Department A.",
         "enforcementType":"Scoping",
         "masterLabel":"SR for Department A",
         "recordFilter":"Department=$User.Department",
         "targetEntity":"Contact",
         "userCriteria":"$User.UserRoleId = '00Exxxxxxxxxxxx'",
         "version":1
    }
}

Scoping rule vs restriction rule

Both are RestrictionRule. They differ in enforcementType, in which objects they support, and — decisively — in how many surfaces they reach.

AspectScoping rule (enforcementType: Scoping)Restriction rule (enforcementType: Restrict)
Effect on accessNone. The user can still open and report on everything sharing gave themRemoves access to non-matching records
Surfaces reachedList Views, Reports, SOQL — three, per the official surface table"Links, List Views, Lookups, Records, Related Lists, Reports, Search, SOQL, SOSL"
Applied by defaultOnly in SOQL. List views and reports require Filter by scope to be selectedAlways, wherever the rule applies
User can turn it offYes — that is the pointNo
Supported objectsCustom objects plus Account, Case, Contact, Event, Lead, Opportunity, TaskCustom objects, external objects, Contract, Event, Quote, Task, TimeSheet, TimeSheetEntry
Operators in criteriaThe same single operator: "Unless you use SOQL, scoping rules support only the EQUALS operator. The AND and OR operators aren't supported." The SOQL operator is the only way past that, and it is API-only"Restriction rules support only the EQUALS operator. The AND, OR, or any other operators aren't supported"
Is it a security controlNoYes, with caveats — it is not applied to code running in system mode, and View All Records / View All Data override it

Note the object lists barely overlap: Account, Case, Contact, Lead and Opportunity can be scoped but not restricted; Contract, Quote, TimeSheet and external objects can be restricted but not scoped. Event and Task are the only standard objects on both lists.

Where the scope actually applies

Salesforce publishes a three-row surface table, and the rows are not the same shape:

FeatureBehaviour
List Views"Applied in Lightning Experience if Filter by scope is selected"
Reports"Applied in Lightning Experience if Filter by scope is selected"
SOQL"Applied, unless a scope other than scopingRule is specified"

Two consequences fall straight out of this and both are counterintuitive.

First, a correctly built, active scoping rule changes nothing a user can see until someone selects Filter by scope. The ListView metadata type's filterScope enumeration spells it out: the ScopingRule value means "Records that meet a scoping rule's record criteria. In Lightning Experience, scoping rules are applied to list views only if the user selects Filter by scope." You wire this up as configuration — "for list views and reports, you can apply the scope through Metadata API (using the filterScope field on the ListView type and the scope field on the Report type)".

Second, SOQL is the surface where the scope is on by default. USING SCOPE carries a scopingRule value — "Filter for records based on the applicable scoping rule. This option is available if an admin has activated at least one scoping rule on the object." Confirm support for a given object by reading supportedScopes from describeSObject() (SOAP) or the sObject Describe REST endpoint. That default-on behaviour is why the rule's own SOQL operator has to opt out, which is the next concept.

Search and SOSL are absent from the table. Restriction rules name Search and SOSL explicitly; the scoping table does not. Do not promise a stakeholder that global search will be scoped.

Criteria syntax: recordFilter and userCriteria

The criteria language is much narrower than it looks. Outside the SOQL operator there is exactly one operator: "Unless you use SOQL, scoping rules support only the EQUALS operator. The AND and OR operators aren't supported." A two-condition requirement — region and open status — has no declarative form; fold the second condition into a formula field the criterion can equal, or move to the SOQL operator and the API. The one multi-value affordance is the comma: "Comma-separated ID or string values are supported in the Record Criteria field." Null and blank values are out too — "including a null or blank value in record criteria isn't supported and can result in unexpected behavior".

Simple comparison form, buildable in Object Manager:

recordFilter:  Department=$User.Department
userCriteria:  $User.UserRoleId = '00Exxxxxxxxxxxx'

Multiple values are comma-separated on the right-hand side, and a double-quoted segment protects a literal comma inside a value:

recordFilter:  Name__c='Tom, Anita, "Torres, Jia"'
recordFilter:  Agent__c.Owner:User.ManagerId=001xx000003HNy7, 001xx000003HNut
userCriteria:  $User.IsActive=true

Note Owner:User — an owner reference must carry its type, because OwnerId is polymorphic. Dot-notation lookups are limited to one level.

SOQL operator form, for when the relationship needs a junction hop. This is API-only — Object Manager will not build it:

SOQL(Id, SELECT AccountId FROM BranchUnitCustomer USING SCOPE EVERYTHING WHERE BranchUnitId IN(SELECT CurrentBranchId From Banker WHERE UserOrContactId = $User.Id))

The rules that govern that string:

ConstraintStatement
Left operand"The left operand must query a single ID (primary key) or reference (foreign key) field" from the target entity
Scope clause"The SELECT statement, including nested SELECT statements, must include USING SCOPE EVERYTHING"
Valid scope"USING SCOPE EVERYTHING is the only valid scope clause syntax for scoping rules"
User variable"The SOQL operator doesn't support $User syntax except for $User.Id"
Object identity"In SOQL operators, the SOQL query object and the scoping rule target entity can't be the same object"
Objects barred from the subquery"These objects aren't supported in the SOQL operator": ActivityHistory, Attachments, Event, EventAttendee, Note, OpenActivity, tag objects, Task. Event and Task are scopeable target entities and still cannot appear inside the operator

USING SCOPE EVERYTHING is mandatory in every nested SELECT for a reason that follows from the surface table: SOQL is scoped by default when an active scoping rule exists, so a subquery inside the rule's own criteria would be filtered by the rule it is defining. EVERYTHING breaks that recursion.


Common Patterns

Pattern: user-attribute scoping (no subquery)

When to use: the "relevant to me" value already lives on both the record and the User record — department, division, region, branch code, a custom Territory__c text field.

How it works:

  1. Confirm the field exists on the target object and the matching field exists on User. Both must be one of the supported data types (boolean, date, dateTime, double, int, reference, string, time, or single picklist value).
  2. In Setup, open Object Manager → the object → Scoping Rules → New. Set the record criteria to Field = $User.Field.
  3. Set user criteria narrowly enough that no user is matched by any other scoping or restriction rule on that object.
  4. Save the rule inactive, deploy, then activate.
  5. Update the relevant list views to filterScope ScopingRule and the relevant reports' scope so users land on the scoped view rather than having to find the toggle.

Why not a sharing change: an OWD tightening or a restriction rule would take the records away. The stakeholder asked for a cleaner default view, not for a wall. Removing access to solve a clutter problem is the error this whole feature exists to avoid.

Pattern: junction-hop scoping (SOQL operator)

When to use: the relationship is indirect — a banker is assigned to a branch, the branch is linked to accounts through BranchUnitCustomer; a specialist is assigned to a product line, the product line reaches Opportunity through a junction.

How it works:

  1. Write the subquery standalone first and run it in an API client as the target user. If it is slow standalone, it will be slow as a rule.
  2. Wrap it: SOQL(<target ID or reference field>, <SELECT … USING SCOPE EVERYTHING …>).
  3. Add USING SCOPE EVERYTHING to every nested SELECT, not only the outer one.
  4. Deploy through Tooling API or Metadata API — Object Manager cannot express this.
  5. Test with a user who matches userCriteria and a user who does not, and confirm the second user's view is unchanged.

Why not build it in Setup: the Object Manager editor produces comparison criteria only. Attempting to paste a SOQL(...) string into the Setup UI is not a supported path.

Pattern: user-switchable scope via the utility bar

When to use: the same user legitimately works in more than one scope — a banker covering two branches, a rep covering two territories — and needs to move between them without an admin.

How it works: Salesforce's documented approach is a Flow surfaced in the Lightning Utility Bar: "You can set up a flow that your users access using the Lightning Utility Bar to set the scope of records that the user sees in list views, reports, and other features." The Flow writes the user-side value (for example a Current_Branch__c field on User) that recordFilter compares against; the rule itself never changes.

Why not one rule per branch: the per-object active-rule cap is low (two in Developer editions, five in Performance and Unlimited editions), and a user matched by two rules on the same object violates the one-rule-per-object-per-user constraint. One parameterised rule plus a mutable user attribute scales; N rules does not.


Decision Guidance

SituationRecommended ApproachReason
"Reps shouldn't have to scroll past other regions' accounts"Scoping rule on AccountFocus requirement — access is not in question
"Reps must not be able to see other regions' accounts"admin/restriction-rules or tighten OWD/sharingScoping rules remove nothing; a user can switch scope
Auditor or regulator asks for enforced record segregationSharing model, then admin/restriction-rulesA control the user can switch off is not a control
Need to filter one Apex query to a subsetapex/soql-using-scope-clauseA query modifier, not a Setup feature — no rule needed
Object is Order, Campaign, Asset, Quote, or ContractNot a scoping ruleOutside the supported object list
Criterion is Contact.Department = User.DepartmentObject Manager scoping ruleSimple comparison; no API needed
Criterion needs a junction object hopTooling or Metadata API with the SOQL operatorThe SOQL operator is API-only
More than five scopes needed on one objectOne parameterised rule + a mutable User fieldThe per-object active-rule cap is edition-bound
The record set is enormous and the criteria are expensiveTest in a full sandbox first"Salesforce reserves the right to disable a scoping rule if a rule you create is inefficient or if your data model has so much data that scoping rules cause slowness when applied"
Rule is built and active but users see no changeCheck Filter by scope on the list view / reportList views and reports are opt-in; SOQL is not

  1. Classify the requirement. Ask whether the user is allowed to see the excluded records if they go looking. "Yes, we just want a cleaner default" → continue here. "No" → stop and route to admin/restriction-rules, and say plainly that a scoping rule would not satisfy the requirement.
  2. Verify feasibility. Confirm the target object is one of the seven supported standard objects or a custom object, confirm the org is Performance, Unlimited, or Developer edition on Lightning Experience, and confirm the acting identity holds Manage Sharing.
  3. Design the two criteria. Write recordFilter and userCriteria as literal strings. Decide at this point whether a single EQUALS comparison suffices or the SOQL operator is required — AND and OR do not exist outside the SOQL operator — because that decision also determines whether the rule can be built in Object Manager at all. Check every referenced field against the supported data-type list and the one-level dot-notation limit.
  4. Prove no user is double-matched. Enumerate the other active scoping and restriction rules on the same object and confirm their userCriteria are disjoint from yours. One scoping or restriction rule per object per user is the platform's stated constraint, and overlap produces unpredictable behaviour rather than an error.
  5. Build inactive, deploy, then activate. Author the .rule file under restrictionRules/ with active set to false, deploy it, remap any hardcoded role, profile, record type or user IDs for the destination org, then activate. Deploying active skips the remap window.
  6. Wire the surfaces. Set filterScope to ScopingRule on the list views that should default to the scope, set scope on the reports, and confirm with an affected user. Without this the rule is live and invisible.
  7. Run the checker and review the gotchas. Run python3 skills/admin/scoping-rules/scripts/check_scoping_rules.py --manifest-dir <metadata-dir>, then read references/gotchas.md before sign-off — the deactivation ordering trap and the org-ID portability trap both bite after deployment, not during it.

Review Checklist


Salesforce-Specific Gotchas

Short form; the mechanism and the recovery for each are in references/gotchas.md.

  1. A live rule that filters nothing — list views and reports only honour a scoping rule when Filter by scope is selected, so a correctly built, active rule is invisible until the views are wired up.
  2. Disabling the rule strands its dependents — "To disable a scoping rule, first delete the list views and reports that have Filter by scope selected. After a scoping rule is disabled, the list views and reports aren't functional nor modifiable." Unwind the surfaces first; there is no repair afterwards.
  3. Hardcoded org IDs do not survive promotion — role, profile and record type IDs embedded in userCriteria or recordFilter differ per org and must be remapped on every hop.
  4. Salesforce can switch your rule off — inefficient criteria or an oversized data model give Salesforce the documented right to disable the rule, and nothing in Setup announces it.
  5. targetEntity is effectively immutable — Salesforce advises deleting and recreating rather than editing it, which means a new rule name and a fresh pass over every list view that referenced the old one.
  6. Scoping quietly narrows duplicate detection — potential duplicates are limited by scope even when the duplicate rule has Bypass sharing rules turned on.
  7. One operator, no AND, no OR — outside the SOQL operator the criteria language supports only EQUALS, so a two-condition requirement needs a formula field or the API path.
  8. The scope stops at the target object — a rule on one object doesn't affect child objects, related lists stay unscoped apart from the contact role related list, and Event and Task cannot be queried inside a SOQL operator even though both are scopeable target entities.

Output Artifacts

ArtifactDescription
.rule metadata fileRestrictionRule with enforcementType: Scoping, ready to deploy under restrictionRules/
package.xml fragment<name>RestrictionRule</name> types block for retrieve and deploy
Tooling API payloadJSON body with FullName and a Metadata block, for orgs where a Setup-free path is preferred
Surface wiring listThe list views (filterScope = ScopingRule) and reports (scope) that must change for users to see the effect
ID remap tableEvery org-specific ID in the criteria, with its value per environment
Classification noteThe written focus-vs-access determination, including the redirect when the answer was access

Package files