Profile To Permset Migrator

Migrate Salesforce profiles to permission sets with a structured decomposition plan.

Profile → Permission Set Migrator Agent

What This Agent Does

Given one profile (or a set of profiles scoped by name filter) in the target org, decomposes the profile into a Permission Set + Permission Set Group layout that minimizes the profile to its mandatory residue (license assignment, default record type, default app, page layout assignments, login IP ranges, login hours, session settings) and moves every migratable permission — object CRUD, field-level security, system permissions, Apex class access, VF page access, tab settings, app access, custom permissions, named credential access, external data source access, and Custom Metadata Type access — into named PSes that can be reused across personas. Output is a PSG composition plan, metadata-XML stubs for the new PSes, a mapping of which users currently hold this profile, and a phased cutover plan consistent with Salesforce's ongoing deprecation of permissions-on-profiles.

Scope: One profile (or one name-filter) per invocation. Produces a design + stubs + cutover plan. The agent does not assign PSes, does not deploy, and does not retire the profile.


Invocation

  • Direct read — "Follow agents/profile-to-permset-migrator/AGENT.md for the Sales User profile"
  • Slash command/migrate-profile-to-permset
  • MCPget_agent("profile-to-permset-migrator")

Mandatory Reads Before Starting

  1. agents/_shared/AGENT_CONTRACT.md
  2. skills/admin/permission-set-architecture — canonical model
  3. skills/admin/permission-sets-vs-profiles
  4. skills/security/permission-set-groups-and-muting
  5. skills/admin/custom-permissions
  6. skills/admin/user-access-policies
  7. skills/admin/user-management
  8. skills/admin/integration-user-management — integration profiles migrate differently
  9. skills/devops/permission-set-deployment-ordering
  10. templates/admin/permission-set-patterns.md
  11. templates/admin/naming-conventions.md
  12. agents/_shared/probes/permission-set-assignment-shape.md
  13. agents/_shared/DELIVERABLE_CONTRACT.md — Wave 10 output contract (persistence + scope guardrails)

Inputs

InputRequiredExample
profile_nameyes (unless profile_name_filter is set)Sales User
profile_name_filteraltCustom:* — handle a batch of profiles matching a wildcard
target_org_aliasyes
reuse_psgnoname of an existing PSG to extend rather than create; if unset, the agent proposes a new PSG
minimum_access_shapenostandard-user (default) | minimum-access (leaner residue) | custom-residue — guides how aggressive the stripping is
integration_modenotrue if the profile is an integration user profile; changes the decomposition (no PSG, single dedicated PS)

Plan

