SfSkills Domains Docs GitHub

Home / admin / role-hierarchy-design

role-hierarchy-design

Use when designing or maintaining the role hierarchy as a record-access mechanism. Trigger keywords: role hierarchy, manager cannot see subordinate records, grant access using hierarchies, role reparenting, portal role, how deep should the role hierarchy be, design the role hierarchy. NOT for territory models - use admin/enterprise-territory-management. NOT for rule-based grants - use admin/sharing-rules. NOT for the overall model - use admin/sharing-and-visibility.

Open the skill folder on GitHub


Role Hierarchy Design

This skill activates when the role hierarchy is being designed, extended, restructured, or blamed for a record-access problem. It treats the hierarchy as one access mechanism among several — the one that grants access upward, along a single branch, and nowhere else.


Before Starting

Gather this context before touching a role:


Core Concepts

What the hierarchy actually grants

Users at any role level can view, edit, and report on all data that is owned by or shared with users below them in their role hierarchy. The Salesforce Security Guide documents exactly two exceptions to that sentence:

  1. For custom objects, Grant Access Using Hierarchies can be disabled on the Sharing Settings page. When disabled, only the record owner and users who are granted access have access to the custom object's records.
  2. After a folder is shared with a role, it is visible only to users in that role, not to superior roles in the hierarchy.

Everything else about the hierarchy follows from three mechanical facts:

FactConsequence for design
Hierarchy access is an inherited grant. Object Sharing tables "store the data that supports explicit and implicit grants"; group membership and inherited grants live in the Group Maintenance tables and are resolved by a join at query timeThere is no share row that says "manager", and the table split is the evidence for that. Do not argue it from AccountShare.RowCause: the current Object Reference lists "Valid values include" fourteen causes (Manual, Owner, Team, Rule, GuestRule, ImplicitParent, GuestParentImplicit, LpuParentImplicit, LpuImplicit, PortalImplicit, ARImplicit, Territory, Territory2AssociationManual, TerritoryManual) and still qualifies the list as open, so its contents cannot prove what is absent. The same page adds that rows for ImplicitParent / Manual / Owner "are compressed into one record with the highest level of access", and that for some mechanisms "sharing entries aren't stored at all". A share query cannot prove or disprove manager access.
Salesforce materialises up to three system-defined groups per role — Role, RoleAndSubordinates, and RoleAndSubordinatesInternal — "depending on if digital experiences is enabled". The availability note in that table sits on RoleAndSubordinates, not on the Internal variantEvery user above a role is stored as an indirect member of that role's group. Depth is not free: it is rows. Check which group types the org actually has before writing a query that assumes one.
Access flows up one branch onlyTwo peers in different branches never see each other through the hierarchy, no matter how close their boxes are on the org chart.

The practical test: if the requirement can be expressed as "everyone above X in exactly one chain", the hierarchy is the right tool. Anything else needs a different mechanism.

Grant Access Using Hierarchies

Two independent switches share this label. Confusing them is the most common configuration error in this domain.

SwitchWhereAPI surfaceEffect
Object-levelSetup → Sharing Settings → Organization-Wide Defaults → EditNot exposed on the CustomObject metadata type — only sharingModel and externalSharingModel areTurns off upward inheritance for that object entirely
Public-group-level (and, from API 67.0, queues)Setup → Public Groups → New/EditGroup.DoesIncludeBosses, which "corresponds to the Grant Access Using Hierarchies checkbox on the detail pages of public groups and queues" and "is only available for groups of type Regular and Queue"Controls whether records shared with that group also reach users above the group's members

Constraints on the object-level switch, verbatim from the Security Guide: it can be deselected only for custom objects, and "You can only deselect this setting for custom objects that don't have a default access of Controlled by Parent." Standard objects have no editable checkbox at all. Changes to it are recorded in the Setup Audit Trail alongside public groups, sharing rules, and org-wide sharing.

