Skip to content
Back to skills

Permission Set Group Composition

ASecurity

Tactical guidance for composing Permission Set Groups: layering permission sets, applying Mute Permission Sets to subtract narrow capabilities, sequencing the recalculation lifecycle, deletion order, and assignment-vs-activation lifecycle. Triggers: 'PSG composition', 'mute permission set', 'PSG recalculation', 'cannot delete permission set in PSG', 'PSG explosion', 'expired PSG assignment'. NOT for the strategic profile-vs-PSG choice or migrating off profiles - use admin/permission-set-archi...

  • 15 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 6, 2026
ai-agentspythongospringapidevopssecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add PranavNagrecha/AwesomeSalesforceSkills --skill permission-set-group-composition --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Permission Set Group Composition?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Permission Set Group Composition
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pranavnagrecha-permission-set-group-composition/badge)](https://www.skillsdirectory.com/skills/pranavnagrecha-permission-set-group-composition)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: permission-set-group-composition
description: "Tactical guidance for composing Permission Set Groups: layering permission sets, applying Mute Permission Sets to subtract narrow capabilities, sequencing the recalculation lifecycle, deletion order, and assignment-vs-activation lifecycle. Triggers: 'PSG composition', 'mute permission set', 'PSG recalculation', 'cannot delete permission set in PSG', 'PSG explosion', 'expired PSG assignment'. NOT for the strategic profile-vs-PSG choice or migrating off profiles - use admin/permission-set-architecture. NOT for record-sharing design (OWD, role hierarchy, sharing rules) - use admin/sharing-and-visibility."
category: admin
salesforce-version: "Spring '25+"
well-architected-pillars:
  - Security
  - Reliability
  - Operational Excellence
tags:
  - permission-set-groups
  - muting-permission-sets
  - psg-composition
  - psg-recalculation
  - assignment-lifecycle
  - access-governance
triggers:
  - "how do I subtract a permission from a permission set group without cloning the group"
  - "permission set group recalculation is stuck or stale"
  - "cannot delete permission set because it is in a permission set group"
  - "manager persona needs everything sales has but not delete on opportunity"
  - "expiration date on permission set group assignment"
  - "PSG explosion fifty permission set groups overlapping"
  - "PSG naming convention environment persona"
  - "description data value too large max length 255"
inputs:
  - "Existing PSG inventory and the permission sets each PSG includes"
  - "Target persona and the delta between persona and the closest existing PSG"
  - "Whether muting is needed (one-way subtract) vs a smaller composable PS"
  - "User license attached to the persona and the PSes that depend on a feature license"
  - "Deployment context — sandbox vs prod, source-tracked vs change-set"
outputs:
  - "PSG composition plan listing included PSes, mute PS (if any), and rationale"
  - "Recalculation and rollout sequence for changes to a high-fan-out PSG"
  - "Deletion sequence for retiring a PS that is referenced by one or more PSGs"
  - "Naming-convention check report and consolidation candidates"
  - "Assignment lifecycle plan covering expiration, activation, and audit trail"
dependencies: []
version: 1.2.1
author: Pranav Nagrecha
updated: 2026-09-12
---

# Permission Set Group Composition

Activate this skill when the strategic decision to use Permission Set Groups is already made and the question is now tactical: which permission sets go into which PSG, when to add a Mute Permission Set, in what order to delete a PS that is wired into 50 PSGs, why a freshly assigned PSG still does not show its effective access, and how to keep PSG count from exploding. This is the operating manual that lives one floor down from `admin/permission-set-architecture` — that skill picks the access model, this skill makes the model survive contact with production.

This is NOT the place to argue PSG vs profile-stack — see `admin/permission-set-architecture` for the strategic case. This is also NOT for record-visibility design — sharing rules, OWD, and role hierarchy are out of scope.

## Before Starting

- How many PSGs already exist, and how many of them share at least one permission set? Heavy overlap is the signal for the "PSG explosion" anti-pattern this skill prevents.
- Is the desired delta from an existing PSG **subtractive** (mute) or **additive** (a new small PS)? Mute Permission Sets only ever subtract — they cannot grant.
- What user licenses are attached to the target personas, and do any included PSes require a feature license that the target user does not have?
- Is the org source-tracked (DX/Source) or change-set based? Mute Permission Sets are separate metadata files and must be retrieved explicitly — they do not travel inside the `permissionsetgroup-meta.xml` file.

## Questions to Ask Before Configuring

Put these to the requester before opening Setup or writing the XML. Each one exists because a specific trap in `references/gotchas.md` (cited by number) is cheap to avoid at question time and expensive to unwind after a group is assigned to 300 users.

| Question | Why it matters | What a good answer adds | What proper configuration adds over just doing it |
|---|---|---|---|
| "Which job function is this group for, and which capabilities does that job need — named one at a time?" | Gotcha 7: a group named after an incumbent or a department rather than a job function is the seed of PSG explosion | A capability list that maps onto small composable permission sets, plus the job title rather than the person's name | One group per job function that outlives the incumbent, instead of a 60-group estate that has to be excavated at audit time |
| "Of the permission sets this group needs, which already exist and are shared with other groups, and which would be private to this one?" | Gotcha 5: a shared set cannot be deleted while any group still references it, so today's sharing decision sets tomorrow's retirement cost | A reuse map — which sets are org-wide building blocks, which are single-group, and who owns each | Retirement runs as a planned detach → wait → delete ladder instead of a destructive deployment that fails halfway |
| "Is the delta from the closest existing group subtractive or additive — and if subtractive, is anyone in this persona still allowed the permission?" | Gotcha 1: muting is one-way, so no included set can hand a muted permission back, and a persona that needs it sometimes needs a second group rather than a mute | A mute-vs-new-set decision with the exact object and permission named | A one-file subtractive delta instead of a cloned group that drifts from its original inside one release |
| "When does this access have to be live, and who is watching the group's `status` between deploy and go-live?" | Gotcha 2: recalculation is asynchronous, so assignments made while `status` is `Outdated` look complete and grant nothing | A go-live time, a named owner for the status poll, and a quiet window for edits to high-fan-out sets | Users get access at the hour they were promised it, instead of a support queue whose answer is "wait and try again" |
| "Does this access belong behind a session — `hasActivationRequired` true — and if so, what performs the activation?" | Gotcha 3: `hasActivationRequired` (`api_meta L95331`, API 53.0+) is a third gate on top of assignment and recalculation, and each one can be true while the others are not | A standing-access-vs-session-activated decision and the UI or API call that activates it | Elevated capability that only exists inside an activated session, rather than standing privilege nobody remembers granting |
| "Which environments carry this group, and does the name encode the environment as `PSG_<persona>_<env>`?" | Gotcha 4: cross-environment moves are where the muting set gets dropped from a hand-curated manifest, and an environment-suffixed name is what makes that manifest reviewable | The environment list plus the manifest entries — `PermissionSet`, `MutingPermissionSet` and `PermissionSetGroup` named together (`api_meta L95396`) | The same composition lands in every org, instead of a production group whose mutes silently never travelled |
| "What single line goes in each `description`, and where does the composition rationale live instead?" | Gotcha 9: `PermissionSet.description` is capped at 255 characters (`api_meta L94788`), and an over-length set is rejected while every group composing it fails with `permission set names are invalid` | A one-line label per file plus the named home for the rationale — the package template or the configuration workbook | A deployment that validates first time, instead of a composition-layer error that sends the team hunting one layer above the real fault |

What a proper composition adds over just building the group: the persona's access is a named union of reusable parts with its subtractions in one auditable file, the rollout waits for the platform instead of racing it, and the same shape deploys to every org because the manifest and the naming were decided before the first click.

## Core Concepts

### Composition Is A Union, Mute Is A One-Way Subtract

A Permission Set Group's effective access is the **union** of every permission granted by every included permission set, minus anything subtracted by an attached Mute Permission Set. The grant-side is symmetric (any included PS can grant a permission and the user gets it) but the mute-side is **not** symmetric — once a permission is muted on the PSG, no other included PS in the same PSG can grant it back. Grant-wins inside a PSG only applies between included PSes; mute always wins over included grants.

This asymmetry is the single most-misunderstood fact about PSGs. "Mute X then re-grant X via another PS in the same PSG" does not work — the user still does not get X.

### The Recalculation Lifecycle Is Asynchronous

When a permission set that lives in N PSGs changes, every one of those PSGs enters a **recalculation** state. During recalculation the PSG status shows "Updating" and the effective access is whatever was calculated last — a freshly assigned user can wait for access while the PSG completes its recalc. Recalc time scales with PSG count, included PS count, and assignee count. Plan rollouts so PS edits land in a quiet window, not at the start of a release where downstream agents will hit stale access.

`Status` on the PSG metadata reports the result: `Updated` (good), `Outdated` (recalc not started), `Updating` (recalc running), `Failed` (recalc could not complete). Until status is `Updated`, do not assume new permissions are live.

### Assignment Is Distinct From Activation

A PSG is **activated** by Salesforce after recalculation completes. A PSG is **assigned** to a user via `PermissionSetAssignment` (yes — the same SObject that holds permission-set assignments; `PermissionSetGroupId` is set instead of `PermissionSetId`). A user can be assigned to a PSG that is still `Outdated`; they will not see effective access until the PSG flips to `Updated`. Conversely, deactivating or deleting a PSG without first removing assignments is blocked.

### Expiration On PSG Assignments

Since Spring '23 Salesforce supports `ExpirationDate` on Permission Set Group assignments — the same way it has supported expiration on Permission Set assignments. This is the right primitive for time-boxed elevation (contractor access, quarterly approver rights). Do not invent a Flow that "removes the PSG at midnight" — set the expiration and let the platform expire it automatically.

### Deletion Order: Detach, Wait, Delete

You cannot delete a permission set that is still referenced by a PSG. The supported sequence is:

1. Remove the PS from every PSG that includes it.
2. Wait for every affected PSG to finish recalculation (`Status = Updated`).
3. Delete the PS.

Skipping step 2 risks a delete that fails mid-deployment because the recalc had not yet released the dependency. Tooling that runs `delete` immediately after `update` on a PSG often hits this — add an explicit wait or split the deployment.

## Common Patterns

### Sales-Rep-With-Manager-Mute Pattern

**When to use:** A manager persona needs everything a sales rep has, except one or two narrow permissions (classic example: "Manager has Sales but NOT Delete on Opportunity" — managers should not bulk-delete pipeline data).

**How it works:** Build one `PSG_SalesRep_Prod` that includes the small composable PSes (`PS_OpportunityRead`, `PS_OpportunityCreate`, etc.). Build one Mute Permission Set `MutePS_NoOpportunityDelete` that subtracts `Delete` on `Opportunity`. Build `PSG_SalesManager_Prod` that includes the same sales PSes **plus** the mute PS. Two PSGs, one set of underlying PSes, one mute. No cloning.

**Why not the alternative:** Cloning `PSG_SalesRep_Prod` to `PSG_SalesManager_Prod` and editing one permission means every future change to the rep bundle has to be re-applied to the manager bundle. Drift starts the day the clone happens.

### Small Composable PSes Over Mega-PSes

**When to use:** Whenever a PS is starting to grow past ~30 object-permission grants or covers more than one "capability."

**How it works:** Split into `PS_<feature>_Read`, `PS_<feature>_Edit`, `PS_<feature>_Delete` and let PSGs compose them. The PSG carries the persona shape; PSes carry the feature shape.

**Why not the alternative:** A single `PS_Sales_Everything` PS forces every persona that wants any of it to get all of it, which forces a mute for every exclusion, which inflates mute count and hides intent.

### Time-Boxed Elevation

**When to use:** A user needs temporary elevated access (contractor, on-call rotation, audit support).

**How it works:** Assign the existing PSG with an `ExpirationDate` set on the `PermissionSetAssignment` row. Salesforce expires the assignment automatically; Setup Audit Trail records the expiration.

**Why not the alternative:** A custom Flow that deletes the assignment introduces a moving part that can fail silently. The platform-native expiration is auditable and does not depend on scheduled job health.

### Deletion-Order Dance For Retiring A PS

**When to use:** A permission set is referenced by N PSGs and needs to go away.

**How it works:**

1. Retrieve every PSG that lists the PS in `permissionSets`.
2. Update each PSG to remove the reference (one deployment).
3. Poll PSG `Status` until every affected PSG reports `Updated`.
4. Deploy the PS deletion.

**Why not the alternative:** A single deployment that updates the PSGs and deletes the PS in the same transaction will fail because the recalc has not yet released the FK-style reference.

## Decision Guidance

Use this when the request is "I need persona Y who is mostly like persona X except…":

| Situation | Recommended Approach | Reason |
|---|---|---|
| Persona Y needs **less** than the closest PSG (a permission must NOT be granted) | Add a **Mute Permission Set** to a new PSG variant | Subtractive delta is exactly what mute is for; no PS duplication |
| Persona Y needs **more** than the closest PSG (a new capability) | Build a **new small composable PS** and add it to the PSG | Mute cannot grant — additive deltas need a real PS |
| Persona Y needs a different **combination** of existing PSes | Build a **new PSG** referencing the existing PSes | PSGs are cheap; PSes are the reusable unit |
| Persona Y is a one-off (single user, ≤30 days) | Direct PS or PSG assignment with `ExpirationDate` | Don't distort the architecture for a temporary need |
| Existing PSG is "almost right" for many personas | Refactor — split the mega-PS into composable PSes | A PSG that needs muting for every persona is signalling its underlying PSes are too coarse |
| The same PS appears in 5+ PSGs | Likely fine — that is reuse working | Reuse of small PSes across PSGs is the goal, not a smell |
| The same permission appears granted in multiple PSes inside the same PSG | Consolidate the duplicates into one PS | Hidden duplication makes future muting harder to reason about |

## Recommended Workflow

1. **Inventory existing PSGs.** Run `python3 scripts/check_permission_set_group_composition.py --manifest-dir <path>` (add `--strict` to fail the run on the naming convention as well) against the `permissionsetgroups/` and `permissionsets/` directories — capture which PSes are referenced in multiple PSGs (good — reuse), which PSGs have zero included PSes (orphan), which PSGs use mute PSes (good — explicit subtract), which names violate the convention, and any `description` over length (`PSGC-DESC-01` ERROR at 255+ characters, `PSGC-DESC-02` INFO headroom at 200+, never fails the run).
2. **Identify the closest existing PSG.** Compare the target persona to existing PSGs and decide: subtractive delta (mute), additive delta (new PS), or different combination (new PSG).
3. **Apply the Decision Guidance table.** Choose mute, new PS, or new PSG based on the row that matches the request. Avoid cloning; cloning is the explosion vector.
4. **Compose the PSG.** Use the template at `templates/permission-set-group-composition-template.md`. Fill persona name, included PSes, mute PS (if any), license dependency, and lifecycle stage (draft / piloted / production). For the deployable XML — permission sets, muting set, group, `package.xml`, and the deploy order between them — copy from `references/metadata-examples.md`.
5. **Plan recalculation.** If a frequently-referenced PS is being touched, list every PSG that will recalc. Schedule the change for a quiet window. Do not pair a PS edit with a PS deletion in the same deployment.
6. **Roll out with assignment-vs-activation in mind.** Wait for `Status = Updated` before assigning users. For time-boxed elevation, set `ExpirationDate` on the assignment.
7. **Verify and audit.** Confirm Setup Audit Trail captured the composition change, run the checker again, and update the inventory artifact.

## Review Checklist

- [ ] No PSG was cloned to make a small variant — mute PS used instead for subtractive deltas.
- [ ] No mute-then-re-grant pattern inside the same PSG (mute always wins).
- [ ] Naming follows `PSG_<persona>_<env>` and `MutePS_<scope>_<delta>`.
- [ ] Every PSG `Status` is `Updated` before users are assigned to it.
- [ ] Permission set deletion sequenced as detach → wait for recalc → delete.
- [ ] Time-boxed assignments use `ExpirationDate`, not custom Flows.
- [ ] Each included PS appears in ≥2 PSGs OR is documented as persona-specific.
- [ ] Mute Permission Sets retrieved as separate metadata in source-tracked deployments.
- [ ] Setup Audit Trail change reviewed for the rollout.
- [ ] No `description` on a permission set, PSG, or mute set exceeds 255 characters; composition rationale lives in the template, not the metadata.

## Salesforce-Specific Gotchas

1. **Mute is one-way subtract — grant inside the same PSG cannot beat it.** A user with a muted permission stays muted even if another included PS grants it. Architecture diagrams that show "PS_A grants Delete, MutePS subtracts Delete, PS_B grants Delete → user gets Delete" are wrong.
2. **Recalculation is asynchronous and can fail.** Until `Status = Updated`, assigned users do not see new effective access; if status is `Failed`, the cause must be diagnosed (most often a deleted PS reference or a license incompatibility).
3. **You cannot delete a PS while any PSG still references it.** Salesforce returns a delete error; the fix is the detach → wait → delete sequence.
4. **Mute Permission Sets are separate metadata.** A change set or `package.xml` retrieve that pulls only `PermissionSetGroup` will not bring the mutes — they require an explicit `MutingPermissionSet` (Metadata API type) entry.
5. **License mismatch on an included PS silently breaks the PSG for some users.** A PSG that includes a PS scoped to "Salesforce" license cannot grant those permissions to a user on the "Platform" license — the PSG is valid, but the effective access for that user is reduced without warning.
6. **A `description` over 255 characters fails the deploy, and the PSG fails as a cascade.** `PermissionSet.description` is capped at 255 characters; a PSG that composes a rejected set fails too, with `permission set names are invalid` — a composition-layer symptom of a field-length problem one layer down.

## Output Artifacts

| Artifact | Description |
|---|---|
| Composition plan | Persona, included PSes, mute PS, license dependency, lifecycle stage (uses the template) |
| Recalculation rollout sequence | Ordered list of PSGs that will recalc when a referenced PS changes, with a quiet-window recommendation |
| Deletion plan | Detach → wait → delete sequence for retiring a PS that is referenced by one or more PSGs |
| Composition checker report | Output of `scripts/check_permission_set_group_composition.py` — ERRORs on platform facts (empty PSG, duplicate PS in a group, missing `label`, unknown `status`, a `description` over 255 characters — `PSGC-DESC-01`), WARNs on naming-convention violations and unresolved references (promoted to failures by `--strict`), INFOs on a `description` over 200 characters (`PSGC-DESC-02`, never promoted), GOODs on multi-PSG reuse and mute usage |

## Related Skills

- `admin/permission-set-architecture` — strategic counterpart: profile-vs-PS-vs-PSG architecture and migration off profile-centric access. Read it first if the request is "should we even be using PSGs."
- `security/permission-set-groups-and-muting` — security-pillar framing for the same domain; reach for it during security review.
- `admin/permission-sets-vs-profiles` — admin-level distinction between PSes and profiles; useful when the request is about the assignment basics rather than composition tactics.
- `devops/metadata-api-retrieve-deploy` — when the deployment context surfaces the Mute Permission Set retrieval gotcha.

Files in this skill

  • SKILL.md15.2 KB
  • references/examples.md8.7 KB
  • references/gotchas.md7.2 KB
  • references/llm-anti-patterns.md8.8 KB
  • references/well-architected.md7.4 KB
  • scripts/check_permission_set_group_composition.py9.7 KB
  • templates/permission-set-group-composition-template.md4.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…