Command details
Command details
Deeper logic notes for warden’s more involved commands. Flag-by-flag
reference lives in the README and in each command’s
--help output; this doc covers the why and the merge/precedence rules
that don’t fit in a flag summary.
Output formats, file destinations, CSV shape, and global --json interaction
are documented in the output contract.
CSV row schemas
CSV is item-grained where a command changes or reports individual access records. Columns are stable and new columns are appended to existing schemas.
| Command | One row | Columns |
|---|---|---|
access | one grant | existing 12 columns, then access flags for the target type |
diff | one delta | userKey,userId,category,kind,value,mode,userName,username,valueApiName,valueLabel,valueType,valueBefore,valueAfter |
provision | one user action or error | userKey,userId,userName,username,personas,matchedBy,status,action,detail,error |
restore | one user action or action item | userKey,userId,userName,username,status,action,category,name,error |
strip | one user action or removed item | userKey,userId,userName,username,status,action,category,itemId,itemApiName,error |
freeze / unfreeze | one user | userKey,userId,userName,username,wasFrozen,status,action,error |
snapshot | one lifecycle report row per user | key,id,status,actions,skipped,warnings,errors |
diff keeps value in its original position and continues to hold the
record Id. Profile and role rows also expose valueBefore and valueAfter;
their value remains the human-compatible before -> after string. A user
with no actions still receives a row, and errors are emitted as separate
rows, so failed or unchanged users cannot disappear from a CSV attachment.
The shared CSV writer prefixes cells beginning with =, +, -, @, tab,
or carriage return with ' before quoting. This prevents spreadsheet formula
execution, including for user-supplied labels and attributes. Snapshot CSV
input removes that protective prefix when it is present so snapshot names
survive a write/read round-trip. CSV user-definition input rules are
documented below.
warden provision
Provisioning logic
User definitions and persona definitions live in two separate JSON files so that user-specific data stays decoupled from reusable access bundles.
Field naming
- User fields must be Salesforce
UserAPI field names. - Input field names are accepted case-insensitively and canonicalized using
Userdescribe metadata before any DML. - User-level fields override persona
userAttributeswhen both supply a value. - A user may also include a
matchmeta key to choose its own lookup field.
Multiple personas per user
When --personas-def is supplied, each user entry must include a non-empty
personas array listing names defined in personas.json. The command merges
all named personas into a single effective persona before planning:
- Assignment lists (
permissionSets,permissionSetGroups,publicGroups,queues) are unioned across all personas, with duplicates removed in first-seen order. - Singular values (
profile,role, per-category modes) must agree across all personas. If two personas specify different values for the same singular field, that user fails with a per-row conflict error — the rest of the batch continues. userAttributeskeys are also conflict-checked. If two personas supply the same key with different values, the user fails unless the user entry itself overrides that field (user-level value always wins).
Username and Alias defaults (insert only)
When creating a new user (no matching existing record), Username and
Alias are automatically derived if omitted:
Username— defaults to<Email>.<myDomain>, where<myDomain>is the first DNS label of the org’s My Domain hostname (e.g.jdoe@acme.comcombined with orgmycompany.my.salesforce.comgivesjdoe@acme.com.mycompany). If the domain cannot be derived, the field is left unset and the normal missing-required-field error is reported.Alias— derived fromFirstNameandLastName: take up to 3 letters from each; when one name is shorter than 3 letters, borrow from the other to target 6 total; lowercase; truncated to Salesforce’s 8-char limit. Examples:John/Doe→johdoe;Jo/Anderson→joande;Al/Bo→albo.
Defaults are never applied to users matched for update, and explicit
Username/Alias values in the user entry are always used as-is.
Field precedence
User-level values override persona values for every overlapping field. The only place a persona “wins” outright is by contributing assignment-list entries (which union) and by supplying a value the user left unset.
| Setting | Set on a user (users.json) | Set on a persona (personas.json) | Who wins | Notes |
|---|---|---|---|---|
| Profile | profile (name or 15/18-char Id) or raw ProfileId | profile (name or Id) | user profile → user ProfileId → persona profile | Profile.Name resolution. Setting both profile and ProfileId on one user is a per-row error. Required on insert. |
| Role | role (DeveloperName/Name or Id) or raw UserRoleId | role | user role → user UserRoleId → persona role | UserRole.DeveloperName/Name resolution. Both role+UserRoleId is a per-row error. Optional. |
| Username | Username | persona userAttributes.Username | explicit value > computed default | Default <Email>.<myDomain>, insert only. |
| Alias | Alias | persona userAttributes.Alias | explicit value > computed default | Default 3+3 name derivation, insert only. |
Permission-flag fields (UserPermissionKnowledgeUser, …) | direct boolean field | persona userAttributes | user field > persona userAttributes | Written onto User; subject to org licenses. |
Other writeable User fields (Title, Department, …) | direct field | persona userAttributes | user field > persona userAttributes | Canonicalized case-insensitively against User describe. |
| Assignment lists (permission sets, groups, queues) | — (not settable per user) | persona lists + per-category …Mode | union across all personas | Resolved by name/Id; additive keeps existing, sync removes unlisted. |
| Match field (upsert key) | match meta key | — | per-user match > --external-id flag | Must be a filterable User field. See User matching. |
| IsActive | — | — | always true | Provisioning always activates and unfreezes. |
A user-level profile or role also suppresses the corresponding
persona-vs-persona conflict error for that field — if you name your own
profile it no longer matters that two personas disagreed.
Profile-only provisioning
Omit --personas-def when each row carries its own profile and/or role
and should receive no permission-set, group, or queue assignments. For
example:
sf warden provision --users-def ./users-profile-only.json \ --dry-run --target-org acme-uatProfile-only rows use the same user matching, profile/role resolution, and
insert defaults as persona-driven rows. Assignment lists remain persona-only,
so the effective assignment persona is empty. Every assignment mode therefore
defaults to additive: this mode can add no assignments and cannot remove
existing access.
Match resolution
For the shared exact/fuzzy resolution rules, including case handling, ambiguity behavior, filterable fields, and sandbox Username suffixes, see User matching.
For provisioning specifically:
--external-idsets the default field used to match existing users;--match-fieldis an alias.- A per-user
matchvalue overrides the flag for that row only. - If neither is present, the user is treated as an insert.
Mixed-source example
{ "users": [ { "personas": ["ops"], "match": "FederationIdentifier", "FederationIdentifier": "ABC123", "LastName": "Park" }, { "personas": ["csr"], "match": "Username", "Username": "bob@acme.com.dev", "LastName": "Bob" }, { "personas": ["finance"], "match": "Employee_ID__c", "Employee_ID__c": "E-9981", "LastName": "Su" }, { "personas": ["creator"], "Username": "alice@acme.com.dev", "LastName": "Alice" } ]}Practical required fields for new user creation
The following fields are required when inserting a new user. Username and
Alias can be omitted and will be defaulted automatically (see above); all
others must be explicitly provided.
Username(auto-defaulted fromEmailand org My Domain if omitted)LastNameAlias(auto-defaulted fromFirstName/LastNameif omitted)TimeZoneSidKeyLocaleSidKeyEmailEncodingKeyLanguageLocaleKeyProfileIdwhen no user-level or personaprofileis specified
Reference lookup behavior
References are resolved by Id or developer/API name. Labels are intentionally unsupported because they are not reliably unique.
| Reference | Resolved by |
|---|---|
| Profile | Id or Profile.Name |
| Role | Id or UserRole.DeveloperName (falls back to Name) |
| Permission Set | Id or PermissionSet.Name |
| Permission Set Group | Id or PermissionSetGroup.DeveloperName |
| Public Group | Id or Group.DeveloperName where Group.Type='Regular' |
| Queue | Id or Group.DeveloperName where Group.Type='Queue' |
Missing optional assignment targets produce a warning and are skipped.
Missing required references (a persona profile or role that cannot be
resolved) fail the affected user.
Assignment modes
Each assignment category has its own mode property. The default for every
category is additive. When multiple personas are merged, the mode for each
category must agree across all personas that specify it; a disagreement is a
per-row conflict error.
| Mode property | Behavior |
|---|---|
permissionSetMode | additive adds missing assignments. sync adds missing and removes any not listed. |
permissionSetGroupMode | Same semantics as permissionSetMode, for permission set groups. |
publicGroupMode | Same semantics, for GroupMember records where Group.Type='Regular'. |
queueMode | Same semantics, for GroupMember records where Group.Type='Queue'. |
sync partitions GroupMember rows by Group.Type, so a public-group sync
will not affect queue memberships and vice versa.
Dry run
--dry-run validates input, resolves references, queries current org state,
and reports planned actions. It performs no inserts, updates, deletes,
upserts, anonymous Apex, or Composite/Tooling write requests. Dry-run is
intentionally planning-only and not rollback-backed.
License headroom
During a dry run, provisioning reports the user-license headroom required by
valid net-new users. Users matched for update and plans with errors are
excluded. Profiles are grouped by their UserLicenseId, so multiple profiles
consume one combined license total. Available headroom is
TotalLicenses - UsedLicenses; a non-active license has zero available, while
a negative TotalLicenses is unlimited. Non-active rows include their status
in the note field.
The result’s licenses array is included in human and JSON dry-run output.
Unlimited capacity is JSON-safe and explicit: it uses available: null and
unlimited: true, with shortfall: 0. Dry runs also report
permissionSetLicenses: { evaluated: false, note: "not evaluated" }; PSL
headroom is intentionally deferred because provisioning does not assign PSLs.
Live results omit both license fields.
--fail-on-insufficient-license checks the dry-run license rows after the
summary is emitted and raises a non-zero error when any shortfall is greater
than zero. Without the flag, each shortfall is emitted as a human warning and
counted in summary.warnings; JSON output exposes the same count without
duplicating warning text. With global --json, the non-zero error envelope
retains the complete preflight result, including result.licenses. The flag
has no effect on live provisioning in this release.
Password handling
Password set/reset is intentionally out of scope.
Example users.json
{ "users": [ { "personas": ["admin"], "FederationIdentifier": "ABCD1234", "FirstName": "John", "LastName": "Doe", "Email": "jdoe@email.com", "LocaleSidKey": "en_US", "EmailEncodingKey": "UTF-8", "LanguageLocaleKey": "en_US", "TimeZoneSidKey": "America/Los_Angeles" } ]}Username and Alias are omitted above and will be defaulted on insert to
jdoe@email.com.<myDomain> and johdoe respectively.
Example users.csv
The same flat user definition can be maintained as CSV. Headers are matched
case-insensitively to User API fields; personas, match, and
fuzzyUsername are metadata columns. Empty cells are omitted, and persona
lists use semicolons by default. Use --input-format json|csv when the file
extension does not identify the format, and --csv-list-delimiter for a
different list separator. See the committed CSV example.
CSV headers are validated strictly and all cells remain strings except for
fuzzyUsername and describe-backed boolean User fields. Accepted boolean
values are true, false, 1, 0, yes, and no, case-insensitively.
CSV is intentionally stricter than JSON for unknown headers so spreadsheet
typos cannot silently discard data. Empty cells cannot represent an explicit
blank value.
Users CSV follows these rules:
- Values are never type-inferred; only describe-backed booleans and
fuzzyUsernameare converted to booleans. - Empty cells omit the corresponding key, like an absent JSON key; an explicit blank value is not representable.
- Boolean values are case-insensitive and accept
true/false,1/0, andyes/no; other values fail with the physical row line. - A UTF-8 BOM is accepted and removed from the first header.
- LF and CRLF line endings are accepted; CSV output uses LF.
personasuses semicolon-separated, trimmed values by default, or the delimiter supplied through--csv-list-delimiter.- Duplicate headers, including case variants, are errors.
- Unknown headers are errors and include a close field-name suggestion when one is available.
- Parse and row-validation errors include the source path and physical line number, including after embedded newlines in quoted cells.
- Rows with the wrong number of cells are errors; a final empty line is ignored.
- Input uses the shared CSV module beside the shared CSV writer.
Example personas.json
{ "personas": { "admin": { "profile": "Admin", "role": "CEO", "permissionSetMode": "additive", "permissionSets": ["Admin_Permissions"], "permissionSetGroupMode": "additive", "permissionSetGroups": ["Admin_Group"], "publicGroupMode": "sync", "publicGroups": ["Admin_Public_Group"], "queueMode": "additive", "queues": ["Case_Queue"], "userAttributes": { "Title": "Salesforce Administrator", "Department": "Sales" } } }}The examples above show the supported multi-persona and profile-only shapes. Larger samples are available as users.json, personas.json, and users-profile-only.json.
Related records (--related-def)
--related-def accepts a JSON catalog of reusable relationships. A JSON user
entry selects catalog names with a related array; the key is metadata only
and is never sent to the User API. CSV user definitions cannot select
relationships, so combining CSV input with --related-def is an error.
The catalog must contain a relationships object. Each selected name must
exist in that object and occur only once in the user’s related array:
{ "users": [ { "related": ["employee"], "FederationIdentifier": "E-9981" } ]}Only phase: "after" is supported: Warden saves the User first, then creates
or updates the related row and can set a lookup from { "from": "user.Id" }.
Before-phase external-user provisioning is deferred to v2. A relationship has
a unique or External-ID, filterable match field sourced from a non-Id User
field. A zero/one/multiple match respectively creates, updates, or fails that
user; duplicate resolved match values across users also fail before DML.
{ "relationships": { "employee": { "sobject": "Employee__c", "phase": "after", "match": { "field": "External_Person_Id__c", "from": "user.FederationIdentifier" }, "fields": { "Department__c": { "from": "user.Department" }, "Employment_Status__c": { "value": "Active" }, "User__c": { "from": "user.Id" } } } }}Every relationship needs a non-empty sobject, phase, match, and fields
object. A field source is exactly { "from": "user.<UserField>" } or
{ "value": <literal> }; { "from": "user.Id" } is also valid for a
field, but never for match.from. User-field sources must name a real User
field and resolve to a non-empty value. The target match field must be
filterable and either unique or an External ID.
mode defaults to setIfEmpty, which preserves populated values on a matched
record (only literal null and "" are empty). Set mode: "sync" to
overwrite every configured field. On creates, both modes write all configured
fields and always write the match field. A configured record type is applied
only on create; Warden never retags a matched record, and a matched row with a
different configured record type fails that user. An Account relationship
must declare an available Person Account record type.
Before planning, Warden checks each selected relationship’s target object,
field access, match metadata, and record type. An ineligible relationship is
reported as a warning and skipped after the command’s single warning
confirmation; --no-prompt and non-interactive JSON runs skip it
automatically. Related DML is bulked by sObject in batches of at most 200
records. Dry runs validate, match, and show the related plan without issuing
DML. In a live run, a related-record save failure leaves the already-saved User
in place but marks that user’s provisioning result failed. Human output prints
one related: line per result; see the output contract
for JSON and CSV shapes.
warden diff
The same reference-resolution rules as provision apply when
--personas-def is supplied, but diff never writes — it only reports what
would change. Omit --personas-def for a profile-only audit when each row
supplies its own profile or role; the result contains profile and role drift
only, with no assignment rows. Use --user/--against to compare two live
users directly instead of a user against its intended persona state; in that
mode, no definition files are needed at all.
Use --fail-on-drift when diff is a CI gate: it returns exit code 1 when
one or more users have drift. The flag is disabled by default. See the
output contract for the interaction with
per-user failures and global --json.
Use --verify with --users-def to turn the diff into a conformance check.
--personas-def is optional when verifying profile-only definitions. It
returns one verdict per user with key, conformant, and violations fields.
Missing intended assignments and profile/role mismatches are violations; extra
assignments are violations only for sync categories. A missing org user is
reported as user not found.
The human format prints a conformant/non-conformant summary and violation
blocks; --output json and global --json return the verdict array, while
--output csv uses key,conformant,violations columns. Verify mode sets
process.exitCode = 1 after rendering whenever any verdict is
non-conformant, so global JSON output remains available to CI consumers with
the normal successful envelope status. It is rejected with --user or
--against.
For example, verify a profile-only definition without assignment expectations:
sf warden diff --users-def ./users-profile-only.json \ --verify --target-org acme-uatwarden access
See Access audits for target and reverse-user scopes, attribution, Permission Set Group muting, limitations, and examples. The command remains read-only and its shared output behavior is covered by the output contract.
warden snapshot / warden restore
Snapshots capture active/frozen state and assignments by developer/API name
(never by Id), so a snapshot taken in one org can be restored into another.
Each entry also optionally records the resolved name, username, email,
profile, and role as advisory review fields. Profile and role are reported
by name when available, with their Id as a fallback; restore never applies
either field.
restore re-resolves users by the snapshot’s match key, reactivates and
unfreezes them, and only adds missing assignments — it never removes
access the user already has. If you need a rollback point before a
destructive change, pair --snapshot on strip with a later restore
rather than taking a separate snapshot step.
The snapshot writer selects JSON or CSV from the output extension. CSV is a long, self-describing format with one row per assignment:
snapshotVersion,capturedAt,org,match,matchValue,userId,userName,username,email,profile,role,isActive,isFrozen,category,nameThe category is permissionSet, permissionSetGroup, publicGroup,
queue, permissionSetLicense, or none. A user with no assignments gets a
none row so active/frozen state and identity are not lost. restore accepts
either JSON or CSV by extension, validates shared metadata, groups rows by
match, matchValue, and userId, and sorts/deduplicates assignment names.
During --dry-run, it shows the recorded snapshot identity beside the
re-resolved org identity and warns about mismatches. Profile and role lines
are annotated (reported, not applied).
See Lifecycle output and snapshots for the resolved identity line, assignment label formatting, itemized action reporting, and the snapshot file fields.
warden freeze / warden unfreeze / warden strip
freeze/unfreeze touch only UserLogin.IsFrozen — no other access is
changed. strip is the composite operation: freeze, then remove access
grants, then deactivate, unless the corresponding --no-freeze,
--keep-*, or --no-deactivate flags opt out of a step. Use --snapshot
on strip to write a restorable snapshot before any writes happen — it is
written even during --dry-run, so you can capture and inspect the
pre-strip state without touching the org.