The switch also governs how sharing rules propagate: users in the role hierarchy are automatically granted the same access that users below them get from a sharing rule, provided the object is a standard object, or Grant Access Using Hierarchies is selected if the object is custom. Turning it off on a custom object therefore silently narrows every sharing rule on that object too.

For the public-group switch, the Security Guide gives one concrete tuning instruction: deselect Grant Access Using Hierarchies when creating a public group with All Internal Users as members, which optimizes performance for sharing records with groups.

The three access levels stored on the role

A role carries its own access settings for the child records of accounts that its users own — and that access applies regardless of who owns the child records.

Metadata fieldUserRole fieldValid valuesUnavailable when
caseAccessLevelCaseAccessForAccountOwnerRead, Edit, NoneCase sharing model is Public Read/Write
contactAccessLevelContactAccessForAccountOwnerRead, Edit, NoneContact sharing model is Public Read/Write or Controlled by Parent
opportunityAccessLevelOpportunityAccessForAccountOwnerRead, Edit, NoneOpportunity sharing model is Public Read/Write

OpportunityAccessForAccountOwner is a required field on UserRole, and the Object Reference adds that "you can't set a user role with an opportunity access less than that specified in organization-wide defaults." When an org tightens Opportunity OWD from Public Read/Write to Private, these settings appear on the role edit page for the first time carrying whatever the platform supplied. Do not assume that fallback is "organization settings": the RoleOrTerritory metadata reference says of each of the three fields, "If no value is set for this field, this field value uses the default access level that is specified in the Manage Territory page in Setup." Whatever it resolves to, it is not a value your repository chose — which is why an OWD change frequently produces a wave of "the account team lost the opportunity" tickets that has nothing to do with the hierarchy shape.

A deployable role, with the child-record access made explicit rather than defaulted:

<!-- force-app/main/default/roles/EMEA_Sales_Manager.role-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Role xmlns="http://soap.sforce.com/2006/04/metadata">
    <name>EMEA Sales Manager</name>
    <description>Field sales managers, EMEA. Inherits rep-owned records one level down.</description>
    <parentRole>EMEA_Sales_VP</parentRole>
    <caseAccessLevel>Read</caseAccessLevel>
    <contactAccessLevel>Edit</contactAccessLevel>
    <opportunityAccessLevel>Read</opportunityAccessLevel>
    <mayForecastManagerShare>false</mayForecastManagerShare>
</Role>

The Role metadata type has been available since API version 24.0; files use the .role suffix and live in the roles directory. parentRole takes the parent's fullName, so a deployment that includes a child role but not its parent fails on an unresolved reference.

Depth, and what depth actually costs

None of the sources listed in references/well-architected.md documents a maximum number of levels. What is documented is a maximum number of roles (500 or 5,000, above) and a mechanism that makes depth expensive. Do not supply the missing number from memory — see references/llm-anti-patterns.md Anti-Pattern 1.

So the design rule is not a number, it is a shape: add a level only when a real access requirement needs an intermediate viewer. Levels that exist purely to mirror reporting lines add recalculation cost and grant nothing new, because a user two levels up already inherits everything a user one level up inherits.

Role changes are asynchronous, and portal roles move with the account

When groups, roles, and territories are edited, sharing rules are recalculated to add or remove access as needed, and "depending on the nature of your updates and your org's setup, these sharing calculations can take a while to complete." The Setup UI returns before that work is done. The documented monitoring surface is the Background Jobs page; a completion email is documented for org-wide default recalculation specifically, so plan the cutover around the Background Jobs page rather than around an email.

Moving a user between roles triggers, per the enterprise-scale guide: new indirect membership for everyone above the new role, share adds and removes when the two roles have different account-child access settings, group membership changes plus share reparenting for every portal-enabled account that user owns, and recalculation of every sharing rule that names the old or new role in its source group. Managers above the old role lose access to that user's data as part of normal inheritance, with no table updates required.


