Orgbench
Overview
Orgbench is an IntelliJ IDEA Tool Window for comparing Salesforce custom-field metadata between two locally authenticated sf aliases and selectively copying source differences to the target org. It uses the installed Salesforce CLI for authentication and operations; the plugin does not implement OAuth or ask for Salesforce credentials.
The first screen lets you pick a metadata type and build a session manifest of non-namespaced components across one or more types before comparing. Objects & Custom Fields is one item: selecting a custom object there compares both the object definition and that object's custom fields, so you never have to pick the same object twice, while a standard object contributes only its custom fields. A deployment runs in dependency waves — custom objects deploy before the custom fields that depend on them, so a source-only object can be created in the target in the same deployment as its fields. An explicit FLS transfer mode can also synchronize field-level security after a field is deployed successfully, or without redeploying metadata when the field already matches the target.
The plugin bundles a native core, sf-core-server, and talks to it over a JSON-RPC 2.0 stdio protocol. Bundled native core binaries ship for macOS arm64, macOS x64 and Windows x64; Linux is planned. A VS Code extension provides a second thin client of the same sf-core-server core, packaged as a platform-specific VSIX; see vscode/README.md.
Requirements
- IntelliJ IDEA 2024.2+.
- A Java 17-compatible IDE runtime for the plugin UI. The bundled native core (
sf-core-server) needs no JRE. - A supported native platform: macOS arm64, macOS x64, or Windows x64 (Linux is planned).
- Salesforce CLI (
sf) installed. The plugin checks the IDE processPATHand the standard macOS/Linux user and system install locations. On Windows it looks forsf.exe,sf.cmd,sf.batandsfinPATHand in%LOCALAPPDATA%\sf\client\bin. - Two authenticated aliases that resolve to different Salesforce org IDs.
- Source and target users with the API, metadata, and data permissions needed by the selected operations.
- Target objects must either already exist or be explicitly selected as CustomObject rows in the same deployment; a source-only custom object is created in the target before its fields deploy.
Check the CLI and aliases in a terminal before opening the Tool Window:
sf version --json
sf org list --json
If aliases are not configured yet, authenticate each disposable or approved org explicitly:
sf org login web --alias source-dev
sf org login web --alias target-qa
Restart IntelliJ IDEA after changing PATH; a GUI-launched IDE can inherit a different environment from an existing terminal. On macOS the plugin also detects ~/.local/share/sf/client/bin/sf, /opt/homebrew/bin/sf, and /usr/local/bin/sf, so a standard Salesforce CLI installation does not depend on the GUI application's reduced PATH.
Install from Disk
- Obtain
orgbench-0.1.1.zip, or build it as described below. Do not unpack the ZIP. - In IntelliJ IDEA, open Settings/Preferences | Plugins.
- Open the gear menu, choose Install Plugin from Disk, and select the ZIP.
- Restart the IDE if prompted.
- Open View | Tool Windows | Orgbench.
The Tool Window checks sf version --json when it loads. It then discovers local aliases. A missing or incompatible CLI is reported with remediation text; choose Restart after correcting the prerequisite. An empty alias list is reported with remediation text too; choose Reload Orgs after authenticating an alias.
Workflow
- Select Source. The Source and Target dropdowns support type-to-search: with a dropdown focused, type a case-insensitive alias prefix to jump to matching aliases, the same case-insensitive prefix matching the component and field filters use. Pick a Metadata Type from its dropdown (Objects & Custom Fields, Custom Metadata Types, Platform Events, Apex Classes, Apex Triggers, Lightning Web Components, Aura Components, Visualforce Pages, Page Layouts, or Flows); the plugin loads customizable, non-namespaced components of that type into a filterable table. The dropdown is grouped by metadata category and lists only the categories you enabled in the settings; out of the box only the Core category is enabled, so the list is exactly the one above. Enabling a further category adds its types under their own group heading; thirteen categories are available beyond Core — Objects & Fields, UI & Navigation, Code, Automation, Security & Sharing, Integration & Auth, Service Cloud, Experience Cloud, Analytics, Sales Cloud, Email & Templates, Localization and Mobile — and together they carry 162 further metadata types, so with every category enabled the dropdown offers 172 types in fourteen groups. Typing in the dropdown searches those labels by substring in the IntelliJ plugin unconditionally, in the VS Code extension only when the combined list exceeds 11 types. If the type you were looking at belongs to a category you just switched off, the screen moves to the first type still on the list rather than showing rows of a type the dropdown no longer offers. Custom Metadata Types (
__mdt) and Platform Events (__e) are object-form definitions listed by their name suffix; their fields are compared and deployed under Objects & Custom Fields like any other object's fields. Type a case-insensitive prefix in the Label or API Name filter cell to match that column (a trailing*is optional); check individual rows, use the header checkbox in the Select column for select-all-visible, or use Select All and Clear, which apply only to the currently visible, filtered components. The selections you make across metadata types accumulate into one session manifest — switching Metadata Type does not discard a prior type's selection, and a summary line reports how many components are selected per type. Export Manifest… and Import Manifest… next to Select All/Clear round-trip that whole session manifest through apackage.xmlfile; see Manifest Export and Import below. - Select Target. Different aliases are still rejected if they resolve to the same underlying org ID.
- Select at least one component and choose Compare. Source and target metadata are retrieved into separate temporary projects.
- Review the
Select,Type,Component,Change, andDiffcolumns. A field row whose target object is missing becomes selectable once the table also carries that object's deployableCustomObjectAddrow; you still have to select that row too, or the deployment is rejected before it starts. CustomCustomObjectrows are deployable; a standard object contributes no definition row at all, because only custom objects can be deployed. - Each column filters independently from a filter row beneath the headers. Type a case-insensitive prefix in the Type or Component cell to match that column
like value*(a trailing*is optional). Use the Change multi-select to pick any subset ofAdd,Update, andNo change; by default onlyAddandUpdateare checked. The Select column's filter cell is a tri-state checkbox: checked shows only selected rows, unchecked shows only unselected rows, and the greyed (indeterminate) default applies no selection filter. The separate header checkbox still selects or clears only visible, eligible rows and becomes indeterminate for a partial visible selection. - Use Back to close the comparison and return with the current Source, Target, per-type component catalog, and session manifest preserved. Alternatively, select fields, choose Do not transfer FLS, Additive FLS, or All FLS from FLS Mode, and choose Next. In Do not transfer FLS, confirm the source, target, and metadata-field count before deployment; cancelling the confirmation leaves the review unchanged. In Additive FLS or All FLS, Next instead opens a container-selection screen listing every eligible container with its Existing/New presence column, filterable through the Presence column's multi-select (both shown by default); Select All/Clear apply only to the currently visible, filtered containers. Deselect any selectable container to exclude it, or Back to return to the review. Choosing Next there is the confirmation step for the FLS path.
- During comparison or deployment, IntelliJ shows a cancellable background task. Cancel stops new work and asks the active CLI process to terminate when possible.
- Read the field and FLS result tables. In the FLS result table, the Status column filter hides
Skippedrows by default; checkSkippedin that column's filter to reveal them alongsideUpdated,Failed, andNot Attempted. Show Details and Copy expose only sanitized diagnostics. New Comparison returns to a fresh alias selection.
Git Source and Target
Each side of a comparison is chosen independently: a Salesforce org or a local git repository (an absolute path plus a branch). All four combinations work — org to org, org to git, git to org, git to git. A repository side is read through the SFDX layout declared by its sfdx-project.json: every packageDirectories entry is scanned in file order, and on a duplicate component the first entry wins. A source repository without sfdx-project.json is rejected; a target repository may be empty or not yet an SFDX project, in which case the plugin creates a minimal sfdx-project.json and a force-app/main/default/ skeleton inside the same commit. A target repository that already has an sfdx-project.json the plugin cannot parse is a hard error instead — the skeleton never overwrites an existing manifest.
Branch reading follows one rule: the current branch is read from the working tree, so uncommitted edits are part of the comparison, while any other branch is read from the git objects. The branch dropdown lists local and remote branches; picking origin/foo as a target means "create or use the local foo from it".
Deploying to a repository writes the selected components into the layout, stages exactly those paths and makes one commit — the deploy ends in a local commit, pushing is up to you, and the plugin never checks out a branch in your repository: a branch other than the current one is written through a temporary git worktree that is removed afterwards, and when the target branch is the current one only the written paths are committed, so unrelated modified files stay out. The "+" button next to the branch dropdown names a new branch, and that branch is created only by the deploy commit — cancelling the session leaves nothing behind. Deploying a bundle (LWC/Aura) to a repository target can delete files: a file left over inside that bundle's directory from a previous version is removed, and the deletion is part of the same commit.
With a repository target there are no deploy modes and no Apex test screen: the write is atomic by construction and the Salesforce CLI is not involved at all. Dependency auto-selection needs the Tooling API and is therefore unavailable for a repository source, with one exception: when the source is an org (whether the target is an org or a repository), a Page Layout's own dependency on its custom fields is read straight from the layout XML and auto-selected without the Tooling API. When the source itself is a repository, this offline layout auto-select is unavailable too, exactly like every other dependency. Two selection rules need no Tooling API at all and therefore work with a repository source as well: a field row whose object is missing from the target auto-selects that object's Add row, and checking that object's Add row auto-checks the object's own field rows. The review screen still warns about every other Tooling-sourced dependency it cannot compute for a repository source. FLS transfer does work with repository sides — Profile and Permission Set documents are read from and written to profiles/ and permissionsets/ files in the same commit, and a Profile file is only ever updated, never created.
GitHub endpoints are not yet supported: the entry is visible but disabled, and the core rejects it.
Manifest Export and Import
The selection screen round-trips the session manifest through a Metadata API package.xml file. Both actions run entirely in the plugin core: the client only shows a native file dialog and hands the core a path.
Export manifest writes the current session manifest — including the checkboxes of the type you are looking at right now, which are committed into the manifest first — to the file you pick in a save dialog (default name package.xml). It uses the same API version as the retrieve path and the standard <types>/<members>/<name> layout, so sf project retrieve start --manifest package.xml accepts it unchanged. Objects & Custom Fields, Custom Metadata Types and Platform Events merge into the single CustomObject section the Metadata API expects; Apex Classes, Apex Triggers, Lightning Web Components, Aura Components, Visualforce Pages, Page Layouts and Flows each get their own section. Field selections are exported at object granularity: a selected object is written as one member, never as Object.Field.
Import manifest reads the package.xml you pick and replaces the session manifest with it — the file is the source of truth, so export the current selection first if you want to keep it. Every member is validated against the source org and nothing is skipped silently: the screen reports how many components were imported plus one warning per skipped member. A <members>*</members> wildcard is expanded to the full source-org listing of that type. A CustomObject member imports the object and its fields, matching Metadata API semantics, and __mdt/__e members are routed to Custom Metadata Types and Platform Events by their suffix; a standard object imports only its fields, because a standard object definition can never be deployed. A CustomField member written as Object.Field is widened to its object with a warning, because the manifest's granularity is the object. A member the source org does not list, a namespaced member, and an unsupported manifest type (Profile, ConnectedApp, and the like) are each reported and skipped — namespaced members are never imported, in line with the rest of the plugin.
The importer rejects a DOCTYPE declaration, a file larger than 5 MB, and any XML whose root element is not Package. A rejected file leaves the session manifest and the current metadata type untouched, and so does a failed source-org listing.
Comparison Rules
Components match by metadata type and API name (fields additionally by their SObject Type). The plugin shows:
Addwhen the source field is absent from an existing target object;Updatewhen a matching field differs in a supported attribute;No changewhen supported attributes are equal;- no row for target-only fields, because the plugin never deletes fields.
The Change multi-select controls which source rows are visible: check any subset of Add, Update, and No change. It defaults to Add and Update checked, so No change rows are hidden until you check that value. A No change row is never a metadata deployment candidate. It becomes selectable only in Additive FLS or All FLS mode, and returning to Do not transfer FLS immediately deselects all such rows.
Updates compare these attributes in deterministic order: type, formula, default value, lookup or master-detail target, length, precision, scale, and label. A short diff is rendered target-to-source, for example Length: 40 -> 80; Label: Old -> New.
Namespaced metadata is ignored. This applies to namespaced objects and fields, including managed-package fields such as namespace__Field__c; the MVP does not try to detect a packaging org in which that namespace is under development. Standard fields are not copied.
When an object is absent from the target, its source fields are displayed as Add with Target object is missing. Those field rows become selectable once the table also carries the object's deployable CustomObject Add row; otherwise they stay disabled. Selecting the field still requires selecting that object row too: if you select such a field without selecting its object row, the deployment is rejected before it starts with Missing target object 'X__c': select its object row or deselect its fields.
CustomObject metadata is compared like any other type — Add, Update, and No change rows are computed the same way. Only a custom object (__c) gets a definition row: a standard object selected in Objects & Custom Fields contributes its custom fields alone, mirroring the rule that standard fields are never copied.
Custom Metadata Types and Platform Events are the same object form on the Metadata API (a CustomObject component), so they deploy in the object wave before their fields exactly like custom objects: a source-only __mdt or __e object can be created in the target in the same deployment as its fields. A Custom Metadata Types row that is not a __mdt object is disabled with Only custom metadata types can be deployed., and a Platform Events row that is not a __e object with Only platform events can be deployed. Custom Metadata records (Type.Record) are covered too, as Custom Metadata Records under Objects & Fields: unlike the object definitions above, a record is compared as the whole document and deploys in the last wave with the other additional types.
Apex Classes and Apex Triggers are a code type: each component is its source body (.cls/.trigger) plus an XML sidecar carrying apiVersion and status. Comparison covers the body text and the sidecar attributes together, so an Update row can come from a body change, a sidecar change, or both. The row diff view shows the source body file. Visualforce Pages have the same shape: the .page body plus a .page-meta.xml sidecar carrying apiVersion and label.
Lightning Web Components and Aura Components are bundles: one component is a whole directory (lwc/<Name>/, aura/<Name>/), so they are compared by composition rather than by attributes. An Update row means the set of files or the content of at least one file differs, summarized as N file(s) added, M removed, K changed (terms that are zero are omitted); the row diff view shows the whole bundle as one concatenated document, each file preceded by a === <relative path> === separator line. Deploying a bundle replaces that bundle in the target completely: a file that exists only in the target's version of the same bundle is gone after the deployment, because the Metadata API treats a bundle as one indivisible component. The plugin still never deletes a target component: a bundle that exists only in the target produces no row and is never touched.
Page Layouts are decomposed: one row is not a whole layout but a single layout element — a field of a section or a related list. A layout that is missing from the target produces one Add row for the whole document; a layout that exists in the target produces rows only for the elements that differ, and matching elements produce no row at all. An element that exists only in the target never produces a row and is never removed. Position is never a difference: the plugin transfers a new element into the position its source neighbours dictate, but it never re-orders elements that already exist in the target. A selected related-list Update row replaces the whole related list with the source version — its columns, sort field, sort order and buttons are all overwritten, so anything present only in the target's copy of the same related list is gone after the deployment; everything else in the layout — the layout's own buttons, quick actions, the highlights panel and the mini layout — is preserved by construction: existing lines are never rewritten. Layouts of standard objects are compared and deployed. The row difference view shows the whole layout document of both orgs, the same document for every row of that layout. A layout whose own name contains :: is excluded from the comparison with a visible warning, because the plugin uses :: to address layout elements. A layout whose metadata cannot be parsed is not softly excluded: reading that layout's metadata fails the operation outright, the same way an unreadable metadata file fails any other type.
Types outside the core set — every type of the thirteen additional categories — are compared as the whole document, not attribute by attribute: one row is one component, and the row difference view shows both documents in full. Their component name is the fullName the Salesforce CLI reports, so a tab of a custom object is listed as Invoice__c and a quick action as Account.New_Note. Namespaced components of these types are excluded like every other namespaced component, and a name that is a custom object's own API name is not mistaken for a namespace prefix. Types that Salesforce stores inside an object's folder — list views, record types, validation rules, field sets, compact layouts, business processes, indexes, sharing reasons and web links — are listed under a composite Object.Name key such as Account.AllAccounts or Invoice__c.My_View, and their file lives in that object's folder. Children of a standard object are compared and deployed like any other child, because a list view or a validation rule is the admin's own configuration rather than the object definition; the standard object's own definition is still never deployed. Selecting a child of a custom object that is missing from the target auto-checks that object's definition Add row as a visible dependency — you see it in the table before the deployment and you can uncheck it. The object's own custom-field rows are auto-checked right alongside it, because a list view's columns or a validation rule's formula name those fields, and the target rejects the child if the object is created without them; each field row can be unchecked on its own, just like the object row. Like the offline layout auto-select, this widening runs when the source is an org; with a repository source neither the object's row nor its field rows are added automatically — select them yourself. Types that Salesforce stores many-to-a-file — assignment rules, auto-response rules, escalation rules, the three kinds of sharing rule and custom labels — are listed one row per rule or label, not one row per file: a rule is keyed Object.Name such as Lead.Web_Leads, and a custom label by its own name, because a single CustomLabels file holds every label in the org. The three sharing-rule types share one physical file per object and are told apart by the element that carries them, so an owner rule and a criteria rule of the same object are two independent rows that deploy together into one file.
Flows are the opposite of Page Layouts: one row is one whole flow. Four header attributes are shown as a short diff — label, status, API version and process type — and any other difference in the document is reported as Definition changed. Version numbers are never compared, because the flow document does not carry one: the Metadata API decides which version it hands over, so the active version of a flow is compared, and when a flow has no active version its latest version is compared instead. An Update row can therefore mean that a different version is active in each org rather than that somebody edited the flow. The <status> value is transferred verbatim, so deploying a flow whose source status is Active activates it in the target; that status is visible in the row before you deploy, and Validate only is available as a dry run. Deploying a flow creates a new version in the target and never deletes or deactivates an existing version. A flow that exists only in the target produces no row and is never touched. This comparison has a known limitation for a git source compared against an org target: the plugin only normalizes line endings, so any other formatting difference between the repository's copy of a flow and the org's own serialization — indentation, element order, manual edits — still reports as Definition changed, and deploying does not clear it, because the target org re-serializes the deployed document on its own terms; every redeploy from that repository copy then adds another version to the target. The other three endpoint pairs are unaffected: org to org passes through the same Metadata API retrieval on both sides, so their serializations match by construction; org to git and git to git write exactly the retrieved or merged bytes, so the compared bytes are exactly the deployed bytes.
Dependency Auto-Selection
When you check a row in the comparison table, the plugin also checks every component that row depends on and that is missing from the target. The dependency data comes from the Salesforce Tooling API object MetadataComponentDependency, queried on the source org only; that object is a Salesforce Beta feature, so the plugin treats it as best effort.
The cascade also runs the other way for a new object: checking a new object's Add row also checks every custom-field row of that object, while checking a single field still checks only that field's object and not its sibling fields. Auto-checked rows are marked added as dependency together with the row that pulled them in, and you can uncheck any of them — the auto-selection is a suggestion, never a lock. Unchecking a row also unchecks the rows that were only there because of it; a row you checked yourself is never unchecked automatically. Dependencies are followed recursively, so checking one field can bring in its object and the Apex class they reference.
Only rows the target is missing (Add rows) are ever auto-checked: an Update or No change dependency already exists in the target and needs no transfer. A dependency the plugin cannot transfer — a standard component, a packaged or namespaced component, or any component type outside the eleven the dependency graph covers (CustomLabel, RecordType, WorkflowRule, and the like) — is simply not auto-checked and never appears in the table. The one exception is a Flow dependency whose metadata name came from the Tooling API as its display name instead of its API name: it could not be matched automatically, so the row reports it, and you should make sure it exists in the target org or select it manually. Page Layouts are one of the eleven the dependency graph covers, but the Tooling dependency graph carries no layout references, so this feature never pulls a layout in as another component's dependency; a layout element's own dependency on a custom field is instead auto-selected offline, without the Tooling API, as described above. That offline auto-select also covers a whole-layout Add row: when the layout itself is missing from the target the comparison shows one row for the whole document, and that row carries the dependencies of every element it contains. A related list element is covered too: when the list is named {child object}.{lookup field}, the lookup field itself (if it is a custom __c field), every column of the list written as a plain custom field API name, and the sort field are auto-selected on the child object. A column written as a UI name (CONTRACT.NAME, OWNER_NAME) or as a person-account field (__pc) is left alone, and a standard related list — a name without a dot — has no dependencies at all.
If the Tooling API object is unavailable — the Beta feature is off in the org, or the query fails — the comparison still completes exactly as before: no Tooling-sourced row is auto-checked and the screen shows Dependency auto-selection is disabled with the reason. The two offline rules still apply — a field still pulls in its missing target object, and checking a new object still checks its own field rows. The plugin never blocks a comparison because of this feature. Because the Salesforce dependency graph has known gaps, a missing edge does not prove there is no dependency; the deployment validation stays the source of truth.
The comparison may pull extra components into itself to resolve a dependency; the screen lists them under Added to comparison as dependencies. That expansion lives only in the comparison — Back returns your original selection screen untouched. Only the rows something you asked for actually needs are shown: a component pulled in this way gets a row when it is reachable over dependency edges from a row of your own selection, and that row's summary then names the row that required it. An object that ends up with no rows at all drops out of that list as well, so the list never sends you looking for a row that is not in the table. Everything else the expansion downloaded stays out of the table — to work on such a field, select its object on the selection screen explicitly.
Row Difference View
Double-click any row in the comparison table to open a side-by-side diff of the
component's Salesforce DX source-format file: the source org on the left, the
target org on the right. The view is built from the metadata already retrieved
for the comparison, so it makes no additional org calls. For an Add row (or a
row whose target object is missing) the right pane shows that the component is
not present in the target. Complex components such as a CustomObject show only
their object-level source (the source format keeps each field in its own file),
never an enumeration of their fields.
Progress Details
While an operation runs, the status text is joined by a live progress line: a spinning indicator, the current phase, the step counter where the number of steps is known, and the elapsed time since the operation started, followed by a bar that fills when the step count is known and runs indefinitely when it is not. The facts come from the core — it is the only layer that knows which Salesforce CLI command is running — and the line carries metadata type names, org aliases and counters only; CLI output is never shown there.
Examples: Loading objects from dev · 4s, Querying dependencies — 3 of 7 · 12s,
Deploying Custom Fields — 2 of 5 · 1m 8s. The step count is a live estimate: while
dependencies are queried it can grow as the core splits an oversized batch, so 3 of 7
may be followed by 4 of 9.
Some steps run without leaving the screen you are on. The screen that started such a step stays where it is, disables its buttons and shows the same live line, and Cancel stays available. There are three such steps today: listing the Apex test candidates after Deploy on the comparison screen (when the package contains Apex or Flow and the target is an org), matching FLS containers after Deploy on the comparison screen (when FLS mode is not "Do not transfer FLS"), and matching FLS containers after you confirm the tests on the Apex test screen.
The line is optional. In the IntelliJ plugin the checkbox lives in
Settings | Tools | Orgbench; in the VS Code extension it is the setting
orgbench.showProgressDetails. Both are on by default. Clearing the checkbox stops
the line: nothing new is drawn, and a line already on screen disappears with the next progress event or state change —
inside one long CLI phase that can take a minute; the core stops sending progress the next time the core process starts —
the plugin never restarts the core to apply this setting, because that would discard the
current comparison. If no progress event ever arrives (a core that does not implement the
channel, or the channel switched off), the screens behave exactly as they did before: the
static status text and the Cancel button.
Settings and Session Log
The FLS transfer mode and the deployment mode are settings, not controls on the review screen. In
the IntelliJ plugin they live in Settings | Tools | Orgbench, reachable in one click from the
gear button in the tool window; in the VS Code extension they are orgbench.flsMode and
orgbench.deployMode, reachable from the gear icon in the view title. Because they are settings,
they no longer reset with every new comparison: a mode you chose once stays chosen. The review
screen therefore always shows what the next deployment will do as a read-only line, for example
FLS: Additive · Deploy: Validate only.
The core writes a session log — one JSON object per line — that answers two questions: how long an
operation took and what it was made of, and what exactly failed when you need to file a bug
report. Each line carries a timestamp, an event kind and an operation id. The line that answers
"where did the two minutes go" is operationFinished: it carries the operation kind, its duration,
how many Salesforce CLI calls it made and how many milliseconds were spent inside them. A cli
line records one sf invocation: the subcommand, the target org alias, the duration, the exit
code, the byte lengths of stdout and stderr, and — on failure — the same sanitized diagnostic the
UI shows. An operationCancelled line covers two distinct reasons and does not distinguish them in
its fields: either the user chose Cancel, or
the core force-aborted the operation after its cleanup failed.
Metadata categories is a setting of the same kind: it decides which categories of metadata
types the Metadata Type dropdown offers. In the IntelliJ plugin it is a list of checkboxes on
the same settings page; in VS Code it is orgbench.metadataCategories, an array of category ids.
By default only the Core category is enabled, so the dropdown lists exactly the types the plugin
offered before the metadata-type expansion. Changing it takes effect on the screen you are already
looking at. Queues, public groups and roles are the three types Security & Sharing does not offer yet:
their documents carry a member list of the source org, and the plugin does not yet know how to create
such a component without it. Other types whose documents reference users or profiles are offered as
they are, and a deploy that names a user the target does not have fails that one component in Best-effort mode; in Validate only or All or nothing the whole package fails, as everywhere else.
Disabling a category does not clear a selection already made in a type it hides: that selection
stays in the session manifest and is still sent to Compare, Deploy and Export Manifest, even
though the type itself is gone from the dropdown. A line below the manifest summary names every
such hidden type and how many of its components are still selected, and choosing Compare while
any such selection exists shows a confirmation listing those types and counts before continuing.
This is true of both clients, not only the IntelliJ plugin.
Three levels are available, chosen in the same place as the modes (orgbench.logLevel in VS
Code):
- Off — nothing is written; neither the log file nor its folder is created, and the retention sweep that deletes older logs does not run either.
- Standard (the default) — every kind of record listed above.
- Detailed — the same plus the full Salesforce CLI argument list of each call. Detailed additionally records the SOQL request text, because it is part of those arguments, and for the same reason it records local file paths such as the retrieve manifest.
The log never records a token, and it records no response body either — with one deliberate
exception: when an operation fails, the log keeps
the same sanitized diagnostic the UI already shows, capped in length. For a failed Salesforce CLI
call that diagnostic quotes an excerpt of the CLI response; for a failure with no CLI call behind
it — a malformed package.xml, say — it is the message you see on screen. Otherwise only the byte
lengths of stdout and stderr survive, and every recorded argument passes the same redaction the
plugin applies to displayed diagnostics. Changing the level takes effect immediately, without
restarting the core process.
The file lives under the plugin's own temporary directory, in orgbench/logs, and is named
session-<timestamp>-<pid>.jsonl. The plugin keeps the five most recent session logs and deletes
older ones; it removes only files matching that name pattern and never anything else in the
folder. A session log is capped at 8 MiB, after which a final logTruncated line is written and
the session stops logging. The settings page shows the current path and offers Open Log Folder
and Copy Path; VS Code offers the same two as commands in the view's overflow menu. The file
appears with the first record, so a freshly started session with nothing logged yet has a path but
no file.
Deployment Behavior
Only the selected component XML is copied into a clean attempt directory. The plugin does not deploy complete object metadata or unrelated fields; deploying a CustomObject writes only that object's .object-meta.xml, never touching its existing fields or other components in the target.
Creating an object (an Add row of Custom Objects, Custom Metadata Types, or Platform Events) deploys a minimal object document synthesized from the compared attributes plus, for Custom Objects, the required nameField, instead of the source org's full file: search layouts, list views, web links, and action overrides reference components that do not exist in the target yet, and Salesforce rejects the whole object because of them. This holds in all three deploy modes — the wave path and the single-package path build the object document the same way. If the source Custom Object carries no nameField, the plugin refuses to package it instead of sending the full file: in Best-effort that object's row fails on its own and its fields are Not Attempted, while on the single-package path the whole deployment fails before any deploy call. An Update row still deploys the source file as it is, and the Row Difference View always shows the real source content.
Deployment runs in dependency waves by metadata type: custom objects (wave 1) deploy before custom fields (wave 2), Apex Classes (wave 4) deploy before Apex Triggers (wave 5), the UI layer — Lightning Web Components, Aura Components, and Visualforce Pages — deploys in wave 6, Page Layouts deploy in wave 7, Flows deploy in wave 8, and the remaining metadata types deploy last, in wave 9. Within the object, field, UI, layout, flow, and remaining-types waves, components are sent in batches of at most 10. A Salesforce component failure causes the batch to split recursively so unaffected components can still succeed. Isolated component failures are retried once after the others, allowing a dependency deployed later in the selection to satisfy a formula or relationship field. Separate successful batches remain committed; there is no cross-batch rollback. If a selected object's Add fails to deploy, its fields are marked Not Attempted because their target object failed to deploy. Because a layout's merged target document is prepared right before deployment, a failure to prepare a selected layout stays isolated to that layout's own rows in Best-effort mode and against a git target, but in Validate only or All or nothing against an org target the same failure fails the whole package before any deploy call.
Apex Classes and Apex Triggers each deploy atomically within their wave: all selected components of that type are sent as one Metadata API deployment, with no batching into groups of 10 and no isolated-failure retry, because Apex compilation validates the whole set together. Apex rows are selectable for any target org. Before deployment, the test screen opens when the package contains Apex or Flow — an Apex Class, an Apex Trigger, or a Flow anywhere in the selection — and the target is an org; a git target never opens it. In that case Next opens a dedicated Select Apex Tests screen. That screen offers only a list of candidate classes with checkboxes — the candidate list combines the Apex classes already present in the target org (namespaced classes excluded) with the @isTest classes being deployed in this very selection, marked This deployment or Target org; classes whose name contains Test are listed first, and the list is searchable. Deploy is always enabled: an empty selection is a legal choice — a sandbox target then deploys with no tests run at all, but a production target still runs its own local Apex tests and requires 75% coverage when no test level is requested. Checking one or more classes runs RunSpecifiedTests; the checked test class names are passed to the Salesforce CLI as separate arguments (--test-level RunSpecifiedTests --tests <Class> repeated per class), and only names from that published list are accepted. Running every local test in the org is not offered as an option. In Best-effort mode, a non-empty test selection merges the Apex Class and Apex Trigger waves into one atomic Metadata API deployment, so the chosen tests run once instead of twice; the tradeoff is that a failing class then rolls back the triggers too. An empty selection leaves the two waves separate, as elsewhere. Flow waves in Best-effort never run Apex tests, regardless of the selection. When a deployment is rejected for insufficient code coverage, the target's own coverage warnings are read from the response and matched to the class they name: that class's own row gets a personal message with just its warning text, for example Code coverage: Test coverage of selected Apex Class is 0%, at least 75% test coverage is required, while any other row in the same rejected batch gets the shared rejection text — the same reply, still naming every flagged class — for example Code coverage: MyClass: Test coverage of selected Apex Class is 0%, at least 75% test coverage is required — instead of a bare status word.
Within the metadata waves themselves, the plugin distinguishes two kinds of failure. A target rejection is Salesforce answering with a terminal verdict that rejects what was sent — an Apex compilation error, a failed test, content the org refuses, a component-level verdict that arrived malformed or that could not be mapped back to exactly one requested component. It fails only the components of that one deployment call: every row of that batch is Failed with the sanitized rejection text, the remaining batches of the same wave are still sent, and every later wave still runs. When a target rejection carries no per-component detail, every component of that call gets the same message, and the plugin does not split the batch to find the culprit. A connection failure is the target never answering at all — authentication, authorization, CLI, network, an unrecoverable timeout, a malformed or missing response, a job that never reaches a terminal state, or a workspace failure. It is fatal: the current attempt is Failed, later components and later waves are Not Attempted, and no new batch is scheduled. Cancellation stops everything the same way. An object wave is the one contained case of a connection failure (cancellation still stops everything): a fatal failure there still stops that wave immediately, the fields of its failed objects are Not Attempted because their target object failed to deploy, but the remaining waves — the other object groups, the fields of objects that already exist in the target, Apex, and the UI layer — still run. Validate only and All or nothing are unaffected by this split: they make a single deployment call for the whole package, so both kinds of failure produce the same all-rows-failed report there. The plugin does not use --ignore-errors, delete target fields, or perform a destructive deployment; it creates a target object only when you explicitly select that object's deployable CustomObject row. The separate FLS step that follows the waves has its own, more forgiving failure handling — see FLS Transfer Modes.
At comparison time, the plugin records both aliases, usernames, and org IDs. Before deployment it reloads the aliases and rejects the operation if either alias no longer resolves to the reviewed username and org ID. Salesforce operations within the reviewed session use those stable usernames rather than trusting a later alias reassignment.
The Deploy mode setting offers three mutually exclusive choices, defaulting to the current behavior:
- Best-effort deployment (default) keeps the wave pipeline described above: batches of at most 10 with recursive split, isolated-failure retry, per-object skip, an inter-wave stop on a connection failure that an object wave contains, a target rejection contained to its own deployment call, and a separate FLS deployment after the waves, whose own non-cancellation failures are contained rather than fatal to the metadata report (a cancellation still stops the whole operation; see FLS Transfer Modes). Partial success is allowed.
- Validate only builds the same package but calls the CLI with
sf project deploy start --dry-run, so nothing changes in the target org. Results report Would succeed / Would fail instead of Succeeded / Failed. - All or nothing sends every selected component of every type plus merged FLS in one atomic Metadata API deployment; any failure rolls back the whole package (
rollbackOnErroris on by default), and non-culprit rows are reported failed with a transaction-rolled-back message.
Validate only and All or nothing take a single-package path (one deployment for the whole selection); Best-effort keeps the multi-call wave path. The plugin never uses --ignore-errors in any mode and never deletes target components. The test screen offer is the same in all three modes — it opens once, before the selected mode starts, whenever the package contains an Apex Class, an Apex Trigger, or a Flow and the target is an org — but what the chosen tests attach to differs. In Best-effort, a non-empty selection merges the Apex Class and Apex Trigger waves into one deployment so the tests run once; Flow waves never run them. In Validate only, the single package carries the selection and really runs the chosen tests (nothing is written to the target), including against a Flow-only package. In All or nothing, the single package carries the selection too, and it rolls back the whole package when a test fails, including against a Flow-only package. When tests ran, the results screen adds a Tests: N run, M failed line, and failed test names with their sanitized messages appear in the failing operation's details.
FLS Transfer Modes
The mode is chosen in the settings (see Settings and Session Log), not on the review screen, and it applies to every deployment until you change it.
The FLS mode setting offers three choices:
- Do not transfer FLS is the default and deploys selected field metadata without retrieving or changing FLS.
- Additive FLS transfers source access for selected fields without revoking existing target access. Read and Edit are merged independently with OR semantics, so the result is the union of normalized source and target access.
- All FLS fully synchronizes each selected eligible field to normalized source access in every matched container, so it can grant or revoke access.
Additive FLS and All FLS include selected No change fields immediately and selected Add or Update fields only after their metadata deployment result is Succeeded. A selection containing only No change rows skips field deployment entirely. The plugin matches source and target standalone Permission Sets by metadata full name and Profiles by metadata full name. Namespaced permission containers and Permission Set Groups are excluded.
Choosing Additive FLS or All FLS and Next opens a container-selection screen listing every eligible container: Existing containers matched by kind and API name in both orgs, and New Permission Sets that exist only in the source and are eligible for creation. Each row shows its presence in an Existing/New column; filter it with the Presence column's multi-select, which shows both values by default. Select All and Clear apply only to the currently visible, filtered containers. Every selectable row starts checked — every Existing container and every New Permission Set; a New Profile is listed for information only, with its checkbox disabled, because Profiles are never created. Deselect any selectable container to exclude it from this deployment. A container that is left unselected is reported Skipped with "Not selected for FLS transfer."; this is an additional, user-driven Skipped reason alongside target-only, namespaced, and other ineligible exclusions, and it does not change any other result semantics.
The plugin retrieves both copies of each matched Permission Set/Profile and uses the target XML as the merge base. All selected fields for that container are accumulated into one metadata document. Only their fieldPermissions entries are merged according to the selected mode; unrelated target field permissions and all other target metadata nodes remain intact. Edit access implies read access, formula fields are normalized to editable=false, and required fields are skipped because Salesforce does not support FLS for them.
In Additive FLS, an absent or narrower source fieldPermissions entry never removes or reduces target access. In All FLS, an absent source entry means no read or edit access, so synchronization removes the corresponding target entry. Target-only containers are Skipped; the plugin never deletes a target-only container. A source-only Profile is also Skipped, because Profiles are never created. A source-only, non-namespaced Permission Set is classified New; when selected, the plugin may create a Permission Set in the target when it is missing there, populated with only the selected fields' field-level security — never a license, never any other permission, and never a Profile. Profiles that do not exist in the target are not created. A selected field whose desired access already matches the target is also Skipped because it requires no deployment.
All FLS can make destructive changes (access revocations) only for a unique, non-namespaced source-and-target container match by both kind and API name. Target-only, namespaced or otherwise ineligible, required-field, and no-op outcomes are Skipped; a source-only Profile is also Skipped, but a selected source-only, non-namespaced Permission Set is instead created in the target and reported Updated. Ambiguous duplicate container identities and Salesforce deployment rejections are Failed. In the FLS results table, the Status column filter hides Skipped rows by default; check Skipped in that column's filter to reveal them.
Changed Permission Sets and Profiles are first submitted together in one aggregate Metadata API deployment. A component-specific failure causes recursive splitting by container until the failing Permission Set/Profile is isolated; the plugin never splits one container's accumulated field changes into per-field deployments. A container is reported successful only after a deployment containing that component succeeds. Fatal authentication, authorization, CLI, network, workspace, cancellation, or response errors stop further scheduling. Updated means an actual permission-metadata change was deployed; no-op rows are Skipped. There is no rollback across separate successful fallback deployments.
Before any of that scheduling starts, the plugin has to read the Permission Set/Profile catalog itself — the SOQL query that lists candidate containers and the retrievable-metadata inventory that tells which of them can actually be retrieved. Each of those two read-only calls is retried once on a failed CLI response, the same one-shot idea as the metadata batch's own isolated-failure retry above; a cancellation is never retried, and it still stops the whole operation like any other cancellation. In Best-effort mode against an org target, if that early catalog read still fails after the retry, the FLS step does not take the metadata report down with it: every component deployed in the waves above keeps its own result row, and every field/container pair that would have gone through FLS is instead reported Not Attempted, with the sanitized failure message available in Show Details. In Validate only and All or nothing, and against a git target, the same catalog failure stops the whole operation before anything is deployed or committed.
The FLS result table's column filters do not change the counters. The Status column filter defaults to Updated, Failed, and Not Attempted checked and Skipped unchecked; check Skipped to include it, or uncheck any of the other three statuses to narrow the view further.
Safety Boundaries
- Salesforce authentication remains in the local Salesforce CLI. The plugin does not request, persist, or display access tokens, refresh tokens, passwords, or auth URLs.
- CLI commands are executed directly as argument lists, not through a shell. Diagnostics are sanitized before display or copy.
- Namespaced objects, fields, Permission Sets, and Profiles are excluded. Ordinary custom names such as
Field__cremain eligible. - An object is created in the target only when its
CustomObjectAddrow is checked in the comparison table — either by you or automatically as a visible dependency of a component you checked; the auto-check can be removed, and the object creation is always visible in the table before the deployment. Target-only fields are never deleted, and standard-object metadata is never deployed: the standard object's own definition and its standard fields are out of scope. Page Layouts and the types stored inside an object's folder (list views, record types, validation rules, field sets, compact layouts, business processes, indexes, sharing reasons, web links) are the deliberate exceptions — those of a standard object are compared and deployed, because they are the admin's own configuration rather than the object definition. - Deploying one child of a container document — an assignment rule, an auto-response rule, an escalation rule, a sharing rule or a custom label — never removes the other children of the same container. The package carries only the children you selected, and the Metadata API leaves the rest in place; against a git target the plugin merges the selected children into the branch's own copy of that file, so a rule or a label that exists only in the repository survives the deployment.
- A selected related-list
Updaterow replaces the whole related list with the source version — columns, sort field, sort order and buttons are all overwritten, so anything present only in the target's copy of the same list is gone after the deployment. This is the only place where a deployment removes content from a layout; everything the merge did not touch is preserved by construction, and elements that exist only in the target are never removed. - Deploying a flow creates a new version in the target and never deletes or deactivates an existing version. The target's active version is replaced exactly when the source flow's
<status>isActive, because the status is transferred verbatim and is visible in the comparison row before the deployment. - Optional FLS synchronization is limited to selected metadata-identical fields plus selected fields whose metadata deployment succeeded. For an Existing container it merges only the selected
fieldPermissionsentries into target-based Permission Set/Profile metadata, and only for the containers the user selected on the container-selection screen; deselected containers remain untouched and are reportedSkipped. The plugin may create a Permission Set in the target when it is missing there, populated with only the selected fields' field-level security — never a license, never any other permission, and never a Profile. Profiles that do not exist in the target are not created. - The IntelliJ system directory is the explicit local filesystem trust boundary. The plugin parent is its direct child
orgbench; operation workspaces below it contain no credentials. On Windows the plugin parent and descendants are created with owner-only ACLs. Workspaces are closed after handled success, failure, cancellation, Back, New Comparison, or project disposal. If cleanup fails, the error is surfaced and the owned workspace is retained for another cleanup attempt on reload or close. - Importing a
package.xmlchanges only the local selection. It never touches either org beyond the standard source-org component listings, never imports namespaced members, and rejects aDOCTYPEdeclaration outright; any failure leaves the previous session manifest in place. - Separate successful field batches and FLS fallback deployments are not rolled back when later work fails. Always review both result tables before treating an operation as complete.
- Automated tests use fixtures and fakes; the default Gradle test task does not connect to a Salesforce org.
- Automated orchestration tests cover max-10 field batching, recursive field splitting, dependency retry, aggregate FLS deployment, container-level FLS fallback, fatal stop, and
Not Attemptedordering. The manual guide deliberately validates one real field component failure without intentionally causing an authentication or network failure against a live org.
All four tables — object selection, field review, FLS container selection, and both result tables — share one filterable layout: a header row, an aligned per-column filter row beneath it, and the data. Column widths are saved globally per table in IntelliJ application properties after a header resize; the compact default sizes any Select column to its actual checkbox header, keeps narrow columns like Change and Status compact, and leaves the widest text column (Diff, Message) expanding. The filter row re-aligns each filter cell to its column as columns are resized. Saved table widths apply across projects and IDE restarts and are clamped to safe minimums when restored.
Build
Use a JDK 17 runtime and the checked-in Gradle wrapper:
./gradlew test
./gradlew verifyPlugin
./gradlew buildPlugin
./gradlew buildPlugin also compiles the Rust core (sf-core-server) for macOS arm64, macOS x64 and Windows x64 and bundles all three under bin/ in the archive. The Windows x64 binary is cross-compiled from a non-Windows host, which needs a one-off cargo install cargo-xwin plus rustup target add --toolchain 1.97.0 x86_64-pc-windows-msvc; on a Windows host the same Gradle task calls plain cargo build. The Rust core has its own checks:
cd core && cargo test --locked
cd core && cargo clippy --all-targets --locked -- -D warnings
The VS Code extension lives under vscode/ with its own npm toolchain; ./gradlew check also type-checks and runs its node:test suite, and ./gradlew buildVsixAarch64 buildVsixX64 buildVsixWin32X64 writes the platform-specific VSIX archives under vscode/.
verifyPlugin can download the configured IntelliJ compatibility target. The installable archive is written to build/distributions/orgbench-0.1.1.zip. Build and verification do not require an authenticated org and do not run the manual live-org scenario.
Troubleshooting
Salesforce CLI is unavailable or incompatible
Run sf version --json in a terminal. Install or update Salesforce CLI, restart the IDE, and choose Restart. The plugin automatically checks the IDE PATH plus standard install locations. If sf is installed in a custom location, add its directory to the environment used to launch IntelliJ IDEA.
The Salesforce core process did not start
The plugin launches a bundled sf-core-server binary from its bin/ directory. If the tool window reports that the core process could not start or terminated, confirm the platform is supported (macOS arm64, macOS x64, or Windows x64) and that the plugin ZIP was installed from disk without unpacking. Reinstall the plugin if the bundled binary is missing. On Windows the bundled binary is unsigned, so SmartScreen or the antivirus may quarantine it on first launch.
On Windows, three further messages come from the core refusing to trust its own temporary workspace rather than from a startup failure: Could not read filesystem identity means the project or IDE cache directory sits on a filesystem (FAT/exFAT, or some network volumes) that does not give files a stable unique identity; plugin temp parent is not a regular directory means that directory is a reparse point, including a cloud-sync placeholder such as an unhydrated OneDrive folder; plugin temp parent must be writable only by its owner means its access control list grants write access beyond the current user. For the first two, move the project (and, if the message names the IDE cache path, that cache) to a local NTFS volume outside any cloud-synced folder. The third one is most often a directory created directly on a drive root, which inherits a write grant for a broad group such as Authenticated Users; a local NTFS volume does not help there. Move the directory under %USERPROFILE%, or drop the inherited grant with icacls <dir> /inheritance:r /grant %USERNAME%:(OI)(CI)F.
No aliases are listed
Run sf org list --json. Authenticate aliases if needed, then choose Reload Orgs. Entries without an alias are intentionally not shown.
Source and target are rejected as the same org
Choose aliases that resolve to different org IDs. Two different alias names can still point to one org.
No components or differences are shown
Confirm that the source user can query metadata and that the component is customizable and non-namespaced. Target-only fields, standard fields, and namespaced components are intentionally omitted. Metadata-identical fields appear as No change; by default the review Change filter hides them (only Add and Update are checked). On the first screen, the Label and API Name column filters each match a case-insensitive prefix, not any substring; switching Metadata Type loads that type's own component list without discarding selections already made for other types.
A target-object row is disabled
The target object does not exist. Either create the object outside this plugin and compare again, or select its custom CustomObject row in the same deployment so the object is created before its fields.
An alias changed after review
The plugin intentionally aborts before mutation if an alias no longer maps to the reviewed username and org ID. Choose Back or New Comparison and repeat the comparison.
Deployment or FLS has failed rows
Open Show Details for sanitized Salesforce messages. Check Metadata API permissions, field dependencies, target validation rules, and exact Permission Set/Profile name matches. Missing target containers are skipped; a failing permission component is isolated by container when Salesforce returns a component-specific failure. If every FLS row is Not Attempted with the same message, the failure happened earlier, reading the Permission Set/Profile catalog itself, before any container-specific deployment was attempted; the field metadata rows above it are unaffected.
Cancellation or temporary cleanup reports an error
Choose Restart to start a fresh workflow and retry cleanup owned by the current plugin session. If the error persists, close the project after the active Salesforce CLI process has stopped and inspect the IDE system directory before removing anything manually.
Manual Validation
The live-org smoke test is intentionally separate from automated builds because it mutates Salesforce metadata and FLS. Follow docs/manual-smoke-test.md only with disposable source and target orgs, record each expected result, and dispose of the test orgs afterward.