Home / admin / custom-permissions
custom-permissions
Use when creating, assigning, or checking custom permissions to control feature access beyond CRUD and FLS. Trigger keywords: 'custom permission', 'FeatureManagement.checkPermission', '$Permission global variable', 'feature gate', 'named access grant', 'beta feature flag', 'SetupEntityAccess', 'requiredPermission', 'who has this custom permission'. NOT for gating Apex service code and its tests — use apex/apex-custom-permissions-check. NOT for permission set design — use admin/permission-set-architecture.
Open the skill folder on GitHub
Custom Permissions
Use this skill when a feature or capability needs a named access gate that goes beyond object, field, or record permissions. Custom permissions grant boolean access to a named capability and can be checked in validation rules, formula fields, Apex, Flow, LWC, Visualforce, and Lightning page component visibility. Each of those contexts spells the check differently.
Before Starting
- Confirm you need a custom permission, not a permission set feature license or a record-level sharing rule. Custom permissions are for feature on/off gates, not record visibility.
- Identify all platform contexts that must check the permission (validation rule, formula, Apex, Flow). Each uses a different syntax.
- Gather the exact API name you want.
DeveloperNamemust begin with a letter, contain only alphanumerics and underscores, not include spaces, not end with an underscore, and not contain two consecutive underscores; limit 80 characters (Object Reference,CustomPermission.DeveloperName). The name cannot be changed after creation without updating every reference. - Determine which permission sets will carry the permission. A
Profilecan also carry one —Profile.customPermissionsexists in the Metadata API from version 31.0 — but a profile-borne grant is invisible in the Custom Permissions related list, so grant through a permission set (references/gotchas.mdGotcha 1).
Questions to Ask Before Configuring
Ask these before creating anything. A custom permission is trivial to create and permanent in practice, so the cost of a wrong answer is paid later, by whoever inherits the org.
| Ask | Why it matters | What a good answer adds |
|---|---|---|
| "What exactly does holding this permission let someone do, in one sentence?" | If the sentence needs an "and", this is two permissions; a single flag that means several things cannot be revoked partially | The permission's description, and possibly a second permission |
| "Which contexts read it — validation rule, formula, Flow, Apex, LWC, component visibility?" | Each has a different spelling, and component visibility needs the extra CustomPermission. segment | The consumer list, and the set of syntaxes to get right (Gotcha 6) |
| "Does anything already hold this access through a profile?" | Profile.customPermissions deploys cleanly and hides in the profile-owned permission set | A SetupEntityAccess baseline before you add a second grant path (Gotcha 1) |
| "Does another custom permission have to be on for this one to make sense?" | requiredPermission enforces that at the platform level instead of in prose | A CustomPermissionDependencyRequired node and a single-package deploy order |
| "Who revokes it, and what proves it was revoked?" | A metadata diff cannot prove revocation — only enabled grants are ever retrieved | An owner and a live SetupEntityAccess query as the evidence artifact (Gotcha 8) |
| "Is this a bypass? If so, which named rules does it suppress?" | An unnamed bypass becomes an org-wide off switch nobody dares remove | The rule list in the description and a scoped Bypass_Validation_<Domain> name |
| "Is the grant permanent, or should it expire?" | Data-fix and migration bypasses outlive their reason by default | A time-limited permission set assignment (admin/permission-set-expiration) |
What a proper configuration adds over just creating the permission: the flag means one thing, every context that reads it uses the right syntax, the grant path is a permission set you can audit and expire, and there is a query that answers "who holds this today" without a Setup click-through.
Core Concepts
What Custom Permissions Are
Custom permissions are named boolean access grants. Unlike CRUD, FLS, and tab access, they carry no implicit meaning to the platform — they exist solely so developers and admins can build their own feature gates. Each custom permission has a label and an API name and can optionally be tied to a Connected App. When granted, the permission evaluates to true in every supported context for that running user's session.
Source: Salesforce Help — Custom Permissions Overview
Creating Custom Permissions
Custom permissions are created in Setup > Custom Permissions. Click New, enter a label and an API name, then optionally provide a description and a Connected App association.
Key constraints on the API name (CustomPermission.DeveloperName, Object Reference):
- Must begin with a letter, and must not include spaces.
- Only alphanumeric characters and underscores are allowed.
- Must not end with an underscore, and must not contain two consecutive underscores —
__is the namespace separator (NamespacePrefix__componentName). - Limit 80 characters, unique in the org.
- Cannot be changed after the permission is referenced in production without a coordinated update of all dependent metadata and code.
label and connectedApp are capped at 80 characters and description at 255 (Metadata API Developer Guide, CustomPermission field table).
The CustomPermission metadata type is supported by the Metadata API (version 31.0 and later) and SFDX source format. The file lives at customPermissions/My_Custom_Permission.customPermission-meta.xml, and the filename stem is the API name — there is no <fullName> element in source format. Deployable shapes, package.xml, and the verification queries are in references/metadata-examples.md.
Assigning Custom Permissions to Permission Sets
Grant custom permissions through permission sets. In Setup, open a permission set, navigate to Custom Permissions, and enable the desired permission. In the Metadata API, the permission set XML includes a <customPermissions> node (PermissionSetCustomPermissions, API 31.0+):
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Refund Pilot</label>
<customPermissions>
<enabled>true</enabled>
<name>My_Custom_Permission</name>
</customPermissions>
</PermissionSet>
A single custom permission can appear in multiple permission sets and permission set groups. When any one of those is assigned to a user, the permission evaluates to true for that user.
Profile accepts the identical shape through ProfileCustomPermissions, also from API version 31.0. It deploys, and it hides — see references/gotchas.md Gotcha 1 and admin/permission-sets-vs-profiles. Permission set structure, grouping, and muting are admin/permission-set-architecture.
Checking Custom Permissions in Each Platform Context
Validation Rules and Formula Fields
Use the $Permission global merge field. This returns true or false:
$Permission.My_Custom_Permission
To let users with the permission bypass a validation rule:
AND(
NOT($Permission.My_Custom_Permission),
/* original rule conditions */
)
Source: Salesforce Help — Use Custom Permissions in Formulas
Apex
Use FeatureManagement.checkPermission(String apiName). It returns a Boolean and does not consume a SOQL row or any measurable governor resource:
if (FeatureManagement.checkPermission('My_Custom_Permission')) {
// user holds the permission — allow the action
}
Source: Apex Developer Reference — FeatureManagement Class
Flow
$Permission is a global resource available in Flow, but only as a formula resource — not directly in a Decision element condition row. The pattern is:
- Create a formula resource of type Boolean with the value
$Permission.My_Custom_Permission. - Reference that resource variable in a Decision element condition.
Visualforce
{!$Permission.My_Custom_Permission}
This evaluates to the string "true" or "false" in merge field contexts, or as a Boolean in rendered attributes.
Lightning Page Component Visibility, Dynamic Forms, and In-App Guidance
Component-visibility filters use a different grammar — note the extra CustomPermission. segment:
{!$Permission.CustomPermission.My_Custom_Permission}
Supported for app, Home, and record pages only (Metadata API Developer Guide, FlexiPage component-visibility expressions; the same expression appears in Prompt.uiFormulaRule). Owned by admin/dynamic-forms-and-actions and admin/in-app-guidance-and-walkthroughs.
LWC
import hasPerm from '@salesforce/customPermission/My_Custom_Permission';
The import resolves to true or undefined, never false, and it shapes the DOM rather than enforcing anything. The Apex method behind the component needs its own check. Owned by apex/apex-custom-permissions-check.
Connected Apps
The CustomPermission metadata type has a connectedApp field: "The name of the connected app that's associated with this permission. Limit: 80 characters." UNVERIFIED (2026-09-04): the Metadata API guide does not document what that association enforces at authorization time, and the CustomPermission sObject exposes no connected-app field to query it back — do not promise an access_denied on OAuth. To require permissions for a connected app, the grounded field is ConnectedApp.permissionSetName (API 46.0+), which needs isAdminApproved set to true.
Common Patterns
Pattern: Feature Gate for Beta or Graduated Rollout
When to use: A new feature is ready for testing by a subset of users before general availability. You want to control who sees it without a code deployment.
How it works:
- Create a custom permission: API name
Beta_New_Case_Console. - Create a permission set:
Beta Testers — Case Console. Add the custom permission to it. - Assign the permission set to pilot users only.
- In an LWC, call a
@AuraEnabled(cacheable=true)Apex method that returnsFeatureManagement.checkPermission('Beta_New_Case_Console'). - Conditionally render the feature component based on the returned Boolean.
- Guard the Apex action itself with the same check so the gate cannot be bypassed via API.
- Expand rollout by assigning the permission set to more users — no code change required.
Why not the alternative: A custom Boolean field on the User object requires a SOQL query in Apex and cannot be read by $Permission in formulas. FeatureManagement.checkPermission is instant, session-accurate, and has no governor-limit cost.
Pattern: Admin-Only Bypass in Validation Rules
When to use: A validation rule must enforce data quality for regular users but admins or integration users need an escape hatch without deactivating the rule.
How it works:
- Create a custom permission:
Bypass_Validation_Rule_Account. - Add it to the integration user's permission set.
- Wrap the existing validation rule with an outer AND condition:
AND(
NOT($Permission.Bypass_Validation_Rule_Account),
/* original rule conditions here */
)
Why not the alternative: $Profile.Name is fragile — profiles get renamed and new admin profiles would require a rule update. Custom permissions decouple the bypass from profile identity entirely.
Pattern: Conditional UI Visibility Gate in LWC
When to use: A button, panel, or action should only appear for users with a specific named entitlement, and server-side enforcement is required.
How it works:
- Create a custom permission:
View_Refund_Controls. - Expose an Apex wire adapter or
@AuraEnabledmethod that returnsFeatureManagement.checkPermission('View_Refund_Controls'). - In the LWC template, use
lwc:ifto conditionally render the element. - In the Apex action handler, repeat the check before executing any sensitive logic.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Gate a feature for a named group of users | Custom permission on a permission set | Named, auditable, toggleable without code change |
| Bypass a validation rule for integration users | NOT($Permission.My_Permission) as outermost AND | Decoupled from profile identity, survives renames |
| Check access in Apex | FeatureManagement.checkPermission('ApiName') | No SOQL, no governor limit cost, session-accurate |
| Check access in Flow | Formula resource of type Boolean: $Permission.ApiName | $Permission is only available inside formula resources |
| Check access in Visualforce | {!$Permission.ApiName} in rendered attribute or expression | Global merge field works natively in VF |
| Grant the permission to specific users | Add to a dedicated permission set and assign that set | Cannot assign a custom permission directly to a profile |
| Verify access in an Apex test | Assign the permission set to the test user in @TestSetup | FeatureManagement.checkPermission returns false in test context without explicit perm set assignment |
Recommended Workflow
- Answer the seven questions above and fill
templates/custom-permissions-template.md. The consumer list you produce there is the input to every later step; if the permission's purpose needs an "and", split it into two permissions now. - Baseline the existing grants. Run the
SetupEntityAccess->PermissionSetAssignmentquery inreferences/metadata-examples.mdfor the permission (or forSetupEntityType = 'CustomPermission'org-wide) and readPermissionSet.IsOwnedByProfile— profile-borne grants will not show up anywhere else (references/gotchas.mdGotcha 1). - Author the
.customPermission-meta.xmlfrom the matching shape inreferences/metadata-examples.md(feature flag, bypass, dependent, or connected-app). Write adescriptionthat names the consumers; leaveisLicensedout entirely — it is read-only (Gotcha 7). - Author the grant as a
<customPermissions>node in a permission set, not a profile. For a bypass, decide expiry here and useadmin/permission-set-expirationif the grant is temporary. - Write each consumer in its own syntax —
NOT($Permission.X)for validation rules (contract intemplates/admin/validation-rule-patterns.md), a Boolean formula resource for Flow,FeatureManagement.checkPermission('X')for Apex,@salesforce/customPermission/Xfor LWC, and{!$Permission.CustomPermission.X}for component visibility. The last one is the one that gets copied wrong (Gotcha 6). - Lint, then validate. Run
python3 skills/admin/custom-permissions/scripts/check_custom_permissions.py --manifest-dir <dir>— it flags dangling grants as ERROR, missingrequiredPermissiontargets, empty descriptions, and$Permission.X/checkPermission('X')references to permissions absent from the tree. Thensf project deploy validate, deploying definitions before grants before consumers. - Verify in the org and record the evidence. Re-run the
SetupEntityAccessquery and keep the result as the access-review artifact; a metadata diff cannot prove a revocation (Gotcha 8). Work the Review Checklist below.
Review Checklist
- [ ] Custom permission API name starts with a letter and contains only alphanumeric characters and underscores.
- [ ] Permission is added to at least one permission set (not directly to a profile).
- [ ] All platform contexts that check the permission use the correct syntax for their context.
- [ ] Apex unit tests assign the permission set to the test user via
System.runAsand aPermissionSetAssignmentinsert. - [ ] Validation rule bypass uses
NOT($Permission.X)as an outer AND condition so the original rule logic is preserved. - [ ] The custom permission metadata file and updated permission set XML are committed to source control.
- [ ]
check_custom_permissions.pyhas been run on the metadata directory and reports no ERROR. - [ ] Every custom permission has a non-empty
descriptionnaming its consumers. - [ ]
descriptionis under 255 characters (CP-DESC-01ERROR) and ideally under 200 (CP-DESC-02INFO, headroom only) — if the consumer list alone does not fit, split the permission rather than compressing it. - [ ]
isLicenseddoes not appear in any authored.customPermission-meta.xml(it is read-only). - [ ] Component visibility filters use
{!$Permission.CustomPermission.X}, not the formula spelling. - [ ] Any
requiredPermissiontarget ships in the same deployment package as its parent. - [ ] Grants were verified in the org with the
SetupEntityAccessquery, andIsOwnedByProfilewas checked for hidden profile grants.
Salesforce-Specific Gotchas
- Grant through permission sets, not profiles — but know that a profile grant deploys —
Profile.customPermissionsexists in the Metadata API from version 31.0, the same version as the permission set element. A profile-borne grant is real and is invisible in the Custom Permissions related list. Full detail inreferences/gotchas.mdGotcha 1. - Apex test classes do not inherit the running user's real permission sets —
FeatureManagement.checkPermission()returnsfalsein test context unless you explicitly create the permission set, add the custom permission to it, and assign it to the test user with aPermissionSetAssignmentrecord insideSystem.runAs. Skipping this causes false-passing tests in development environments that have the permission set already assigned. $Permissionin Flow is only available inside formula resources — it cannot be typed directly into a Decision element condition row. Create a formula resource (type: Boolean) with value$Permission.My_Permission, then reference that resource in the Decision element.- API name changes break all references without warning — renaming a custom permission does not cascade to validation rules, formula fields, or Apex code. Formulas referencing the old name silently evaluate to
false; Apex code fails at deploy time if the reference is in a compile-time string but may silently fail at runtime in dynamic contexts. Treat the API name as immutable once the permission is in production. - A
descriptionover 255 characters fails the deploy — the consumer list (the only place it survives, Gotcha 9) plus any rationale text overflows the Metadata API's 255-characterCustomPermission.descriptionlimit fast;scripts/check_custom_permissions.pyflags it (CP-DESC-01ERROR at 255+,CP-DESC-02INFO headroom at 200+) before the deploy does.
Output Artifacts
| Artifact | Description |
|---|---|
| Custom permission metadata | CustomPermission XML file deployable via SFDX or Metadata API |
| Permission set update | Updated .permissionset-meta.xml containing the <customPermissions> node |
| Apex guard clause | FeatureManagement.checkPermission call wrapped in a helper method with test coverage pattern |
| Validation rule expression | NOT($Permission.X) bypass wrapper for existing validation rule formulas |
| Checker report | Output of check_custom_permissions.py: coverage table, ERROR on dangling grants, WARN on missing dependency targets, empty descriptions, and undefined $Permission / checkPermission references |
| Access evidence | SetupEntityAccess -> PermissionSetAssignment result set, timestamped, from references/metadata-examples.md |
Reference Files
| File | Read it when |
|---|---|
references/metadata-examples.md | Writing or reviewing the deployable XML, package.xml, deploy order, or the SetupEntityAccess verification queries |
references/gotchas.md | A permission "isn't working", a grant cannot be explained, or a metadata diff is being used as access evidence |
references/examples.md | Working an end-to-end scenario: beta gate, validation-rule bypass, or the Apex test-setup pattern |
references/well-architected.md | Justifying custom permission vs custom setting vs permission set license, or citing the source behind a claim |
references/llm-anti-patterns.md | Reviewing AI-generated custom permission guidance before it ships |
templates/custom-permissions-template.md | Starting a task — capture the API name, consumer list, and grant plan before authoring metadata |
Related Skills
admin/permission-set-architecture— use when the question is how to structure permission sets and groups, not how to create or check a custom permission.admin/permission-sets-vs-profiles— use when deciding whether a grant belongs on a profile at all; owns the migration argument behind Gotcha 1.admin/permission-set-expiration— use when a bypass or pilot grant must expire on a date rather than be remembered.admin/validation-rules— use when validation rule logic is the primary concern and the custom permission is only the bypass mechanism; owns theNOT($Permission.X)bypass contract withtemplates/admin/validation-rule-patterns.md.admin/dynamic-forms-and-actions— use for field and component visibility rules; owns the{!$Permission.CustomPermission.X}filter syntax.admin/in-app-guidance-and-walkthroughs— use for prompts and walkthroughs; ownsuiFormulaRuletargeting on custom permissions.admin/flow-for-admins— use when the check happens inside a Flow; owns the Boolean formula-resource pattern.apex/apex-custom-permissions-check— use when the gate lives in Apex or LWC; ownsFeatureManagement.checkPermission, the@salesforce/customPermissionimport, managed-package namespacing, and the test patterns.security/security-health-check— use when auditing org-wide access and permission hygiene at scale.