Skip to main content

SF-0293 · Coding · Hard

Can you elaborate how we can share records using apex sharing or programmatically?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026

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

  1. Confirm OWD is Private or Public Read Only on the object. On Public Read/Write the share rows are accepted but have no effect.
  2. (Custom objects only) Define an Apex Sharing Reason in Setup so your shares survive owner changes and are filterable.
  3. Build the share record in code with ParentId, UserOrGroupId, the access-level field, and RowCause.
  4. Mark the class without sharing because the running user usually can’t see the records they’re sharing.
  5. DML with Database.insert(..., false) to tolerate duplicate-share errors gracefully.

Field reference for a share row

FieldWhat it holds
ParentIdId of the record being shared (e.g., the Project__c record)
UserOrGroupIdId of the user, public group, role, “role and subordinates”, or queue
AccessLevel field — name varies by objectRead, Edit, or All (All is owner-only and reserved)
RowCauseManual, 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 = Manual are 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 Manual is 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 sharing on the class? The running user often can’t see the records they’re sharing with others — with sharing would 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 single Database.insert(shares, false).

Verified against: Apex Developer Guide — Apex Managed Sharing and Sharing in Apex. Last reviewed 2026-05-17.