SfSkills Domains Docs GitHub

Home / admin / permission-set-expiration

permission-set-expiration

Use when an assignment must expire on a date, not stand forever. Trigger keywords: permission set expiration date, permission set group expiry, temporary access, time-boxed elevation, contractor access, ExpirationDate. NOT for PSG composition - use admin/permission-set-group-composition. NOT for muting mechanics - use security/permission-set-groups-and-muting. NOT for criteria-based auto-assignment - use admin/user-access-policies.

Open the skill folder on GitHub


Permission Set Expiration

This skill activates when access has an end date: contractor and vendor grants, on-call elevation, audit-season read access, a migration cutover role, or any permission a security review has agreed must not become standing privilege. It covers where the expiration is stored, what the platform does and does not take away when the date passes, which assignment types cannot carry an expiry at all, and how to audit the population before it silently drifts back to permanent.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The expiry is a field on the assignment row

PermissionSetAssignment "represents a user's assignment to a permission set or permission set group" and is available in API version 22.0 and later. Its supported calls are create(), delete(), describeSObjects(), query(), retrieve(), and update(). Access to the object requires View Setup and Configuration, Assign Permission Sets, or Manage User.

FieldType — PropertiesWhat it means for time-boxing
ExpirationDatedateTime — Create, Filter, Nillable, Sort, Update"The date that the assignment of the permission set or permission set group expires for the specified user." API version 52.0 and later. Nillable, so an assignment with no end date is the same row with this field null.
IsActiveboolean — Defaulted on create, Filter, Group, Sort"Indicates whether the assignment is active (true) or not (false). Defaults to false." API version 52.0 and later. Not createable and not updateable — the platform sets it. Filter on it; never try to write it.
IsRevokedboolean — Defaulted on create, Filter, Group, Sort, Update"Indicates whether the assignment was revoked (true) or not (false)." Present only when user access policies are enabled. API version 57.0 and later.
AssigneeIdreference (User) — Create, Filter, Group, SortNo Update. Retargeting an assignment to a different user is a delete plus an insert.
PermissionSetIdreference — Create, Filter, Group, Nillable, SortNo Update. Swapping which permission set is granted is a delete plus an insert.
PermissionSetGroupIdreference — Create, Filter, Group, Nillable, SortNo Update. API version 45.0 and later. One row grants the whole group.
LastCreatedByChangeId / LastDeletedByChangeIdreference (UserAccessChange)Populated only when user access policies are enabled. API version 57.0 and later. UserAccessChange.Source carries, "for example, UserAccessPolicyId."

The Object Reference's blanket instruction — "To update an assignment, delete an existing assignment and insert a new one" — is about the Create-only fields above. ExpirationDate is the exception: it is explicitly updateable, which is why extending an expiry is a one-field update, not a re-assignment. See references/gotchas.md Gotcha 2 for what the delete-and-insert route costs you.

Expiry closes one door, not the room

Expiry ends this assignment. It does not audit the rest of the user's access. A permission that is also on the profile, on a second permission set, or inside a permission set group the user is still assigned to survives the date unchanged. Time-boxing therefore only works when the elevated permission lives in exactly one place — which is the whole argument for a narrow, single-purpose elevation permission set rather than hanging an expiry on a broad job-function group.

What can and cannot carry an expiration date

Grant mechanismExpiry supported?Evidence
Permission set assignmentYesPermissionSetAssignment.ExpirationDate, API version 52.0 and later
Permission set group assignmentYesSame field, same row — the description names "the permission set or permission set group"
Permission set license assignmentNoPermissionSetLicenseAssign has no ExpirationDate, and its supported calls are create(), delete(), describeSObjects(), query(), retrieve() — no update()
ProfileNoA profile is a lookup on the User record; there is no assignment object to hang a date on
Muting permission setNoMutingPermissionSet "is used in conjunction with PermissionSetGroup" — it is a component of a group, never assigned to a user, so there is no assignment row
Public group or queue membershipNoGroupMember has exactly two fields, GroupId and UserOrGroupId, and does not support update()
Package licenseNoNo expiration field on the assignment; user access policies can Grant or Revoke a PackageLicense, but with no date attached

Turning on the Setup control

The toggle lives on the User Management Settings Setup page, and it is deployable. UserManagementSettings values are stored in UserManagement.settings; in the package manifest all org settings types are referenced by the name Settings.

<!-- force-app/main/default/settings/UserManagement.settings-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<UserManagementSettings xmlns="http://soap.sforce.com/2006/04/metadata">
    <psaExpirationUIEnabled>true</psaExpirationUIEnabled>
    <userAccessPoliciesEnabled>true</userAccessPoliciesEnabled>
</UserManagementSettings>
<!-- manifest/package.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>UserManagement</members>
        <name>Settings</name>
    </types>
    <version>67.0</version>
