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.
A share can point at a bucket
Section titled “A share can point at a bucket”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.
One share per person, or one per bucket?
Section titled “One share per person, or one per bucket?”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.
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.
How many buckets is too many?
Section titled “How many buckets is too many?”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.
Four targets code can write
Section titled “Four targets code can write”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.
Hidden territories as buckets
Section titled “Hidden territories as buckets”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.
Where the territory trick stops
Section titled “Where the territory trick stops”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 writeTerritory2AssignmentRule. 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.
Watch it on video
Section titled “Watch it on video”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.
What an architect decides here
Section titled “What an architect decides here”- 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.
Explore
Section titled “Explore”- Sharing a Record Using Apex (opens in a new tab), Apex Developer Guide, Winter ‘27 (checked 2026-11-07)
- ObjectTerritory2Association (opens in a new tab), Object Reference for the Salesforce Platform, Winter ‘27 (checked 2026-11-07)
- UserTerritory2Association (opens in a new tab), Object Reference for the Salesforce Platform, Winter ‘27 (checked 2026-11-07)
- Public Group Considerations (opens in a new tab), Salesforce Help (checked 2026-11-07)
- sObjects That Can’t Be Used Together in DML Operations (opens in a new tab), Apex Developer Guide, Winter ‘27 (checked 2026-11-07)
