Skip to content

Keeping Programmatic Shares Right

Module 5 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: Once code writes the shares, what keeps them right: after go-live, when people change, when the rules change, and on day zero?

Module 1 said it plainly: the moment your code writes a share, you own everything the platform did for you before. This module is what owning it looks like. Two triggers, a recalculation, and a plan for the first day.

Let’s go back to the apple object from module 2. Somewhere you keep the rules for who sees which apples. Where do people usually keep them? Often in custom metadata: if the user is this, give them the green apples; if the user is that, give them the red apples.

Then you write a flow or a trigger on the apple. A new apple is inserted, the logic reads the rules, queries the users and writes the share records. Something changes on an apple, the logic runs again and adds or removes shares.

So here’s the question I ask in class. Is the apple trigger the only flow or trigger I need to keep access right over time?

A student got it: what about the users? A new user arrives, and there are no share records for them. A user’s role changes, or their region, or their skill, and they should no longer see the green apples. Nothing on the apple changed, so the apple trigger never runs.

You need a trigger on the user too. Put the share logic in one place, and call it from both: from the apple trigger and from the user trigger.

When I review someone’s programmatic sharing, that’s how I start. The first thing I ask is: where’s the trigger on the apple object? Then I ask: where’s the trigger on the user object? Ask the simple question: what happens when a user is added?

Two triggers, one piece of share logic Two triggers, one piece of share logic. The record trigger, on the record being shared, runs for a new record, when a field the rules read changes, and when the owner changes if the shares use the Manual reason. The user trigger, on the User object, runs for a new user or one made active again, when a role, region or skill changes, and when a user is deactivated; it writes shares in its own transaction. Footnote: both call the same share logic; in every review, ask where the trigger on the record is, and where the trigger on the user is. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-SVA-PS-09", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison 0.5.0", "theme": "stevetecharc 1.4.0"} The record trigger on the record being shared A new record A field the rules read changes The owner changes, if shares use Manual The user trigger on the User object A new user, or one made active again A role, region or skill changes A user is deactivated Writes shares in its own transaction Both call the same share logic. In every review: where's the trigger on the record, and where's the trigger on the user? © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

The record trigger listens for:

  • A new record. Write its shares.
  • A change to a field the rules read. Remove the shares it no longer deserves, add the new ones.
  • An owner change, on a standard object, if your shares use the Manual reason. They’re deleted when the owner changes, so you put them back. Better, since Winter ‘27: give the code its own sharing reason, and they’re kept (module 3).

The user trigger listens for:

  • A new user, or a user made active again. Write the shares they should have.
  • A change to a field the rules read: a role, a region, a skill, a certification.
  • A user deactivated. Decide whether their shares should go.

And notice what module 4 buys you here. If your shares point at buckets, the user trigger doesn’t touch the share table at all. It changes who’s in the group, and Salesforce does the rest.

One more trap, from chapter 9. When an account changes owner, its opportunities can change owner with it, and Salesforce says that cascaded change “doesn’t invoke any automations, such as flows or Apex triggers” (What Survives an Owner Change). A trigger that puts shares back on the opportunity won’t fire for those. That’s one more reason for a custom sharing reason.

The user trigger needs its own transaction

Section titled “The user trigger needs its own transaction”

Here’s something that catches developers the first time. The User object is a setup object, and Salesforce won’t let you mix it with ordinary records in one transaction. “DML operations on certain sObjects, sometimes referred to as setup objects, can’t be mixed with DML on non-setup sObjects in the same transaction. This restriction exists because some sObjects 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).

The list includes User, Group, GroupMember, UserRole and UserTerritory2Association. A share record isn’t on it. So a trigger on the user that inserts share records in the same transaction fails with a mixed DML error. The user’s half of the logic runs in its own transaction: Salesforce’s example uses a method with the future annotation, and a queueable job works the same way.

That has a consequence for the business, too. The shares arrive a moment after the user is saved, not in the same instant. Usually nobody notices. Your tests will.

Here’s the shape, with the apple. These are sketches, not a finished app: the rules sit in custom metadata, and a helper class I’ll call AppleRules reads them. I don’t show it, because every org’s rules are different. The apple has a Color__c field, the user’s Department is the field the rules read, and the sharing reason is Party, from module 2.

First, the share logic, written once:

