Programmatic sharing in Salesforce is done by inserting rows into the object’s *__Share table. Every standard object whose OWD is Private or Public Read Only has an auto-generated share object (AccountShare, OpportunityShare, etc.), and every custom object with the same OWDs has MyObject__Share. Each row grants one user or group access to one record at a specified access level, with a RowCause describing why the share exists.
The five steps
- Confirm OWD is Private or Public Read Only on the object. On Public Read/Write the share rows are accepted but have no effect.
- (Custom objects only) Define an Apex Sharing Reason in Setup so your shares survive owner changes and are filterable.
- Build the share record in code with
ParentId,UserOrGroupId, the access-level field, andRowCause. - Mark the class
without sharingbecause the running user usually can’t see the records they’re sharing. - DML with
Database.insert(..., false)to tolerate duplicate-share errors gracefully.
Field reference for a share row
| Field | What it holds |
|---|---|
| ParentId | Id of the record being shared (e.g., the Project__c record) |
| UserOrGroupId | Id of the user, public group, role, “role and subordinates”, or queue |
| AccessLevel field — name varies by object | Read, Edit, or All (All is owner-only and reserved) |
| RowCause | Manual, a system reason, or a custom Apex sharing reason (custom objects only) |
On standard objects the access field is named OpportunityAccessLevel, CaseAccessLevel, AccountAccessLevel, etc. On custom objects it’s just AccessLevel.
Code: sharing a custom Project record with stakeholders
public without sharing class ProjectStakeholderSharing {
public static void shareProject(Id projectId, Set<Id> stakeholderUserIds) {
if (projectId == null || stakeholderUserIds == null || stakeholderUserIds.isEmpty()) {
return;
}
List<Project__Share> shares = new List<Project__Share>();
for (Id uid : stakeholderUserIds) {
Project__Share s = new Project__Share();
s.ParentId = projectId;
s.UserOrGroupId = uid;
s.AccessLevel = 'Edit';
// Custom reason defined on Project__c in Setup
s.RowCause = Schema.Project__Share.RowCause.Stakeholder_Access__c;
shares.add(s);
}
// allOrNone = false: partial successes allowed, duplicates ignored
Database.SaveResult[] results = Database.insert(shares, false);
for (Database.SaveResult r : results) {
if (!r.isSuccess()) {
for (Database.Error e : r.getErrors()) {
if (e.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION
|| e.getStatusCode() == StatusCode.DUPLICATE_VALUE) {
// expected — share already exists, ignore
continue;
}
System.debug(LoggingLevel.ERROR, 'Share insert failed: ' + e);
}
}
}
}
}
Code: sharing a standard Opportunity with a public group
public without sharing class OpportunityCfoSharing {
public static void share(Id oppId, Id publicGroupId) {
OpportunityShare s = new OpportunityShare();
s.OpportunityId = oppId;
s.UserOrGroupId = publicGroupId;
s.OpportunityAccessLevel = 'Read';
s.RowCause = Schema.OpportunityShare.RowCause.Manual;
insert s;
}
}
Note the key differences: the parent field is OpportunityId not ParentId, the access field is OpportunityAccessLevel not AccessLevel, and standard objects only allow RowCause = Manual — no custom Apex sharing reasons.
Why custom Apex sharing reasons matter
On a custom object, you can define one or more Apex Sharing Reasons in Setup (e.g., Stakeholder_Access). Using a custom reason instead of Manual gives you:
- Persistence across owner changes. Shares with
RowCause = Manualare wiped when ownership changes. Custom-reason shares survive. - Filterability. You can query
WHERE RowCause = 'Stakeholder_Access__c'to find and recalculate shares created by your code. - Recalculation hook. When OWD or sharing-rule definitions change, Salesforce recalculates standard shares. You can register an Apex sharing recalculation class to recompute your custom shares too.
Common gotchas
- DUPLICATE_VALUE error when the same share already exists — handle with
allOrNone = false. - Standard objects can’t use custom reasons — only
Manualis allowed. - Master-detail child objects don’t support Apex sharing — they’re Controlled by Parent and inherit access.
- Effective access is the maximum of all shares pointing at a record for that user. Inserting a Read share on top of an existing Edit share doesn’t downgrade access.
Common interview follow-ups
- Why
without sharingon the class? The running user often can’t see the records they’re sharing with others —with sharingwould block the DML on records they don’t own. - Can I delete a share? Yes —
delete [SELECT Id FROM Project__Share WHERE ParentId = :projectId AND RowCause = 'Stakeholder_Access__c'];. But only custom-reason and manual shares are deletable; hierarchy and sharing-rule shares are system-managed. - What’s the bulk pattern? Build a
List<*__Share>across the trigger’s record set, then a singleDatabase.insert(shares, false).
Verified against: Apex Developer Guide — Apex Managed Sharing and Sharing in Apex. Last reviewed 2026-05-17.