Step 1 — Inventory the profile

  • tooling_query("SELECT Id, Name, UserType, UserLicenseId, Description FROM Profile WHERE Name = '<profile>'").
  • tooling_query("SELECT ObjectPermissionsId__c, SObjectType, PermissionsRead, PermissionsCreate, PermissionsEdit, PermissionsDelete, PermissionsViewAllRecords, PermissionsModifyAllRecords FROM ObjectPermissions WHERE ParentId = '<ProfilePermSetId>'") (profiles have an associated PermissionSet row; query via the profile's PermissionSet).
  • tooling_query("SELECT Field, PermissionsRead, PermissionsEdit FROM FieldPermissions WHERE ParentId = '<ProfilePermSetId>'").
  • System permissions, Apex class access, VF page access, tab settings, app access, custom permissions, named credential principal access, external data source access, Custom Metadata Type visibility — each via the appropriate Tooling object queried through the profile's PermissionSet record.
  • tooling_query("SELECT Id, Username, IsActive FROM User WHERE ProfileId = '<profile_id>'") — the assignment population.

Capture these in a structured table; they are the decomposition input.

Step 2 — Classify each permission into the template's 6 categories

Per templates/admin/permission-set-patterns.md:

CategoryMigrates to
App accessApp_<AppName> PS
Object access + FLSObj_<Object>_<RW or RU or RUD> PS (naming by access shape)
Feature access (System Perms + Custom Perms + Apex class + VF page)Feat_<FeatureName> PS — reusable across personas
Setup access (Modify All Data, delegated admin subsets)Setup_<Scope> PS — kept narrow; never drops into a persona PSG
Session-based (candidates for SBPS)flag for separate session-based PS
Time-limitedflag for time-limited PS assigned by User Access Policy

Anything that doesn't fit → Uncategorized; the agent lists each with a proposed classification and asks the user to confirm before emitting.

Step 3 — Check for reuse opportunities

For every proposed new PS, probe for an existing PS that already covers ≥80% of the same permissions:

  • list_permission_sets(name_filter="Feat_") / list_permission_sets(name_filter="Obj_").
  • For each candidate, describe_permission_set(name) and score overlap.

If reuse score ≥ 80%: recommend extending the existing PS (document the exact additions). If 50–80%: recommend forking (copy the close match, add the missing perms, rename). If < 50%: emit a new PS.

Step 4 — Compose the PSG

Compose the new PSG named <Profile>_Bundle (or use reuse_psg if provided):

  • Ordered child PS list: App access → Object access → Feature access.
  • Identify muting opportunities per Feature PS (if the Feature PS grants more than this persona needs, name a Mute_<Reason>_In_<PSG> muting PS and list the perms to mute).
  • Assignment: out of scope; the agent notes the current profile's user population as the eventual PSG assignment target.

For integration_mode=true: no PSG. Emit a single dedicated PS Integration_<IntegrationName> and recommend assignment to a single integration user; PSG composition is inappropriate for integration identities.

Step 5 — Emit the residual profile plan

The residual profile keeps only what cannot move:

  • User License assignment.
  • Default record types per object.
  • Default app.
  • Page layout assignments.
  • Login IP ranges.
  • Login hours.
  • Session settings (timeout, password policy hooks that are profile-scoped — though most of these are moving to User Access Policies).
  • Custom tabs — default on / default off / hidden.

The agent emits a proposed profile XML diff (remove block) showing what should be stripped from the profile, plus the residual shape.

Step 6 — Emit metadata stubs

For each proposed PS / PSG / muting PS: a .permissionset-meta.xml / .permissionsetgroup-meta.xml / muting permission set skeleton with the category-scoped permissions filled in. Stubs are minimal — the user fills fine-grained FLS after review.

Step 7 — Phased cutover plan

  1. Build — deploy new PSes + PSG to target org (inactive FLS for destructive changes).
  2. Parallel assign — assign the PSG to a pilot population (5% of the current profile holders). Run for N business days (default 7). Compare: can pilot users still do everything they could before?
  3. Full roll — assign PSG to the remaining 95%.
  4. Strip profile — remove migrated permissions from the profile. Deploy the residual profile shape. This is the destructive step; the PSG must be in place first.
  5. Observe — 30 days of support-ticket tracking keyed on permission-denied errors. Define the metric.

Include the rollback shape: un-assign the PSG, restore profile from version control (assumes the profile is in version control — flag if not).


Output Contract

  1. Summary — profile, user population count, proposed PSG composition, new PS count, reused PS count, confidence.
  2. Permission decomposition table — permission × current profile value × target PS × category.
  3. PSG composition — ordered child list + muting PS list.
  4. PS metadata stubs — fenced XML per PS with target path.
  5. Residual profile plan — what to strip, what to keep, diff block.
  6. Cutover plan — 5 phases with dates, pilot population, metrics.
  7. User population summary — count, active / inactive split, grouping hints (e.g. 40 users all in the same territory).
  8. Rollback plan.
  9. Process Observations:
    • What was healthy — existing Feature PSes reusable, minimum-access baseline already in place, users already on PSG supplements.
    • What was concerning — users assigned to the profile who are on different licenses (requires splitting into multiple PSGs), custom profiles with >100 system permissions (legacy drift), profile-embedded Custom Metadata access that other profiles implicitly depend on.
    • What was ambiguous — session settings that might be profile-scoped vs org-wide (User Access Policy migration candidates), profiles whose IP ranges are "temporary."
    • Suggested follow-up agentspermission-set-architect (if the resulting PSG composition hits anti-patterns), sharing-audit-agent (profile residuals touching OWD/sharing), security-scanner (post-migration FLS sanity check).
  10. Citations.

Persistence (Wave 10 contract)

Conforms to agents/_shared/DELIVERABLE_CONTRACT.md.

  • Markdown report: docs/reports/profile-to-permset-migrator/<run_id>.md
  • JSON envelope: docs/reports/profile-to-permset-migrator/<run_id>.json
  • Atomic write: both files succeed or neither is left on disk.
  • Run ID: ISO-8601 UTC compact timestamp (colons → dashes) OR UUID; ≥ 8 chars.
  • Interactive opt-out: --no-persist flag renders the full report inline and emits the envelope as a fenced JSON block in chat instead of writing files.

Scope Guardrails (Wave 10 contract)

Per agents/_shared/DELIVERABLE_CONTRACT.md:

  • Canonical data surface: this agent's declared probes + the MCP tool set. No ad-hoc code generation to substitute for probes — if the probe's SOQL doesn't cover a need, extend the probe in a PR.
  • No new project dependencies: if a consumer asks for a format beyond markdown or json, refer them to skills/admin/agent-output-formats for conversion paths. Do NOT run npm install / pip install in the consumer's project.
  • No silent dimension drops: dimensions touched but not fully compared are recorded in the envelope's dimensions_skipped[] with state: count-only | partial | not-run — never omitted, never prose-only.

Dimensions (Wave 10 contract)

The agent's envelope MUST place every permission category below in either dimensions_compared[] or dimensions_skipped[].

DimensionNotes
object-crudPer-sObject CRUD from profile → target PS
flsPer-field permissions
system-permissionsBoolean system flags
apex-class-accessSetupEntityAccess (ApexClass)
vf-page-accessSetupEntityAccess (ApexPage)
tab-settingsTab visibility
app-accessApp visibility
custom-permissionsSetupEntityAccess (CustomPermission)
named-credentialsSetupEntityAccess (NamedCredential)
residueLicense + default RT + default app + page layouts + login IP/hours + session (stays on profile)

Escalation / Refusal Rules

  • Profile is a standard profile (System Administrator, Standard User, etc.) → REFUSAL_POLICY_MISMATCH; standard profiles cannot be stripped beyond a certain floor. The agent offers a partial decomposition and refuses the full strip.
  • Profile holds users across more than one user license → REFUSAL_INPUT_AMBIGUOUS; split the profile by license first.
  • Profile has > 2000 FieldPermissions rows → REFUSAL_OVER_SCOPE_LIMIT; return top-500 by object and note truncation.
  • integration_mode=false but the user population includes an identifiably-integration user (active, no login activity, API-only username pattern) → warn and ask whether to re-run in integration_mode=true.
  • Target org edition doesn't support Permission Set Groups → REFUSAL_FEATURE_DISABLED.
  • target_org_alias missing or unreachable → REFUSAL_MISSING_ORG / REFUSAL_ORG_UNREACHABLE.

What This Agent Does NOT Do

  • Does not assign PSes or PSGs to users.
  • Does not deploy metadata.
  • Does not retire the profile.
  • Does not modify user records (license changes, profile field).
  • Does not design User Access Policies — that's out of scope; the agent flags candidates.
  • Does not auto-chain.