Skip to content

Groups and Hidden Territories as Targets

Module 4 of 6 in the chapter Programmatic Sharing: Apex and Flow Managed Sharing, chapter 11 of Securing Salesforce: Sharing and Visibility. Each module stands on its own.

The question this module answers: When code shares a record, who should the share point at, and what does that choice cost later?

Module 2 took the share row apart: which record, who, how much, and why. This module is about the “who.” It’s the field that decides how many rows your code writes, and how much of your logic you’ll have to keep right forever.

The “who” on a share row is the UserOrGroupId. Salesforce’s words: “The user or group IDs to which you’re granting access.” And a group can be “A public group or a sharing group associated with a role,” or “A territory group” (Sharing a Record Using Apex (opens in a new tab), Apex Developer Guide, Winter ‘27, checked 2026-11-07). One thing it can’t be: “You can’t grant access to unauthenticated guest users using Apex.”

Here’s why that matters. Share a record with a user, and that user gets access. Share it with a group, and Salesforce looks inside the group: the users in it, the groups in the group, the roles in the group. Anyone in the group gets the access, and so does anyone who joins it later.

That’s the real power. Point the share at a group, and every future member of that group gets it, without your code doing anything.

Say a record needs to be seen by three people. You can write three share records, one each. Or, if those three people have something in common, create a group that stands for what they have in common, put the three of them in it, and write one share record to the group.

Now a fourth person needs it. With individual shares, your code has to write another row. With the bucket, someone adds them to the group. Someone leaves: with individual shares, your code has to find the row and delete it. With the bucket, they come out of the group. If a hundred people need access, why create a hundred share records? I call it a bucket: one share to the bucket, then add and remove people from the bucket.

One share per person, or one per bucket One share per person, or one per bucket. One share per person: three people, three share rows; a new person means code writes a row; someone leaves and code finds and deletes their row; the share table grows with every person. One share to a bucket: one share row to a group with three people in it; a new person is added to the group, no code; someone leaves the group, no code; the share table grows with records, not people. Footnote: Is the access about them one by one, or about something they have in common? {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-SVA-PS-07", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison 0.5.0", "theme": "stevetecharc 1.4.0"} One share per person three people, three share rows A new person: code writes a row Someone leaves: code finds and deletes their row The share table grows with every person One share to a bucket one share row to a group of three A new person: added to the group, no code Someone leaves: out of the group, no code The share table grows with records, not people Is the access about them one by one, or about something they have in common? © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

So when you design a sharing reason, ask the question: am I giving this person access because of something about them individually, or because of a grouping they belong to? If it’s individual, write individual shares. If it’s something about them as a team, create a group that represents it.

Remember module 1’s student, who spent a week removing more than 100 million shares written by code? That’s what one share per person per record looks like at scale.

Groups aren’t free either. Salesforce’s own tips for public groups start with these two: “Create a group when at least a few users need the same access,” and “Create a group for members who don’t need to frequently move in or out of the groups” (Public Group Considerations (opens in a new tab), Salesforce Help, checked 2026-11-07). Every change to a group’s members makes Salesforce recalculate who sees what, so a bucket whose people change every hour isn’t saving you much.

And the page has two numbers to keep in mind:

  • Five levels of nesting. “Avoid creating groups within groups that result in more than five levels of nesting.”
  • Big groups. “If your group contains more than 10,000 members, you can experience performance issues or group members being deleted when updating membership on the group’s Edit or Create pages.” Manage those through the group’s access summary, user access policies or the GroupMember API.

My rule of thumb from class: if you have 10 or 20, or even 50 or 100 teams, create them as groups and drop people in. When you get into the thousands, it starts to get a little ridiculous. Don’t go crazy and get too granular, or you might as well go back to individual shares.

Roles as targets, and what changes when you add a portal

Section titled “Roles as targets, and what changes when you add a portal”

A role can be a target too: through a role’s sharing group, the share reaches everyone in the role, and if you choose, everyone below it. In Setup today that choice is called Roles and Internal Subordinates; Salesforce lists the older Roles and Subordinates as deprecated (Public Groups).