// The share logic, written once. Both triggers and the recalculation call it.
// Without sharing, on purpose: it has to see every apple to share it.
public without sharing class AppleSharing {
// Make the Party shares for these apples and these users match the rules.
public static void rebuild(List<Apple__c> apples, List<User> users) {
// 1. Remove what this logic wrote before. Only our own reason, never Manual.
List<Apple__Share> oldShares = [
SELECT Id FROM Apple__Share
WHERE ParentId IN :apples
AND UserOrGroupId IN :users
AND RowCause = :Schema.Apple__Share.RowCause.Party__c];
delete as system oldShares;
// 2. Write what the rules say now. An inactive user gets nothing.
List<Apple__Share> shares = new List<Apple__Share>();
for (Apple__c apple : apples) {
for (User u : users) {
if (u.IsActive && AppleRules.allows(u, apple)) {
shares.add(new Apple__Share(
ParentId = apple.Id,
UserOrGroupId = u.Id,
AccessLevel = 'Read',
RowCause = Schema.Apple__Share.RowCause.Party__c));
}
}
}
Database.insert(shares, false, AccessLevel.SYSTEM_MODE);
}
}

Then the trigger on the apple. It only calls the logic when something the rules read has changed:

// On the apple: a new apple, or a change to the field the rules read.
trigger AppleSharingTrigger on Apple__c (after insert, after update) {
List<Apple__c> changed = new List<Apple__c>();
for (Apple__c apple : Trigger.new) {
if (Trigger.isInsert
|| apple.Color__c != Trigger.oldMap.get(apple.Id).Color__c) {
changed.add(apple);
}
}
if (!changed.isEmpty()) {
AppleSharing.rebuild(changed, AppleRules.usersFor(changed));
}
}

And the trigger on the user. It does the same check, but it can’t write the shares itself, so it hands the user IDs to a queueable job, which runs in its own transaction:

// On the user: a new user, active or inactive, or a change to the field the rules read.
trigger UserAppleSharing on User (after insert, after update) {
Set<Id> changed = new Set<Id>();
for (User u : Trigger.new) {
User before = Trigger.isUpdate ? Trigger.oldMap.get(u.Id) : null;
if (before == null
|| u.IsActive != before.IsActive
|| u.Department != before.Department) {
changed.add(u.Id);
}
}
// Not here: share records can't be written in the same transaction as a user.
if (!changed.isEmpty()) {
System.enqueueJob(new AppleShareJob(changed));
}
}
// The user's half, in its own transaction.
public class AppleShareJob implements Queueable {
private Set<Id> userIds;
public AppleShareJob(Set<Id> userIds) {
this.userIds = userIds;
}
public void execute(QueueableContext context) {
List<User> users = [
SELECT Id, IsActive, Department FROM User WHERE Id IN :userIds];
AppleSharing.rebuild(AppleRules.applesFor(users), users);
}
}

Look at what the deactivated user gets. The job removes their Party shares and writes none back. That’s one answer to “decide whether their shares should go”. Yours may be different, but write it down.

A real version would go further: narrow the apples and the users before the loop, handle the rows that fail to insert, and test with large volumes. But the design is all here. One piece of logic, two doors into it.

The triggers handle records and users. But what if the rules themselves change? Someone edits the custom metadata, and now the blue apples go to a different team. No apple changed, no user changed, and every share is out of date.

That’s a recalculation, and on a custom object Salesforce gives you a place for it. “To recalculate Apex managed sharing, you must write an Apex class that implements a Salesforce-provided interface to do the recalculation.” Then you attach the class to the custom object, in the Apex Sharing Recalculation related list on its detail page (Recalculating Apex Managed Sharing (opens in a new tab), Apex Developer Guide, Winter ‘27, checked 2026-11-07).

What to know about it:

  • It’s batch Apex. The class implements Database.Batchable, so it works through the records in chunks, within the governor limits. Salesforce’s example finds the existing shares with its own sharing reasons, and writes the shares the logic says each record should have.
  • It runs on its own when the default changes. “Every time a custom object’s organization-wide sharing default access level is updated, any Apex recalculation classes defined for associated custom object are also executed.”
  • You can run it yourself. From the object’s detail page, or with Database.executeBatch. Salesforce’s example of why: “if a locking issue prevented Apex code from granting access to a user.”
  • Custom objects only, on that page. “Apex sharing reasons and Apex managed sharing recalculation are only available for custom objects.” For standard objects, you write and schedule the batch class yourself.
  • Watch it. Running recalculations are in Setup, under Apex Jobs.

With the share logic in one class, the recalculation is short. It’s the third door into the same logic:

// When the rules change: rebuild every apple's Party shares, one chunk at a time.
public without sharing class AppleSharingRecalc implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext context) {
return Database.getQueryLocator(
[SELECT Id, Color__c FROM Apple__c], AccessLevel.SYSTEM_MODE);
}
public void execute(Database.BatchableContext context, List<Apple__c> apples) {
AppleSharing.rebuild(apples, AppleRules.usersFor(apples));
}
public void finish(Database.BatchableContext context) {
// Tell someone it finished. Salesforce's example sends an email.
}
}