</Package>

There is only one settings file per settings component, so retrieve the org's existing UserManagement.settings and add the element rather than deploying the two-line file above over the top of whatever else is configured there. userAccessPoliciesEnabled is available in API version 58.0 and later and also defaults to false; include it only if the org has actually adopted policies.

User Access Policies assign; they do not hold the clock

A user access policy is the supported no-code way to make assignments from user criteria — UserAccessPolicy is available in API version 57.0 and later and requires the Manage User Access Policies permission. But the action shape is deliberately small. UserAccessPolicyAction has exactly three fields: action (Grant or Revoke), target, and type (Group, PackageLicense, PermissionSet, PermissionSetGroup, PermissionSetLicense, Queue). There is no expiration attribute anywhere in the policy metadata.

The consequence is architectural: a policy can express "everyone in this role gets this group" and "everyone who leaves this role loses it," but it cannot express "for ninety days." Criteria-driven revocation and date-driven expiry are two different controls. Use admin/user-access-policies for the first; use ExpirationDate for the second. A policy-driven revoke sets IsRevoked = true and leaves the row behind rather than deleting it, so ALL ROWS is required to see the revoked population — a date expiry and a policy revocation are two different terminal states and one audit query will not find both.


Common Patterns

Pattern: Single-Purpose Elevation Permission Set

When to use: A named individual needs one dangerous capability — Manage Users, an integration-user permission, an object's Delete — for a bounded period, and the org's job-function permission sets are too broad to expire safely.

How it works:

  1. Build a permission set that contains only the elevated capability. Name it for the elevation, not for the person.
  2. Assign it to the individual with ExpirationDate set to the agreed end instant.
  3. Verify the capability is not simultaneously granted by the assignee's profile or by any other assigned permission set — if it is, the expiry buys nothing.
  4. Record the approver and the business justification alongside the assignment; the platform stores the date, not the reason.
  5. Let the date pass. Do not build a job to remove the assignment.

Why not hang the expiry on the job-function group: the group carries the user's normal, permanent access as well. When it expires, the user loses their day job, not just the elevation. Expiring a group assignment is correct only when the entire group is the temporary grant — a project role, a migration cutover role, an audit-window reader.

Pattern: Renewal by Update, Not Re-Assignment

When to use: A contractor's engagement is extended and the assignment is close to expiring, or already inactive.

How it works: ExpirationDate is updateable. Query the assignment by AssigneeId and PermissionSetId, then update the single field with the new instant.

// Extend one contractor's elevation by another quarter.
PermissionSetAssignment psa = [
    SELECT Id, ExpirationDate, IsActive
    FROM PermissionSetAssignment
    WHERE AssigneeId = :contractorId
      AND PermissionSet.Name = 'PS_Temp_ManageUsers'
    LIMIT 1
];
psa.ExpirationDate = newEndInstant;
update psa;

Why not delete and re-insert: a new row resets the assignment's creation audit and breaks any reporting keyed on the original grant. It also risks a window where the user has no assignment at all if the insert fails. Re-insert only when the target changes — a different user, a different permission set, a different group — because those fields are Create-only.

Pattern: Expiry-Aware Access Review

When to use: A quarterly or SOX-style access review must show which elevations are live, which are about to lapse, and which already have.

How it works: three queries, run together, because no single one covers the population.

-- 1. Live elevations with a scheduled end, next 30 days first.
--    IsActive = true is load-bearing: without it, NEXT_N_DAYS:30 also
--    returns every assignment that already lapsed.
SELECT Id, Assignee.Name, Assignee.Username, PermissionSet.Name,
       PermissionSetGroup.DeveloperName, ExpirationDate, IsActive
FROM PermissionSetAssignment
WHERE IsActive = true
  AND ExpirationDate != NULL
  AND ExpirationDate <= NEXT_N_DAYS:30
ORDER BY ExpirationDate ASC

-- 2. Standing privilege: elevation permission sets assigned with NO end date.
SELECT Id, Assignee.Name, PermissionSet.Name, ExpirationDate
FROM PermissionSetAssignment
WHERE ExpirationDate = NULL
  AND PermissionSet.Name LIKE 'PS_Temp_%'

-- 3. Policy-revoked rows, which are NOT deleted and NOT returned without ALL ROWS.
SELECT Id, ExpirationDate, Assignee.Name, PermissionSet.Name
FROM PermissionSetAssignment
WHERE IsRevoked = true ALL ROWS

Query 2 is the one that finds the failure. Every org that adopts time-boxing accumulates elevation permission sets assigned without a date, because the API path makes ExpirationDate optional and nillable and nothing rejects the insert.


Decision Guidance