That old name hides a trap for code. The Apex guide warns: “After enabling digital experiences, records accessible to Roles and Subordinates via Apex managed sharing are automatically made accessible to Roles, Internal, and Portal Subordinates.” To keep external users out, change the code to share with the Role and Internal Subordinates group; it’s a large change, so Salesforce suggests batch Apex (Sharing a Record Using Apex).

Read that slowly. Turn on an Experience Cloud site, and code that used to share with a manager and their team can now reach portal users under that manager. No line of your code changed. If your code shares to role groups, check which group it queries for, before the site goes live.

Test knowledge. Code that shares to a role’s Roles and Subordinates group: after digital experiences are enabled, those records become accessible to Roles, Internal, and Portal Subordinates. Salesforce’s advice is to write the shares to the Roles and Internal Subordinates group instead. A likely area, not a known question.

A share row isn’t the only thing code can insert to give access. In class I put it this way: you can programmatically add a share to a person, add a share to a group, add a team member, or add territory access. And you can do these with both Apex and flows (module 6 covers flows).

They don’t behave the same:

  • A share to a person. A row in the share table. Nobody sees it on the record; you find it with a query. With the Manual reason, it’s deleted when the owner changes, unless the org keeps manual shares; with a custom reason, it’s kept (module 3).
  • A share to a group. The same row, pointed at a bucket. The same owner-change rules as above, because those rules are about the reason, not the target. The membership is managed in Setup, where people can see it.
  • A team member. Code inserts an AccountTeamMember (or an opportunity or case team member). The difference: people can see it. The team is on the record, and the users can see who’s on it and take them away later. On an owner change, account team members are deleted unless Keep account team is selected, and then they’re kept only if they were added by the owner, someone above the owner in the role hierarchy, or an admin (What Survives an Owner Change). So ask who your code runs as.
  • A territory. Code assigns the account to a territory, and the users to the same territory. It survives an owner change, because the assignment was never about the owner.
Four targets code can write Four targets code can write. A share to a person: code inserts a share row; seen only by a query; on an owner change, a Manual share is deleted unless the org keeps manual shares, a custom reason is kept. A share to a group: one share row to a public group or role group; members seen in Setup; the same owner-change rules, because they follow the reason. A team member: code inserts an account, opportunity or case team member; seen on the record and removable by users; on an owner change, kept only with Keep account team and depending on who added it. A territory: code assigns the account and the users to a territory; seen in the territory model; kept on an owner change. Footnote: the target decides how many rows you write and who can see them. The reason decides what survives an owner change. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-SVA-PS-08", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison 0.5.0", "theme": "stevetecharc 1.4.0"} A share to a person a share row Seen only by a query Owner change: Manual goes, unless manual shares are kept Owner change: a custom reason stays A share to a group one row to a public or role group Members seen in Setup Owner change: the same rules, because they follow the reason A team member account, opportunity or case team Seen on the record, users can remove it Owner change: kept only with Keep account team And only if the right person added it A territory the account and the users in one territory Seen in the territory model Owner change: kept The target decides how many rows you write and who can see them. The reason decides what survives an owner change. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

The fourth one is the trick I want to show you. I found it while I was looking for a way around module 3’s old problem: code shares on accounts that vanished when the owner changed.

You may have a full territory model: West, East, every state. Now create another branch beside it, a side branch, and call it something like Special. Give it its own territory type. Under it, create a territory. It isn’t part of your sales model. It’s a target bucket.

On that territory you decide the access, just like any territory: how much of the account its users get, and what that opens on the account’s opportunities and cases (chapter 9). Then your code does two things:

// One transaction: put the account in the bucket.
insert new ObjectTerritory2Association(
ObjectId = accountId,
Territory2Id = bucketTerritoryId,
AssociationCause = 'Territory2Manual');
// A separate transaction: put the user in the same bucket.
insert new UserTerritory2Association(
UserId = userId,
Territory2Id = bucketTerritoryId);