To run it yourself: Database.executeBatch(new AppleSharingRecalc());. Salesforce’s own example, a recruiting app with a Job object, has the same shape: it finds the shares with its own reasons, deletes them, and inserts new ones in system mode. It also shows one error to expect and ignore. A share that grants no more than the org-wide default already does is refused, and “an insert exception is acceptable” (Recalculating Apex Managed Sharing).

Now the first day. You’re going live with a million accounts, and your logic will give each one, on average, 10 user shares. That’s 10 million share records before anyone logs in.

What are the two ways to get day-zero shares in? One is to load the accounts and let your logic run: kick off a batch class, and it can sit there and run for hours, or a day or two. What’s a way to get them in quicker? A student in class said it: the data loader. Work the shares out ahead of time and insert the share records themselves with a bulk data load.

Day zero: two ways to load the shares Day zero: two ways to load the shares. A batch class: the logic runs inside Salesforce, the same share logic as the triggers; it works in chunks within the governor limits and can run for hours, or a day or two. A bulk load: the share records are worked out ahead of time and inserted with Data Loader or the Bulk API; much faster, but its logic must match the triggers'. Footnote: a million accounts with 10 shares each is 10 million share records before anyone logs in. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-SVA-PS-10", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison 0.5.0", "theme": "stevetecharc 1.4.0"} A batch class the logic runs inside Salesforce The same share logic as the triggers Works in chunks, within the governor limits Can run for hours, or a day or two A bulk load the share records worked out ahead Inserted with Data Loader or the Bulk API Much faster Its logic must match the triggers' A million accounts with 10 shares each: 10 million share records before anyone logs in. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

The bulk load is faster, and it has one risk: the logic that worked out those rows lives outside your triggers. If it doesn’t match, day one starts with shares your own code would never have written. Some clients go further: their rules of visibility live in another system entirely, and that system feeds the share records in through the REST API or the Bulk API, for good (module 1).

Big loads also move groups, roles and users. Salesforce lets an admin hold back part of that work: “a Salesforce admin can suspend sharing calculations, specifically for sharing rules and group membership, then resume calculations during an organization’s maintenance period.” It isn’t on by default: “To enable it for your organization, contact Salesforce Customer Support” (Defer Sharing Calculations (opens in a new tab), Salesforce Help, checked 2026-11-07). If your go-live needs it, ask early.

Spring ‘26: access arrives a little later

Section titled “Spring ‘26: access arrives a little later”

One more change, and it touches every design in this module. In Spring ‘26, Salesforce released an update to how role and group changes are recalculated. In the words of Salesforce’s admin blog: “Currently, when you change a user’s Role or Group, Salesforce tries to update all affected record permissions immediately (synchronously).” With the update, these changes are processed in the background, and “there might be a short delay before a user actually gets access to their new records.” The release update is “Prepare your Apex code and flows for changed asynchronous sharing updates,” available starting in Spring ‘26, and “Salesforce plans to enforce this in Spring ‘27” (Jen’s Top Spring ‘26 Features for Admins (opens in a new tab), Salesforce Admins blog, checked 2026-11-07).

Here’s where it bites programmatic sharing. Your user trigger puts someone in a bucket group, and the next step assumes they can now see the bucket’s records: it assigns them a case, or runs a query as them. With the update, that access may not be there yet. Salesforce’s advice: “Update Apex classes, tests, and flows that update group membership or roles if they rely on synchronous sharing recalculation.” Turn the update on in a sandbox first, and run your tests.

Test knowledge. Programmatic sharing needs logic in two places: on the record being shared, and on the user, because a new user or a changed user gets no shares from a record trigger. When the rules change, a batch recalculation; on custom objects, the class goes in the Apex Sharing Recalculation related list. A likely area, not a known question.

  • Where the share logic lives, once, and which triggers or flows call it: on the record, and on the user.
  • Which fields on the record and on the user the rules read, so each trigger knows what to listen for.
  • How the user’s half runs in its own transaction, and what the business sees while it waits.
  • Who can change the rules, and what recalculates the shares when they do.
  • For go-live: a batch class or a bulk load of the share records, how long each takes at today’s volume, and whether to ask Support about deferring sharing calculations.
  • Whether the Spring ‘26 release update for asynchronous sharing is tested in a sandbox, and which automation assumed access was immediate.

Beyond the exam: what AI changes. Ask an AI assistant to write the sharing trigger, and it will usually write one, on the record. Ask it my review questions: where’s the trigger on the user, what happens when a user is added, and what runs when the rules change? And agents create users and change roles too. Each of those is a user change your logic has to hear, and since Spring ‘26 the access it grants may arrive a little later. Chapter 20 covers agents.