Add, move, or remove fields on page layouts using a YAML file or single-operation flags.
USAGE
$ sf migrate layout edit -t <value> [--json] [--flags-dir <value>] [-f <value>] [--field <value>] [--remove]
[--at-top-of <value>] [--at-bottom-of <value>] [--layouts <value>...] [--read-only] [--required] [--api-version
<value>]
FLAGS
-f, --file=<value> Path to a YAML layout edit specification (`upsert` / `remove`).
-t, --target-org=<value> (required) Username or alias of the org to retrieve from and deploy to.
--api-version=<value> Override the api version used for api requests made by this command
--at-bottom-of=<value> Place the field immediately below this existing `Object.Field` (single-operation mode).
--at-top-of=<value> Place the field immediately above this existing `Object.Field` (single-operation mode).
--field=<value> Field API name as `Object.Field` (single-operation mode; not used with `--file`).
--layouts=<value>... Limit to these layout FullName values (repeat flag or use multiple times). Default: all
layouts for the object.
--[no-]read-only Set layout item behavior to Readonly (upsert / single-operation upsert only).
--remove Remove this field from the selected layouts (single-operation mode). Cannot be combined
with `--at-top-of` / `--at-bottom-of`.
--[no-]required Set layout item behavior to Required (upsert / single-operation upsert only).
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Add, move, or remove fields on page layouts using a YAML file or single-operation flags.
Lists layouts **only** via the **Tooling API** (no Metadata `listMetadata` fallback). First resolve
**`Layout.TableEnumOrId`** with `SELECT DurableId FROM EntityDefinition WHERE QualifiedApiName = '<ObjectApiName>'` —
for **standard** objects `DurableId` is the same as the API name (e.g. `Account`); for **custom** objects it is the
entity id (e.g. `01I…`). Then `SELECT Id, Name FROM Layout WHERE TableEnumOrId = '<DurableId>'`. Layout metadata
**FullName** values are built as `<ObjectApiName>-<Name>` (Tooling cannot return `FullName` when multiple layout rows
match in one query). Failures: unknown object (`UnknownObjectApiName`), Tooling errors (`LayoutListError`), or no
layouts (`NoLayoutsForObject`). Retrieves **Layout** metadata with `@salesforce/source-deploy-retrieve` into a
temporary directory, edits `layoutSections` XML, then deploys when something changed.
If a **reference field** for placement is not on a layout, that layout is skipped with a **warning** (no deploy for
that file).
Detail **layoutSections** only; related lists and other regions are not searched for the reference.
EXAMPLES
YAML specification:
$ sf migrate layout edit -f layouts.yaml -t myorg
Place `Contact.Custom__c` immediately below `Contact.Email` on all Contact layouts:
$ sf migrate layout edit -t myorg --field Contact.Custom__c --at-bottom-of Contact.Email
Remove a field from named layouts only:
$ sf migrate layout edit -t myorg --field Contact.Custom__c --remove --layouts "Contact-Contact Layout"
See code: src/commands/migrate/layout/edit.ts
Create, update, or remove FieldPermissions (FLS read/edit) on one or more target orgs from a YAML specification.
USAGE
$ sf migrate permset assign -f <value> -t <value>... [--json] [--flags-dir <value>] [--include-profile-owned] [--strict]
[--api-version <value>]
FLAGS
-f, --file=<value> (required) Path to the YAML assign specification file.
-t, --target-org=<value>... (required) Username or alias of an org to update (repeat for multiple targets).
--api-version=<value> Override the api version used for api requests made by this command
--include-profile-owned Include permission sets owned by profiles (IsOwnedByProfile = true).
--strict Fail if any object or field from the YAML is missing on the target org. When omitted,
missing target fields/objects are skipped with warnings.
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Create, update, or remove FieldPermissions (FLS read/edit) on one or more target orgs from a YAML specification.
Reads a YAML file that defines **PermsetGroups** (named groups of permission set API names), an **Upsert** section
(grant read/edit via group references), and a **Remove** section (revoke read/edit). Each of Upsert and Remove is a
list of entries; each entry is either **Objects** (nested object and field names) or **Fields** (full `Object.Field`
API names).
Preflight validation uses the Tooling API (`EntityDefinition`, `FieldDefinition`) on each target org. By default,
objects or fields missing on a target are skipped with warnings. Use **--strict** to fail instead.
**Upsert** merges grants onto the current org state (OR for read/edit). Granting edit forces read on for that row.
**Remove** is applied after Upsert on the merged state: revoking read also clears edit. If both read and edit end up
false, the FieldPermissions row is deleted.
By default, only permission sets with `IsOwnedByProfile = false` are considered. Use **--include-profile-owned** to
include profile-owned (shadow) permission sets.
EXAMPLES
Apply `spec.yaml` to two orgs:
$ sf migrate permset assign --file spec.yaml -t sandbox -t prod
Strict validation and include profile-owned permission sets:
$ sf migrate permset assign -f ./perms/spec.yaml -t myorg --strict --include-profile-owned
See code: src/commands/migrate/permset/assign.ts
Copy FieldPermissions and/or ObjectPermissions from a source org to a target org for matching permission sets.
USAGE
$ sf migrate permset copy -s <value> -t <value> -p <value> [--json] [--flags-dir <value>] [--include-profile-owned]
[--strict] [--api-version <value>]
FLAGS
-p, --perms=<value> (required) Comma-separated list: object API name for object-level permissions only;
Object.Field or Object:Field for one field; Object._ or Object:_ for all FLS-enabled
fields on that object (expanded using Tooling FieldDefinition on the source org).
-s, --source-org=<value> (required) Username or alias of the org to read permissions from.
-t, --target-org=<value> (required) Username or alias of the org to update.
--api-version=<value> Override the api version used for api requests made by this command
--include-profile-owned Include permission sets owned by profiles (IsOwnedByProfile = true).
--strict Fail if any object or field from --perms is missing on the target org. When omitted,
missing target fields/objects are skipped with warnings.
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Copy FieldPermissions and/or ObjectPermissions from a source org to a target org for matching permission sets.
Provide one or more permission targets with `--perms`:
- **Object only** (e.g. `Account`) — syncs **ObjectPermissions** for that object (create/read/edit/delete, etc.). Does
not read FieldPermissions for that token.
- **Field** (`Object.Field` or `Object:Field`, e.g. `Account.Name`) — syncs **FieldPermissions** for that field.
Object-level permissions for that object are also synced for permission sets that have source field rows (narrow
scope).
- **All fields on an object** (`Object.*` or `Object:*`) — expands to every field on that object that is
**FLS-enabled** in metadata (Tooling **FieldDefinition** / `IsFlsEnabled` on the **source** org), then syncs
FieldPermissions like individual field tokens. Wildcard expansion uses the Tooling API so the list does not depend on
the user’s field visibility.
You may mix tokens in one command (e.g. `Account,Contact.Email` or `Contact.*`).
Queries FieldPermissions and ObjectPermissions on the source and target orgs as needed, matches permission sets by API
name (PermissionSet.Name) and NamespacePrefix, computes the diff (source is the source of truth), then applies
inserts, updates, and deletes on the target.
Wildcard expansion and preflight validation use the **Tooling API** (`FieldDefinition`, `EntityDefinition`) so object
and field names come from org metadata; the running user must have Tooling access (e.g. View Setup and Configuration),
not only FLS on the migrated fields.
By default, only permission sets with IsOwnedByProfile = false are considered. Use --include-profile-owned to include
profile-owned (shadow) permission sets.
By default, fields (and objects) that are not present on the **target** org are **skipped** with a warning so the rest
of the migration still runs. Use **--strict** to fail the command instead when anything in `--perms` is missing on the
target.
EXAMPLES
Copy FLS for two fields between aliases `org1` and `org2`:
$ sf migrate permset copy --source-org org1 --target-org org2 --perms \
"Account.My_Field**c,Custom_Object**c.Other\_\_c"
Object-level permissions for Account only, plus one field on Contact:
$ sf migrate permset copy -s org1 -t org2 --perms "Account,Contact:Email\_\_c"
Use colon separators and include profile-owned permission sets:
$ sf migrate permset copy -s org1 -t org2 --perms "Object**c:Field**c" --include-profile-owned
Sync all permissionable Contact fields (wildcard):
$ sf migrate permset copy -s org1 -t org2 --perms "Contact.\*"
See code: src/commands/migrate/permset/copy.ts
- Permission sets are matched across orgs by API name (
PermissionSet.Name) and NamespacePrefix. If the same API name exists in different namespaces, resolve collisions by ensuring metadata is unambiguous on both sides. - Only permission sets that already exist on the target are updated. Source-only permission sets are reported in JSON output under
skippedPermissionSetsand are not created on the target. - Object-only tokens (e.g.
Account) sync ObjectPermissions for that object for every matched permission set on both orgs. Field tokens sync FieldPermissions only for that field; if you do not pass any object-only token, object-level rows for that object are synced only for permission sets that have source field rows. If you pass both (e.g.Account,Account.Name), object-level sync covers all matched sets for the objects in your--permslist, and the source query includes ObjectPermissions for every matched permission set so rows are not missed when discovery omitted a parent. - DML order on the target is: insert ObjectPermissions then FieldPermissions, then updates in the same order, then delete FieldPermissions then ObjectPermissions, so object rows exist before field rows and revokes remove field rows before object rows. Field/Object permission deletes use one REST DELETE per record Id (not the composite
?ids=batch) for compatibility with Salesforce responses. Query rows normalizeId/idfrom the API.Object.*(orObject:*) expands to fields on that object where Tooling FieldDefinition reports IsFlsEnabled (same idea as “permissionable” in describe). The field list is resolved on the source org via the Tooling API (metadata, not the user’s FLS visibility). By default, fields (or whole objects) that are missing on the target are skipped with a warning; use--strictto fail instead if anything in--permsis absent on the target.
- Schema checks use the Tooling API (
FieldDefinition,EntityDefinition). The integration user needs access to Tooling (e.g. View Setup and Configuration or equivalent), not only FLS on the fields being migrated.FieldDefinitionSOQL filters use theEntityDefinitionrelationship in theWHEREclause (not onlyFROM FieldDefinition). - Field sync only considers
FieldPermissionsrows whoseSobjectTypematches an object from--perms(e.g.Contact.*ignores unrelated rows such as other entities that can appear in broad queries). That avoids diffing or deleting FLS for objects you did not request. sf migrate layout editretrieves and deploys Layout metadata via@salesforce/source-deploy-retrieveinto a temp directory (no local DX project required). Layout listing is Tooling-only (no MetadatalistMetadatafallback). It resolvesLayout.TableEnumOrIdviaSELECT DurableId FROM EntityDefinition WHERE QualifiedApiName = '<apiName>'— for standard objectsDurableIdmatches the API name; for custom objects it is the entity id (e.g.01I…). ThenSELECT Id, Name FROM Layout WHERE TableEnumOrId = '<DurableId>'; metadata FullName values are<apiName>-<Name>(Tooling returns MALFORMED_QUERY ifFullNameis selected when more than one layout row is returned). After deploy, you can confirm field order withnode scripts/verify-layout-field-position.mjs(afteryarn compile) on a retrieved.layout-meta.xml. Placement looks for the reference field only under detaillayoutSections→layoutColumns→layoutItems. If the reference is missing, that layout is skipped with a warning and is not deployed. The plugin build enablesskipLibCheckso third-party packages (including SDR) type-check without pulling their JSON typings into the project compile.- Layout deploy errors (
LayoutDeployError) include component-level messages from the Metadata API when present (many orgs returnstatus: Failedwith a blankerrorMessage, while the real text is underdetails.componentFailures). The message includes the Deploy Id so you can match it in Setup → Deployment Status. If you move a standard Name field and set--read-only, the deploy may be rejected—retry without--read-onlyif the failure mentions name or editability.
# define the groups of permission sets that we will grant permissions to
PermsetGroups:
- name: 'Group 1'
permsets: [set1, set2, setN]
- name: 'Group 2'
permsets: [set1, set2, setN]
# "Upsert" contains the permissions the command will create or update
Upsert:
# objects that we will grant permissions to
- Objects:
- name: 'Object'
perms:
- type: read
permsets: ['Group 1', 'Group 2']
- type: edit
permsets: ['Group 1']
# fields that we will grant permissions to
- Fields:
- name: 'Object.Field'
perms:
- type: read
permsets: ['Group 1', 'Group 2']
- type: edit
permsets: ['Group 1']
# The above literally means:
# For these objects, grant read permission to Group 1 and Group 2, where
# "read" is granted to permission sets in group "Group 1" and "Group 2"
# and "edit" is granted to permission sets in group "Group 1"
# For the field "Field" on the object "Object", grant read permission to
# groups "Group 1" and "Group 2", where "read" is granted to permission sets
# in group "Group 1" and "edit" is granted to permission sets in group "Group
# 1"
# "Remove" contains the permissions the command will remove
Remove:
- Objects:
- name: 'Object'
perms:
- type: read
permsets: ['Group 1', 'Group 2']
- type: edit
permsets: ['Group 1']
- Fields:
- name: 'Object.Field'
perms:
- type: read
permsets: ['Group 1', 'Group 2']
- type: edit
permsets: ['Group 1']
# The above literally means:
# For these objects, revoke read permission from Group 1 and Group 2, where
# "read" is revoked from permission sets in group "Group 1" and "Group 2"
# and "edit" is revoked from permission sets in group "Group 1"
# For the field "Field" on the object "Object", revoke read permission from
# groups "Group 1" and "Group 2", where "read" is revoked from permission sets
# in group "Group 1" and "edit" is revoked from permission sets in group "Group
# 1"- FieldPermissions
- ObjectPermissions — includes
PermissionsViewAllFields(API v63.0+) for object-level “View All Fields”. - PermissionSet