Keep them apart. UserTerritory2Association is one of the setup objects that “can’t be mixed with DML on non-setup sObjects in the same transaction,” because they “affect the user’s access to records in the org” (sObjects That Can’t Be Used Together in DML Operations (opens in a new tab), Apex Developer Guide, Winter ‘27, checked 2026-11-07). In my demo I ran them one at a time. In real code, the user’s half goes in its own transaction, such as a queueable job (module 5).

That’s it: a handful of lines. ObjectTerritory2Association “Represents an association (by assignment) between a territory and an object record such as an account or a lead,” and its AssociationCause is either Territory2AssignmentRule or Territory2Manual (ObjectTerritory2Association (opens in a new tab), Object Reference, Winter ‘27, API version 68.0, checked 2026-11-07). UserTerritory2Association is the same idea for a user (UserTerritory2Association (opens in a new tab), Object Reference, Winter ‘27, checked 2026-11-07). Both support create() and delete().

When I tested it, the user got access to the account. Then I changed the account’s owner to a different user, and the account was still inside the territory, and the user still had access.

What have you built? Buckets of territories, with any number of users in each, and code deciding which accounts go in. It’s the equivalent of creating sharing rules: dynamic sharing rules. And it’s easy to see who’s in which territory.

How many buckets? “Allocations for each territory model include up to 1,000 territories,” and Salesforce Customer Support can raise that depending on your edition (Sales Territories, from Allocations and Considerations for Territories). Your sales territories come out of the same 1,000.

It isn’t a perfect solution for every scenario. It’s a great tool in your tool belt. Know its edges:

  • Accounts and leads only. A record goes into a territory through ObjectId, which refers to Account or Lead (ObjectTerritory2Association). For other objects, the access comes through the account: its opportunities and cases, as set on the territory. For anything else, share the record to the territory’s group instead, since a territory group is a valid share target (Sharing a Record Using Apex).
  • Only the active model counts. You can have several models, but only one is active, and the bucket has to live in it (Sales Territories).
  • You now run two hierarchies. Turn territories on and you manage both the role hierarchy and the territory hierarchy (Two Hierarchies). If the org isn’t using territories already, that’s a lot to take on for a bucket.
  • Assignment rules use the same objects. Your code writes Territory2Manual; the rules write Territory2AssignmentRule. Don’t assign rules to the bucket territories: let your code be the only thing that decides who’s in.
  • Deletes don’t go to the recycle bin. “If you delete associations, you can query them for up to 12 hours. Keep in mind that deleted associations bypass the recycle bin” (ObjectTerritory2Association).
  • The logic is still yours. The bucket survives the owner change, but somebody still has to decide who goes in and who comes out. That’s module 5: the user trigger.

In 2023 I made a video of hidden territories used as buckets: code puts an account and a user in the same special territory, and the access stays when the owner changes. The note under the video says what’s still right and what’s changed since.

STA 4.17 · Salesforce Security Apex Sharing on Account Using Territories as Targets · 10:36 · published 2023-12-07 · plays from YouTube when you click it · Watch on YouTube · More videos: Salesforce Security Architecture Videos
  • For each programmatic share: is the access about the person, or about something they have in common? One share per person, or one per bucket.
  • How many buckets the design needs, today and at three times the volume, and how often their members move.
  • For code that shares to role groups: which group it writes to, and what happens to it when an Experience Cloud site is turned on.
  • Which of the four targets: a share to a person, a share to a group, a team member people can see, or a territory that survives an owner change.
  • If territories are the buckets: where the hidden branch lives, its access levels, how its buckets stay out of the assignment rules, and who owns the model.

Beyond the exam: what AI changes. Ask an AI assistant to share records with a list of people and it will usually write one share per person per record, because that’s the simplest code. Ask it the bucket question instead: what do these people have in common, and is there a group for it? And when an agent’s own user is put in a bucket, it gets every record the bucket gets, today and every record added later. Chapter 20 covers agents.