SituationRecommended approachReason
One person needs one capability for a fixed windowSingle-purpose permission set + ExpirationDateNarrow grant means expiry actually revokes something
A whole project role is temporaryPermission set group assignment + ExpirationDateOne row expires the entire aggregate grant
Access should end when a user attribute changes, not on a dateUser access policy with a Revoke actionPolicy actions have no date; criteria are the trigger. See admin/user-access-policies
Access must end the moment the session doesSession-based permission setTighter than any wall-clock date. Note: session-based permission sets included in a permission set group do not require activation for users assigned to the group
The elevation depends on a permission set licenseNo expiry exists — build a manual removal stepPermissionSetLicenseAssign has no ExpirationDate and no update()
The permission is also on the profileFix the profile first, then time-boxExpiry cannot remove a grant it does not own
An engagement is extendedupdate the ExpirationDate fieldThe field is updateable; delete-and-insert is only needed for Create-only fields
Reporting must show elevations that already endedFilter IsActive = false and add ALL ROWSThe state, not the row's existence, is what changes. ALL ROWS is correct whether or not lapsed rows are hidden — see references/gotchas.md Gotcha 3
You are tempted to write a nightly job that unassignsUse ExpirationDateA scheduled job fails silently and leaves standing privilege behind — the exact outcome the control exists to prevent

Step-by-step instructions for an AI agent or practitioner working on this task:

  1. Confirm the org can express an expiry. Check UserManagementSettings for psaExpirationUIEnabled. If it is false and the admin will work through Setup, turn it on first — the metadata snippet above is deployable. Also record whether userAccessPoliciesEnabled is on, because it changes which fields exist on the assignment.
  2. Locate every grant of the target permission. Enumerate the assignee's profile, all assigned permission sets, and all assigned permission set groups. If the elevated permission appears more than once, either narrow the grant or accept that the expiry is decorative and say so out loud.
  3. Choose the unit of expiry. A single-purpose permission set for an individual capability; a permission set group only when the entire group is the temporary grant. Never attach an expiry to a permission set that also carries the user's day-to-day access.
  4. Set ExpirationDate as an explicit instant. The field is a dateTime. Through the API, write a full timestamp so the cutoff is unambiguous; through Setup, confirm what clock time and time zone the org's assignment screen applies before you promise an auditor a cutoff time. Do not assume the two paths resolve to the same moment.
  5. Record the decision outside the platform record. The assignment stores a date, not an approver or a justification. Capture both in the request ticket or in the workbook produced by templates/permission-set-expiration-template.md.
  6. Verify the expiry landed and schedule the review. Re-query the assignment and confirm ExpirationDate is populated. Run the three access-review queries above and confirm the new row appears in query 1 and not in query 2. Read references/gotchas.md before writing any reporting that assumes an expired assignment disappears.
  7. Run the checker script — python3 skills/admin/permission-set-expiration/scripts/check_permission_set_expiration.py --plan <plan.json> to validate the assignment plan's field shape before anyone loads it.

Review Checklist

Run through these before marking a time-boxed elevation complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviours that cause real production problems. references/gotchas.md carries ten of these in full, with the mechanism and the recovery; items 1-4 and 6 below summarise five of them, and item 5 is stated in full here.

  1. The Setup control is off by default — psaExpirationUIEnabled defaults to false, so an admin can work through the assignment screens and never see an expiration option while the API field has existed since API version 52.0.
  2. The two Setup paths are not equivalent — the Security Guide's expiration step appears in the permission-set-side Manage Assignments → Add Assignments flow, not in the user-record-side Edit Assignments procedure. Admins who always start from the user record will not find it.
  3. Liveness is a state, not the row's existence — IsActive is the platform-owned, read-only flag that carries it. Any report or integration that treats "an assignment row exists" as "the user has access" reads a lapsed elevation as live.
  4. Setup Audit Trail still labels the expiration-date entry beta — in the Summer '26 Security Guide the tracked change reads "Permission set (or group) changes to the assignment expiration date (beta)". Confirm the entry appears in your org before relying on it as audit evidence.
  5. The bulk Setup flow caps removal at 1,000 users at a time — relevant when an expiry experiment has to be unwound by hand across a large population.
  6. Guest, integration, and system users are not in the Manage Assignments UI — "Certain types of users, such as guest, Self-Service, integration, and system users, aren't available in the Manage Assignments page. To view or manage these users, use the PermissionSetAssignment API object." Time-boxing an integration user's elevation is an API-only operation.

Output Artifacts

ArtifactDescription
Time-boxed assignment planPermission set or group, assignee, ExpirationDate instant, approver, justification, review date
Access-review query setThe three SOQL queries covering expiring-soon, no-end-date, and policy-revoked populations
UserManagement.settings-meta.xmlDeployable settings file enabling psaExpirationUIEnabled
Renewal procedureSingle-field update on ExpirationDate, with the delete-and-insert cases called out
Checker script outputField-shape validation of the assignment plan before it is loaded

Package files