Common Patterns

Pattern: two branches with a deliberately thin root

When to use: Sales and Service must not inherit each other's records, but a small executive group must see both.

How it works:

  1. Create one top role (Executive) and assign only the users who genuinely need org-wide visibility. Everyone in it inherits everything below, so treat membership as a privilege review, not an org-chart exercise.
  2. Create Sales_VP and Service_VP as siblings under it. Neither inherits from the other.
  3. Hang functional roles under each VP only where a real "must see subordinate records" requirement exists.
  4. Set opportunityAccessLevel on the Service branch roles to None so that service users who own accounts do not pick up opportunity access on those accounts.
  5. Give cross-branch requirements to sharing rules, not to a shared parent role.

Why not one deep chain: Collapsing both functions into a single chain forces every Service manager to sit under, or above, Sales roles. Once that is built, there is no exit that is not structural. Opportunity is a standard object, so there is no Grant Access Using Hierarchies checkbox to switch off — and restriction rules are not a fallback either, because the Security Guide scopes them to "custom objects, external objects, contracts, events, quotes, tasks, time sheets, and time sheet entries". Opportunity is on none of those lists. The only remaining correction is to reshape the hierarchy, which is the most expensive operation in this domain.

Pattern: opting a sensitive custom object out of inheritance

When to use: A custom object such as Compensation_Plan__c must be visible to its owner and to named reviewers only, never to the owner's management chain.

How it works:

  1. Set the object's OWD to Private. Confirm it is not on the detail side of a master-detail relationship — a custom object on the detail side of a master-detail with a standard object has its OWD set to Controlled by Parent and it is not editable.
  2. In Setup → Sharing Settings → Organization-Wide Defaults → Edit, deselect Grant Access Using Hierarchies for that object.
  3. Grant the reviewers access with a criteria-based sharing rule targeting a public group, and deselect Grant Access Using Hierarchies on that group as well if the reviewers' own managers must stay out.
  4. Record the decision — the checkbox is not in the CustomObject metadata, so it does not travel with a deployment and it will not appear in a source diff.

Why not a restriction rule: Restriction rules filter what specified users can access; they are the right tool when only some users must be narrowed. When nobody should inherit upward, turning the inheritance off at the object is one switch instead of a rule per persona.

Pattern: parking a high-volume owner outside the hierarchy

When to use: An integration user, migration user, or "unassigned leads" bucket owns a very large share of an object's records.

How it works:

  1. Confirm the skew: a single user owning more than 10,000 records of an object is the documented threshold at which sharing computations become a problem.
  2. If that user does not need to share data through roles, leave them with no role at all.
  3. If a role is required, place them in a separate role at the top of the hierarchy — noting that this user then inherits access to all data owned by or shared with users below them — never move them out of it, and keep them out of public groups used as the source for sharing rules.

Why not just assign them to the obvious functional role: Moving a skewed owner into or out of a role, or into or out of a sharing-rule source group, forces Salesforce to adjust a very large number of sharing-table entries, which is what produces the long-running recalculations and the lock errors covered in references/gotchas.md.


Decision Guidance

