Generate a SalesforceBackup permission set XML from live org metadata — ensuring your backup/restore integration user has access to every restorable object, field, and record type.
- Permishifizer 9000
permishifizer9000.py connects to an authenticated Salesforce org, introspects its full metadata surface, and writes a SalesforceBackup.permissionset-meta.xml file. When deployed and assigned to a user, this permission set grants the minimum access needed for comprehensive backup and restore operations across all standard and custom objects.
The permission set is always regenerated from scratch so it stays aligned with the current org schema — no manual upkeep required when objects, fields, or record types change.
| Permission Type | Scope | Access Granted |
|---|---|---|
objectPermissions |
All permissionable objects in the org | allowRead=true, allowCreate=true |
fieldPermissions |
All permissionable fields on those objects | readable=true; editable=true for all permissionable fields |
recordTypeVisibilities |
All record types on included objects (including PersonAccount) | visible=true |
userPermissions |
Configured user-level permissions in USER_PERMISSION_NAMES |
enabled=true |
The following categories are intentionally excluded because they are event streams, not restorable record data:
- Change Data Capture objects — API names ending in
ChangeEvent - Platform Event objects — API names ending in
__e - Predefined exclusions — any API names listed in the
EXCLUDED_OBJECT_API_NAMESset within the script - Compound component fields — component field APIs (for example
BillingStateCode) are skipped because they are not valid infieldPermissions; compatible non-component fields are used instead - Person Account alias fields —
Account.*__pcfields (mirrors of Contact custom fields) are skipped to avoid duplicate entries
| Requirement | Details |
|---|---|
| Python | 3.12 or later |
Salesforce CLI (sf) |
Installed and on PATH. Install guide |
| Authenticated org | At least one org authenticated via sf org login web or sf org login sfdx-url |
| User Permissions | The authenticated user requires: • API Enabled ( ApiEnabled) • Customize Application ( CustomizeApplication) • Manage Profiles and Permission Sets ( ManageProfilesPermissionsets) • Modify Metadata Through Metadata API Functions ( ModifyMetadata) |
Note: No third-party Python packages are required — the script uses only the standard library.
Clone the repository (or copy the script) into your project:
git clone <repository-url> permishifizer9000
cd permishifizer9000# Use the default target org (sf CLI default)
python permishifizer9000.py
# Recommended (explicit): specify an org alias
python permishifizer9000.py --target-org myOrgAlias
python permishifizer9000.py -o myOrgAlias
# Recommended (explicit): specify an org username
python permishifizer9000.py --target-org admin@example.com
# Convenience shorthand (also supported): positional target org
python permishifizer9000.py myOrgAlias
# Generate and automatically deploy the permission set
python permishifizer9000.py --target-org myOrgAlias --deploy
python permishifizer9000.py -o myOrgAlias -d
# Generate and deploy with a custom deploy wait window (minutes)
python permishifizer9000.py --target-org myOrgAlias --deploy --deploy-wait 20
python permishifizer9000.py -o myOrgAlias -d -w 20
# Custom output path using shorthand
python permishifizer9000.py -o myOrgAlias -f ./output/SalesforceBackup.permissionset-meta.xmlThe generated permission set is written to:
./force-app/main/default/permissionsets/SalesforceBackup.permissionset-meta.xml
Deploy it immediately:
sf project deploy start --ignore-conflicts --source-dir "force-app/main/default/permissionsets/SalesforceBackup.permissionset-meta.xml"Assign to a user:
sf org assign permset --name SalesforceBackup --target-org myOrgAliasusage: permishifizer9000.py [-h] [--target-org TARGET_ORG] [--output-file OUTPUT_FILE] [--deploy] [--deploy-wait DEPLOY_WAIT] [target_org]
positional arguments:
target_org Optional Salesforce org alias/username (convenience shorthand)
options:
-h, --help Show this help message and exit
--target-org TARGET_ORG, -o TARGET_ORG
Salesforce org alias/username (preferred for CI/CD; same meaning as positional target_org)
--output-file OUTPUT_FILE, -f OUTPUT_FILE
Output permission set XML path (default: force-app/main/default/permissionsets/SalesforceBackup.permissionset-meta.xml)
--deploy, -d Automatically deploy the generated permission set after writing it
--deploy-wait DEPLOY_WAIT, -w DEPLOY_WAIT
Minutes to wait for deployment completion when using --deploy (default: 10). Must be >= 1.
Short-flag note: this project maps -o to --target-org and -f to --output-file.
For automation and CI/CD, prefer the explicit --target-org flag for readability in pipeline logs and scripts. The positional argument remains supported as a convenience shorthand for interactive/local usage.
The script resolves the target org in the following priority order:
| Priority | Source | Example |
|---|---|---|
| 1 | --target-org flag |
python permishifizer9000.py --target-org myOrg |
| 2 | Positional argument | python permishifizer9000.py myOrg |
| 3 | SF_TARGET_ORG environment var |
export SF_TARGET_ORG=myOrg |
| 4 | Salesforce CLI default org | Set via sf config set target-org=myOrg |
Write the permission set to a different location:
python permishifizer9000.py --output-file ./output/BackupPerms.permissionset-meta.xml
python permishifizer9000.py -f ./output/BackupPerms.permissionset-meta.xmlTo deploy immediately after generation, add --deploy:
python permishifizer9000.py --target-org myOrg --deployWhen --deploy is used, the script runs sf project deploy start --ignore-conflicts for the generated permission set file.
Before any org/API processing starts, deploy mode performs a preflight validation and fails fast if either condition is not met:
sfdx-project.jsoncan be located at project root- The generated output file path is inside one of the
packageDirectories[].pathroots fromsfdx-project.json
This prevents long-running metadata generation when deployment cannot succeed due to project structure.
By default, the script waits up to 10 minutes for deployment completion. To change that (minimum 1 minute):
python permishifizer9000.py --target-org myOrg --deploy --deploy-wait 20Edit the EXCLUDED_OBJECT_API_NAMES set in the script to exclude specific objects from the generated permission set:
EXCLUDED_OBJECT_API_NAMES = {
"Task",
"Event",
"CustomObject__c",
"acme__Subscription__c",
}All excluded objects are removed from objectPermissions, fieldPermissions, and recordTypeVisibilities.
Some objects are valid for objectPermissions but are not returned by the PicklistValueInfo query that the script uses in Step 1. Salesforce handles these objects specially and omits them from the picklist. Edit the OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS set in the script to ensure they are included:
OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS = {"Task", "Event"}These names are merged into the Step 1 result set before the cross-reference against EntityDefinition, so they appear in the generated objectPermissions as long as they also exist as non-excluded objects in the org.
Some objects have permissionable fields that are not indexed in the EntityParticle Tooling API table the script queries in Step 3, or are filtered out of the EntityParticle scan because EntityDefinition.IsFlsEnabled = false even though they have permissionable custom fields (for example User, whose standard fields are governed by user-management permissions rather than the <fieldPermissions> framework). For these objects, the script falls back to querying FieldDefinition one object at a time. Edit the FIELD_QUERY_SUPPLEMENTAL_OBJECTS list in the script to add or remove objects that need this treatment:
FIELD_QUERY_SUPPLEMENTAL_OBJECTS = [
"EmailMessage",
"User",
]Fields returned for these objects are added to the generated fieldPermissions alongside the regular EntityParticle results.
A small number of fields are valid in fieldPermissions (and selectable in the Salesforce permission set UI) but are not returned by either EntityParticle or FieldDefinition — for example DisputeItemChargeBack.CardBrand. Add fully-qualified field API names to FIELD_PERMISSION_SUPPLEMENTAL_FIELDS in the script to inject them verbatim:
FIELD_PERMISSION_SUPPLEMENTAL_FIELDS = {
"DisputeItemChargeBack.CardBrand",
}Each entry must use the Object.Field format. Entries whose parent object is excluded (event objects, predefined exclusions) are skipped.
Edit the USER_PERMISSION_NAMES set in the script to add/remove generated userPermissions entries:
USER_PERMISSION_NAMES = {
"AllowViewEditConvertedLeads",
"ModifyAllData",
"ViewAllData",
}Depending on your backup/restore setup, additional permissions may be required. For guidance, see Salesforce Help: https://help.salesforce.com/s/articleView?id=platform.backup_recover_o_auth_user_perms.htm&type=5
Each entry generates:
<userPermissions>
<enabled>true</enabled>
<name>AllowViewEditConvertedLeads</name>
</userPermissions>The script produces a standard Salesforce metadata XML file. A condensed example:
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Grants object, field, and record type access for full-record backup/restore scope (excluding ChangeEvent and __e event objects).</description>
<hasActivationRequired>false</hasActivationRequired>
<label>Salesforce Backup</label>
<fieldPermissions>
<editable>true</editable>
<field>Account.Name</field>
<readable>true</readable>
</fieldPermissions>
<!-- ... more fieldPermissions ... -->
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>false</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Account</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
<!-- ... more objectPermissions ... -->
<recordTypeVisibilities>
<recordType>Account.MyRecordType</recordType>
<visible>true</visible>
</recordTypeVisibilities>
<!-- ... more recordTypeVisibilities ... -->
<userPermissions>
<enabled>true</enabled>
<name>AllowViewEditConvertedLeads</name>
</userPermissions>
<!-- ... more userPermissions ... -->
</PermissionSet>During execution the script prints a progress summary:
Connecting to Salesforce org...
Querying org metadata (this may take several minutes for large orgs)...
Step 1/4 Fetching permissionable object list from PicklistValueInfo...
Found 412 permissionable objects.
Step 2/4 Fetching object list from EntityDefinition...
Found 1247 objects (892 with FLS enabled).
Excluded 38 event objects, 0 predefined objects, 355 non-FLS objects.
Using 374 objects for objectPermissions.
Step 3/4 Querying EntityParticle in 25 batch(es) of up to 50 objects...
[ 1/ 25] Account – Contact
...
Total permissionable fields: 8342
Step 4/4 Fetching record types via Metadata API...
Found 67 record types from Metadata API.
Using 54 record types in scope.
Permission set written: .../SalesforceBackup.permissionset-meta.xml
objectPermissions: 374
fieldPermissions: 8342
recordTypeVisibilities: 54
userPermissions: 52
Done!
Deploy with: sf project deploy start --ignore-conflicts --source-dir "..."
The script is designed for automated pipelines. It has zero Python dependencies beyond the standard library and exits with meaningful error codes.
All CI examples below use python permishifizer9000.py --deploy, which already invokes sf project deploy start --ignore-conflicts internally.
If you switch to a direct CLI deploy step, use sf project deploy start --ignore-conflicts --source-dir "...".
| Exit Code | Meaning |
|---|---|
0 |
Success |
1 |
Script error (auth, network, etc.) |
130 |
Interrupted by user (Ctrl+C) |
trigger: none
schedules:
- cron: "0 6 * * 1"
displayName: Weekly Monday 06:00 UTC
branches:
include:
- main
pool:
vmImage: "ubuntu-latest"
steps:
- task: UseNode@1
inputs:
version: "20.x"
- task: UsePythonVersion@0
inputs:
versionSpec: "3.12"
- script: npm install -g @salesforce/cli
displayName: Install Salesforce CLI
- script: |
echo "$(SF_AUTH_URL)" > auth_url.txt
sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
rm auth_url.txt
displayName: Authenticate to Salesforce
- script: python permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
displayName: Generate and deploy permission set
- script: sf org assign permset --name SalesforceBackup --target-org ci-org
displayName: Assign permission setname: Generate & Deploy Backup Permission Set
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1" # Weekly on Monday at 06:00 UTC
jobs:
generate-backup-perms:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli
- name: Authenticate to Salesforce
run: |
echo "${{ secrets.SF_AUTH_URL }}" > auth_url.txt
sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
rm auth_url.txt
- name: Generate and deploy permission set
run: python permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
- name: Assign permission set
run: |
sf org assign permset \
--name SalesforceBackup \
--target-org ci-orggenerate-backup-perms:
image: node:20
stage: deploy
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "web"
before_script:
- npm install -g @salesforce/cli
- apt-get update && apt-get install -y python3
- echo "$SF_AUTH_URL" > auth_url.txt
- sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
- rm auth_url.txt
script:
- python3 permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
- sf org assign permset --name SalesforceBackup --target-org ci-orgpipelines:
custom:
generate-backup-perms:
- step:
name: Generate & Deploy Backup Permission Set
image: node:20
script:
- npm install -g @salesforce/cli
- apt-get update && apt-get install -y python3
- echo "$SF_AUTH_URL" > auth_url.txt
- sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
- rm auth_url.txt
- python3 permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
- sf org assign permset --name SalesforceBackup --target-org ci-orgpipeline {
agent any
triggers {
cron('H 6 * * 1') // Weekly on Monday
}
environment {
SF_AUTH_URL = credentials('sf-auth-url')
}
stages {
stage('Setup') {
steps {
sh 'npm install -g @salesforce/cli'
}
}
stage('Authenticate') {
steps {
sh '''
echo "$SF_AUTH_URL" > auth_url.txt
sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
rm auth_url.txt
'''
}
}
stage('Generate') {
steps {
sh 'python3 permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10'
}
}
stage('Assign') {
steps {
sh 'sf org assign permset --name SalesforceBackup --target-org ci-org'
}
}
}
}If you prefer environment-based org targeting without explicit --target-org flags:
export SF_TARGET_ORG="ci-org"
python permishifizer9000.pyThe script picks up SF_TARGET_ORG automatically when no positional or flag argument is provided.
The script executes four sequential phases against the target org's APIs:
┌──────────────────────────────────────────────────────────────────────┐
│ Step 1 — PicklistValueInfo (REST API) │
│ Fetches all permissionable object names from the ObjectPermissions │
│ SobjectType picklist. Uses ordered keyset pagination (`ORDER BY` + │
│ `Value > lastValue`) in pages of 2,000 rows. Merges in any names │
│ from `OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS` that Salesforce omits │
│ from the picklist (e.g. Task, Event). │
├──────────────────────────────────────────────────────────────────────┤
│ Step 2 — EntityDefinition (Tooling API) │
│ Fetches all non-deprecated object names and FLS status via ordered │
│ keyset paging (`QualifiedApiName > lastValue`), excludes event, │
│ predefined, and non-FLS objects, and cross-references Step 1 to keep │
│ only concrete objects in scope for objectPermissions. │
├──────────────────────────────────────────────────────────────────────┤
│ Step 3 — EntityParticle (Tooling API) │
│ Queries all permissionable fields in batches of 50 objects. │
│ Marks included fields as editable in the generated permission set. │
│ Excludes compound component fields (IsComponent=true), Person │
│ Account alias fields (Account.*__pc), and any fields belonging to │
│ predefined-excluded objects as a final guard. Objects listed in │
│ `FIELD_QUERY_SUPPLEMENTAL_OBJECTS` are absent from EntityParticle; │
│ their fields are fetched individually via FieldDefinition instead. │
├──────────────────────────────────────────────────────────────────────┤
│ Step 4 — Metadata API (SOAP listMetadata) │
│ Retrieves all RecordType full names. Filters to objects in scope │
│ and normalizes PersonAccount record types. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
Writes SalesforceBackup.permissionset-meta.xml
- Full regeneration — The XML file is always written from scratch, not patched, ensuring the output always reflects the current org state.
- Keyset pagination — Large object surfaces are fetched with deterministic, ordered pagination (
ORDER BY+> lastValue) inQUERY_PAGE_SIZEchunks, reducing query count and complexity. - Permissionable-first field access — Any field returned as
IsPermissionable=trueis emitted withreadable=trueandeditable=trueto maximize backup/restore coverage, including formula and other non-writable field types. - Compound compatibility guard —
EntityParticle.IsComponent=truefields are excluded so generatedfieldPermissionsonly contain valid field names Salesforce accepts during deployment. - PersonAccount normalization —
Account.PersonAccountrecord types are normalized toPersonAccount.PersonAccountas required by permission set XML format. - No external dependencies — Uses only the Python standard library (
urllib,xml.etree,json,subprocess, etc.) to simplify CI/CD setup.
| Symptom | Cause | Fix |
|---|---|---|
Error: Salesforce CLI (sf) was not runnable |
sf not installed or not on PATH |
Install the Salesforce CLI and restart your shell |
Error: Could not parse org display output |
Org not authenticated or token expired | Re-authenticate: sf org login web --alias myOrg |
Error: \sf org auth show-access-token --json` failed` |
Salesforce CLI is outdated and lacks the show-access-token subcommand |
Update the CLI: sf update (or reinstall from https://developer.salesforce.com/tools/salesforcecli) |
Error: HTTP 401 during REST query |
Access token expired mid-run | Re-authenticate and re-run |
Error: No objects returned |
Org connectivity issue or insufficient permissions | Verify the authenticated user has API and metadata access |
Error: Timeout after 90s |
Slow org or network | Retry; consider running closer to the Salesforce instance |
Error: Conflicting target org values |
Both positional and --target-org provided with different values |
Use one or the other, not both with different values |
Error: Python 3.12+ is required |
Older Python version detected | Upgrade Python to 3.12 or later |
Error: --deploy requires sfdx-project.json at the project root |
Deploy mode could not find sfdx-project.json from output path/cwd/script directory |
Run from a Salesforce DX project or provide an output path inside that project |
Error: --deploy requires the generated metadata file to be within a configured package directory |
--output-file is outside all packageDirectories[].path entries in sfdx-project.json |
Use an output path under a configured package directory or update packageDirectories |
| Script runs but output has fewer objects than expected | Objects may be in EXCLUDED_OBJECT_API_NAMES or are event types |
Review exclusions in the script; check console output for exclusion counts |
These constants can be modified at the top of permishifizer9000.py:
| Constant | Default | Description |
|---|---|---|
PERMSET_NAME |
"SalesforceBackup" |
API name of the generated permission set |
PERMSET_LABEL |
"Salesforce Backup" |
Display label of the permission set |
PERMSET_DESC |
(see source) | Description embedded in the permission set XML |
DEFAULT_PERMSET_FILE |
./force-app/main/.../SalesforceBackup.permissionset-meta.xml |
Default output path |
EXCLUDED_OBJECT_API_NAMES |
set() (empty) |
Set of object API names to exclude from objectPermissions, fieldPermissions, and recordTypeVisibilities (does not affect userPermissions) |
OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS |
{"Task", "Event"} |
Objects valid for objectPermissions but missing from the PicklistValueInfo query; merged into the Step 1 result set before cross-referencing |
FIELD_QUERY_SUPPLEMENTAL_OBJECTS |
["EmailMessage", "User"] |
Objects whose permissionable fields are not indexed in EntityParticle (or whose parent object reports IsFlsEnabled = false, e.g. User); their fields are fetched individually via FieldDefinition at the end of Step 3 |
FIELD_PERMISSION_SUPPLEMENTAL_FIELDS |
{"DisputeItemChargeBack.CardBrand"} |
Fully-qualified field API names (Object.Field) injected verbatim into fieldPermissions; use for fields valid in permission sets but absent from both EntityParticle and FieldDefinition |
USER_PERMISSION_NAMES |
(see source) | Set of user permission API names emitted as userPermissions entries |
BATCH_SIZE |
50 |
Number of objects per EntityParticle query batch |
QUERY_PAGE_SIZE |
2000 |
Page size for PicklistValueInfo and EntityDefinition keyset pagination |
HTTP_TIMEOUT_SECONDS |
90 |
Timeout for each HTTP request to Salesforce |
SF_COMMAND_TIMEOUT_SECONDS |
120 |
Timeout for Salesforce CLI subprocess calls |
DEPLOY_TIMEOUT_BUFFER_SECONDS |
300 |
Extra seconds added to deploy_wait × 60 for the deploy subprocess timeout |
MIN_PYTHON_VERSION |
(3, 12) |
Minimum required Python version; script exits if the runtime is older |
TARGET_ORG_ENV_VAR |
"SF_TARGET_ORG" |
Environment variable checked for target org fallback |