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.
DeveloperName must 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
Profile can also carry one — Profile.customPermissions exists 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.md Gotcha 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 returns FeatureManagement.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
@AuraEnabled method that returns FeatureManagement.checkPermission('View_Refund_Controls').
- In the LWC template, use
lwc:if to 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 -> PermissionSetAssignment query in references/metadata-examples.md for the permission (or for SetupEntityType = 'CustomPermission' org-wide) and read PermissionSet.IsOwnedByProfile — profile-borne grants will not show up anywhere else (references/gotchas.md Gotcha 1).
- Author the
.customPermission-meta.xml from the matching shape in references/metadata-examples.md (feature flag, bypass, dependent, or connected-app). Write a description that names the consumers; leave isLicensed out 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 use admin/permission-set-expiration if the grant is temporary.
- Write each consumer in its own syntax —
NOT($Permission.X) for validation rules (contract in templates/admin/validation-rule-patterns.md), a Boolean formula resource for Flow, FeatureManagement.checkPermission('X') for Apex, @salesforce/customPermission/X for 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, missing requiredPermission targets, empty descriptions, and $Permission.X / checkPermission('X') references to permissions absent from the tree. Then sf project deploy validate, deploying definitions before grants before consumers.
- Verify in the org and record the evidence. Re-run the
SetupEntityAccess query 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
Salesforce-Specific Gotchas
-
Grant through permission sets, not profiles — but know that a profile grant deploys — Profile.customPermissions exists 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 in references/gotchas.md Gotcha 1.
-
Apex test classes do not inherit the running user's real permission sets — FeatureManagement.checkPermission() returns false in test context unless you explicitly create the permission set, add the custom permission to it, and assign it to the test user with a PermissionSetAssignment record inside System.runAs. Skipping this causes false-passing tests in development environments that have the permission set already assigned.
-
$Permission in 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 description over 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-character CustomPermission.description limit fast; scripts/check_custom_permissions.py flags it (CP-DESC-01 ERROR at 255+, CP-DESC-02 INFO 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 the NOT($Permission.X) bypass contract with templates/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; owns uiFormulaRule targeting 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; owns FeatureManagement.checkPermission, the @salesforce/customPermission import, managed-package namespacing, and the test patterns.
security/security-health-check — use when auditing org-wide access and permission hygiene at scale.