SituationMechanismReason
Manager must see everything their reports ownRole hierarchyThis is the only thing the hierarchy does natively
Peers at the same level must see each other's recordsSharing rule with a public groupAccess flows up a branch, never sideways
A user must see records across several unrelated branchesSharing rule, or territories if the axis is account-basedRoles are single-parent; a user holds exactly one role
Coverage is account-based and multi-dimensional (geography × segment × product)Enterprise Territory Management — admin/enterprise-territory-managementTerritories allow many-to-many account-to-user coverage; roles cannot
A custom object must not roll up to managementOWD Private + deselect Grant Access Using HierarchiesThe only supported way to stop inheritance, and only for custom objects
A standard object must not roll up to managementReshape the hierarchy so the chain does not sit above the owner, or move the data to a custom objectThe Grant Access Using Hierarchies checkbox is not editable for standard objects, and restriction rules are not a fallback for most of them — they cover only custom objects, external objects, contracts, events, quotes, tasks, time sheets and time sheet entries
Records shared to a group must not reach the group members' managersDeselect Grant Access Using Hierarchies on the public group (Group.DoesIncludeBosses)Separate switch from the object-level one
Access rule cannot be expressed declaratively at allApex managed sharing — security/apex-managed-sharing-patterns__Share rows with a custom row cause
Object OWD is Public Read/WriteDo nothing to the hierarchyEveryone already has access; the hierarchy is inert
Need to grant object or field accessPermission sets, not rolesRoles never grant CRUD or FLS

  1. Build the OWD × requirement matrix. For each object in scope, record the internal OWD, the external OWD, and whether the requirement is vertical or horizontal. Drop every object whose OWD is Public Read/Write — the hierarchy cannot affect it.
  2. Draft the hierarchy from access requirements only. Add a level only where a named person must see records owned by a named group. Do not transcribe the org chart. Record, for each role, the reason it exists; a role with no access justification is a role to delete.
  3. Set the three account-child access levels explicitly on every role (caseAccessLevel, contactAccessLevel, opportunityAccessLevel). An unset field falls back to a platform default the metadata reference describes as "the default access level that is specified in the Manage Territory page in Setup" — a decision made by accident, and one that never shows up in a source diff.
  4. Decide Grant Access Using Hierarchies per custom object and write the decision down outside the metadata, since it is not part of the CustomObject source.
  5. Run the checker — python3 skills/admin/role-hierarchy-design/scripts/check_role_hierarchy_design.py --roles-dir force-app/main/default/roles — to catch cycles, unresolved parentRole references, invalid access-level values, and role counts above the org ceiling before a deploy fails.
  6. Sequence the rollout against recalculation. Load users into roles before creating sharing rules; add sharing rules one at a time and let each finish; schedule reparenting into a window where no other group maintenance, deployment, or Apex test run is in flight.
  7. Verify by impersonation, not by query. Log in as a user under each branch and confirm what they see. Hierarchy access produces no share rows, so a SOQL check against AccountShare or OpportunityShare will not show it.

Review Checklist


Salesforce-Specific Gotchas

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

  1. Manager access leaves no evidence. Inherited access is computed from group membership, so no AccountShare or OpportunityShare row explains it. Debugging by share query produces false "the manager has no access" conclusions.
  2. The standard-object opt-out does not exist. Grant Access Using Hierarchies is editable for custom objects only, and not even for those when the default access is Controlled by Parent.
  3. The role's Opportunity/Case/Contact settings are about accounts the role owns, not about records the role owns, and they vanish from the edit page when the child object's OWD is Public Read/Write.
  4. Reparenting is the blocking operation. It blocks almost all other group updates; expect "could not acquire lock" and "Group membership operation already in progress" during concurrent loads.
  5. Portal roles are invisible and immutable. They do not appear on the Roles setup page, they are appended below the portal account owner's role, and no field on a portal role can be updated.
  6. Ownership skew converts a role change into an outage. One user owning more than 10,000 records of an object is the documented threshold.

Output Artifacts

ArtifactDescription
Access requirement matrixObject × OWD × vertical/horizontal, the input that decides whether the hierarchy is relevant at all
Role hierarchy designNamed roles, parentage, and a one-line access justification per role
Role metadata filesDeployable .role-meta.xml with parentRole and the three account-child access levels set explicitly
Grant Access Using Hierarchies registerPer-custom-object decision, kept outside metadata because the setting is not in CustomObject source
Role-change runbookSequencing, maintenance window, lock-retry behaviour, and post-change verification steps
Checker outputCycles, unresolved parents, invalid access-level values, and role-count breaches from check_role_hierarchy_design.py

Package files