Docs/Operate

Operate

Run local services, manage project state, connect orgs, and tune runtime behavior.

Commands

nimbus serve Pro

Start a local Salesforce-compatible API server. See the Local Server guide for full documentation.

In JetBrains IDEs the same server starts from the Server tab of the Nimbus tool window's Data workspace, which also shows every request it answers and everything it prints.

bash
nimbus serve
FlagDefaultDescription
--addr127.0.0.1:8080Address to listen on
--api-version60.0Salesforce API version to expose
--usernameadmin@nimbus.localEmail for the default admin user
--passwordpasswordPassword for OAuth flow
--grpc-addr127.0.0.1:7443gRPC address for Pub/Sub API

nimbus app Pro

Feature overview: local app hosting.

Run any App preview target against the local Nimbus runtime: a Salesforce Multi-Framework UI bundle (React + Vite), an LWC component, or a Visualforce page. Replaces sf ui-bundle dev for offline development: no scratch org, no sandbox, no internet required. The dev experience is identical — HMR, source maps, npm run dev all work — but every /services/data/* and /services/apexrest/* call from the app hits Nimbus's Apex interpreter and embedded Postgres instead of a real org.

The targets you can run are exactly the ones nimbus app list prints, named by display name or by the stable id in that listing. LWC and Visualforce previews are deliberately org-free: they run on the local runtime and ignore --org, so starting one never becomes an authenticated describe.

What it does:

  1. Discovers React bundles under force-app/**/uiBundles/<name>/ (marker: *.uibundle-meta.xml), LWC components under force-app/**/lwc/<name>/, and Visualforce pages at **/pages/<Name>.page
  2. For React and LWC, spawns a dev server (default: npm run dev) — Vite, HMR, source maps, the lot. Visualforce is rendered by Nimbus itself at /apex/<Name>; no npm involved
  3. Starts a proxy on 127.0.0.1:4545 (same default as sf ui-bundle dev) that injects window.SFDC_ENV (basePath, accessToken, instanceUrl, apiVersion, userId) into the served HTML
  4. Routes the React app's API calls to the embedded Salesforce-compatible API surface (REST sobjects, SOQL query, Apex REST, GraphQL UIAPI with mutations and introspection, UI API REST, Connect API)
  5. Opens the browser
bash
# Auto-discover the single bundle in this project and run it
nimbus app

# Pick a specific target by name — React bundle, LWC component or VF page
nimbus app reactRecipes
nimbus app brewCoffeeCard
nimbus app BrewShowcase

# Disambiguate two targets sharing a name, by the id nimbus app list prints
nimbus app lwc:force-app/main/default/lwc/brewCoffeeCard

# Don't spawn vite — connect to one you started yourself
nimbus app reactRecipes --no-spawn

# Print the URL but leave the browser alone (CI, remote shells)
nimbus app brewCoffeeCard --no-open

# List every App preview target — React bundles, LWC components,
# Visualforce pages
nimbus app list

# The same listing as JSON, with each LWC package's component inventory
nimbus app list --json

# Generate a GraphQL SDL from your local SObject metadata (replaces
# "npm run graphql:schema" which requires a real org)
nimbus app schema > schema.graphql

# Or fetch the same SDL from the live server
curl http://localhost:4545/__nimbus/app/schema > schema.graphql

# Run the bundle's production build (npm run build, no deploy)
nimbus app build reactRecipes

# Run the bundle's own test suite (npm test)
nimbus app test reactRecipes

# Serve the built artifact instead of the HMR dev server (npm run preview)
nimbus app preview reactRecipes

# Score what the local LWC preview actually renders, per component
nimbus app scorecard

# The same run as a committable Markdown artifact
nimbus app scorecard --markdown > lwc-scorecard.md

API surface served (full coverage of what Salesforce Multi-Framework production apps reach for):

  • OAuthPOST /services/oauth2/token, GET /services/oauth2/userinfo (OIDC identity)
  • SObject REST/services/data/v{ver}/sobjects/<Object> full CRUD; POST /sobjects/ContentVersion handles multipart/form-data file uploads
  • SOQLGET /query?q=...
  • SOSL (Search)GET /search?q=FIND ... RETURNING ..., /parameterizedSearch — translated to per-object SOQL with LIKE clauses across text fields
  • GraphQL UIAPIPOST /graphql. Queries: first/after cursor pagination, multi-field orderBy with NULLS FIRST/LAST, full where (eq/ne/lt/lte/gt/gte/like/nlike/in/nin + and/or/not combinators that nest), byId shortcut, child-relationship subqueries via nested edges/node, parent lookups via dotted paths. Aggregates: count, sum(field:), avg(field:), min(field:), max(field:) on every Connection. Pagination: real cursors with pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor). Directives: @skip, @include, @optional. Variables: resolved from request payload. Mutations: <Object>Create, <Object>Update, <Object>Delete per SObject. Introspection: __schema served from a model built at startup — codegen tools work directly against the running server.
  • UI API REST/ui-api/object-info/<Obj> (describe with picklists + record types + child relationships + defaults), /object-info/<Obj>/picklist-values/<rtId>[/<field>], /records[/<id>] (GET/POST/PATCH/DELETE), /record-defaults/create/<Obj> + /clone/<id>, /list-ui/<Obj> + /list-records/<listId>, /mru-list-ui/<Obj> + /mru-list-records/<Obj>, /related-list-info/<Obj>/<rel> + /related-list-records/<parentId>/<rel>, /layout/<Obj>, /actions/record/<id>, /duplicates/<Obj>
  • Composite APIPOST /composite (allOrNone with referenceId chaining), POST /composite/batch (independent requests), POST /composite/tree/<Obj> (nested record insert), POST /composite/sobjects (multi-type batch), GET /composite/sobjects/<Obj>?ids=...&fields=... (multi-get)
  • Bulk API 2.0 — full lifecycle for /jobs/ingest (POST create → PUT batches CSV → PATCH UploadComplete → GET state + /successfulResults / /failedResults) and /jobs/query (POST → poll → /results). Nimbus processes synchronously on UploadComplete since the runtime is in-process; the polling shape stays faithful.
  • Apex Invocable ActionsPOST /actions/custom/apex/<ClassName> dispatches to the class's @InvocableMethod via the interpreter
  • Apex REST/services/apexrest/<path> for your @RestResource classes
  • Connect / Chatter/chatter/users/me (sourced from local User SObject if present), /chatter/users/<id>, /chatter/feeds/news/.../feed-elements, /connect/communities, /connect/cms/delivery/channels
  • Limits/limits returns a stub governor-limits envelope so diagnostic UIs render
  • Diagnostics/__nimbus/app/status (JSON), /__nimbus/app/schema (live SDL), /__nimbus/app/dashboard (HTML)
  • Everything else falls through to Vite (HMR websocket, @vite/client, source maps, assets)

What's still not covered: sharing rules / FLS enforcement, picklist value dependencies as bitmaps (validFor arrays are empty), per-record-type picklist filtering, JWT / OAuth web-flow authentication, Tooling API, Reports/Dashboards, Wave Analytics, Surveys, Knowledge, TLS / HTTPS, live schema reload on metadata change, and production deploy (build only — deploy goes through sf project deploy). Anything not in the served list returns 501 NOT_IMPLEMENTED with the path so you see exactly what to fill in.

Target selection:

  • One target in the project → used automatically
  • Name or id passed as argument → that target is used
  • Several targets, no argument, terminal → interactive numbered picker listing every kind
  • Several targets, no argument, non-TTY (CI) → errors with the target list
  • --allReact bundles only: every React bundle served simultaneously under /lwr/application/<name>/ with an HTML index page at root. Each bundle gets its own vite dev server with output prefixed by bundle name. Use --primary to pick which one opens in the browser by default. LWC and Visualforce targets are served one at a time — name one instead.
FlagDefaultDescription
--addr127.0.0.1:4545Address the proxy listens on (matches sf ui-bundle dev)
--api-version66.0Salesforce API version exposed via SFDC_ENV
--no-spawnfalseDon’t run npm run dev — assume vite is already running at the bundle’s dev.url
--no-openfalseDon’t open the browser on startup
--allfalseServe every discovered React bundle simultaneously (path-based routing under /lwr/application/<name>/). React only — LWC and Visualforce targets are served one at a time
--primaryIn --all mode, which bundle to open in the browser by default (first discovered if unset)
--installfalseRun npm install in the bundle directory if node_modules/ is missing (otherwise nimbus fails with a hint to install manually)

If the bundle has no node_modules/, nimbus fails early with the exact command to run rather than letting vite die with the cryptic sh: vite: command not found. Pass --install to have nimbus run npm install itself. Common vite failure modes (exit 127, EADDRINUSE) are recognised and surfaced with hints during the readiness wait — no more 30-second timeout waiting for a process that's already dead.

Subcommands:

FlagDefaultDescription
list--jsonPrint every App preview target nimbus can see — React UI bundles, LWC components and Visualforce pages. --json emits the same listing as a structured document, adding each LWC target’s component inventory (name, preview route, bundle files) and project-relative paths
schemaPrint a GraphQL SDL derived from your local SObject metadata — byte-identical to what the running server advertises via introspection
buildRun a React bundle’s production build (npm run build). Useful for CI: build once, deploy the artifact separately. React bundles only — these run npm scripts, and LWC/Visualforce targets have no package.json
testRun a React bundle’s test suite (npm test). Wraps Vitest or whatever test runner the bundle uses
previewServe a React bundle’s production build locally (npm run preview). The bundler’s own preview server, unmodified — no SFDC_ENV injection, no Nimbus API proxy
scorecard--json --markdown --browser --serve --provenance-file --timeoutScore every LWC component the preview can see, tier by tier. --json emits the structured report; --markdown emits the committable artifact; --browser off skips the mount tier; --serve boots a local runtime so wire adapters and Apex calls reach real data

build, test and preview are deliberately thin: Nimbus does not reimplement Vite or Vitest. Each discovers the bundle and runs that bundle's own package.json script (npm run build, npm test, npm run preview) in its directory, with stdio wired through to your terminal and its exit code passed back. What they add is the discovery and script resolution — a CI job runs nimbus app test and needs no bundle-aware logic of its own. A bundle with no package.json is an error rather than a silent no-op.

Their bundle selection is simpler than the dev server's: a named argument picks that bundle, a project with exactly one bundle needs no argument, and anything ambiguous is an error listing the candidates — there is no interactive picker on these three, TTY or not.

Note that preview starts the bundle's own preview server and nothing else. Unlike nimbus app, it does not stand up the Nimbus proxy, so the previewed build gets no SFDC_ENV injection and its /services/data/* calls do not reach the local runtime. It verifies that the optimised bundle builds and serves; verifying it against Nimbus's API surface still means running nimbus app.

scorecard measures the local LWC preview instead of describing it. For every component it can see, it records four tiers: whether the preview's own toolchain compiles the component, whether the real preview page mounts it in a headless browser, which platform data adapters (lightning/ui*Api, lightning/messageService, @salesforce/apex/*) it needs, and which other platform modules (@salesforce/*, lightning/*) it needs. The first two tiers are measured by running the real pipeline; the last two are classified statically against the renderer's own module tables, and the report says which is which rather than blending them. A component blocked at an unsupported platform import is reported as a declared boundary, not a defect — the renderer fails that import on purpose instead of returning invented platform data. Anything the machine cannot measure (no dependency cache, no headless browser) is marked unmeasured with the reason; it never becomes a pass, and Nimbus downloads nothing to produce a number.

--serve boots a local Nimbus runtime on an ephemeral port behind the measured page, so a mounted component's wire adapters and Apex calls reach the real interpreter and the embedded database instead of nothing. Without it a component that needs platform data still mounts — proving its modules loaded, not that its data path works — and the report says which of the two it measured. The run records what each mounted component actually rendered, which is the difference between a green row and evidence. It starts an embedded Postgres, so it is opt-in.

What the org-free renderer supplies. @salesforce/label/<ns>.<Name> and @salesforce/schema/<Object>[.<Field>] resolve at build time, the way the platform compiler resolves them: a label becomes the value your project's .labels-meta.xml declares, and a schema import becomes the same { objectApiName, fieldApiName } descriptor the platform emits. Neither reaches a server, in an org or here. A label your project does not declare fails by name rather than resolving to an empty string, and scoring a component set that has no Salesforce project behind it refuses labels outright — the report's environment block says which of the two applied.

lightning/uiRecordApi and lightning/uiObjectInfoApi are supplied too, wired to the UI API endpoints Nimbus already serves: getRecord, getRecords, getRecordCreateDefaults, getObjectInfo, getObjectInfos, getPicklistValues, getPicklistValuesByRecordType, the imperative createRecord/updateRecord/deleteRecord, and the pure helpers (getFieldValue, getFieldDisplayValue, the generateRecordInput* family). The records are real — rows in the embedded Postgres, written through the same handlers a React bundle uses, firing the project's own triggers. What is not there is the Lightning Data Service: no normalised cache, so two components wiring the same record issue two requests and a write does not re-emit to a sibling's wire. getRecordNotifyChange and notifyRecordUpdateAvailable exist only to invalidate that cache, so they are visible no-ops that announce themselves in the preview header rather than pretending to do something. The scorecard reports both modules as stubs for exactly this reason — the data is right, the behaviour is local.

@salesforce/apex/<Class>.<method> resolves to the same callable-adapter value the platform's compiler generates — one export that returns a Promise when called imperatively and provisions a property when used with @wire. The call reaches the local Apex interpreter and runs the project's own method against the embedded Postgres, returning what JSON.serialize would. Only methods annotated @AuraEnabled are reachable: anything else is refused by name, because a bridge that called any method on any class would be an arbitrary RPC into your source rather than a preview of Salesforce. A thrown Apex exception arrives as error.body.message / error.body.exceptionType, the shape an org sends for a controller call.

Two differences are reported rather than imitated. @AuraEnabled(cacheable=true) runs the method on every provision here — there is no Lightning Data Service cache to populate — and the response says so on X-Nimbus-Apex-Cacheable, which the preview surfaces as a header badge. If a cacheable method performs DML, something the platform refuses outright, Nimbus reports the statement count on X-Nimbus-Apex-Cacheable-Dml and lets the call stand instead of raising an exception whose exact org wording is not yet verified. refreshApex re-runs the request behind the value you hand it; with no cache there is no cross-component invalidation, and a value that did not come from an Apex wire is announced rather than silently treated as refreshed.

lightning/messageService and lightning/empApi remain blocked at compile time, as do the lightning/ui*Api families with no local endpoint behind them.

TypeScript components. A bundle whose class lives in <name>.ts is a component on the same terms as a .js one — discovered, previewed, hot-reloaded, and scored, with the scorecard's Lang column saying which language each row was written in. A .js beside a .ts resolves the way the platform compiler resolves it: .js wins. The bundle metadata stays <name>.js-meta.xml in both cases.

The renderer strips the type annotations ahead of the LWC compiler and pins two settings while doing it — experimentalDecorators: false and useDefineForClassFields: true — overriding whatever your tsconfig.json says. Both decide whether @api, @wire and @track are still decorators by the time the compiler sees them; with either the other way they are rewritten first and the component compiles, mounts, and silently has no public properties. Worth checking in your own build too: the failure has no error message.

The Pro gate is on nimbus app itself — the dev server and its Salesforce API surface. list, schema, build, test, preview and scorecard run on the free tier.

nimbus mcp

Start a Model Context Protocol server over stdio. AI coding agents (Claude Code, Cursor, and any other MCP-compatible client) call Nimbus's local test runner directly through structured tool calls instead of parsing CLI output.

The runner is initialised once at server start (database, project sources) and reused across tool calls. The agent disconnects when stdin closes; the server tears the runner down on exit.

The tool catalog, the setup files for each client, and the frozen span schema live in AI agents. This section covers the command itself. An agent can also get the catalog at runtime by calling nimbus_capabilities, which is always in step with the binary it is talking to.

Tools exposed: run_apex_tests, execute_anonymous, run_method, run_mutation_tests (Pro), get_test_failure, explain_failure, triage_failures, get_execution_trace, query_field_history (Pro), get_governor_usage, query, describe_schema, get_coverage, list_test_classes, query_graph, find_dependency_cycles, scaffold_metadata, nimbus_capabilities.

Operator visibility. The server logs each tool call to stderr in structured form (mcp.run_apex_tests.start, ...complete, ...timeout, ...failed) so you can tail the agent's MCP transport and see exactly what's happening. --verbose raises the log level to debug.

Does the verification loop actually help? That is an empirical question, so it gets an experiment rather than a claim. The Nimbus repository ships verifybench, a harness that measures an agent's fix rate on real failing Apex tests with and without this toolset, over a frozen, hashed task set. The harness and its methodology are available now; no registered run has been executed, so there is no result to report yet. We will publish the task set, the raw per-attempt records, and the report together, or not at all.

Resources exposed: nimbus://capabilities, nimbus://project, nimbus://parity, nimbus://coverage (and nimbus://coverage/{class}), nimbus://schema (and nimbus://schema/{object}). All read-only; they execute nothing and are never refused by a cap or by read-only mode.

Operator visibility. The server logs each tool call to stderr in structured form (mcp.run_apex_tests.start, ...complete, ...timeout, ...failed) so you can tail the agent's MCP transport and see exactly what's happening. A refused call logs too — mcp.refused.read_only or mcp.refused.budget. --verbose raises the log level to debug.

Register it with the AI clients on this machine:

bash
nimbus mcp install                    # every client detected here
nimbus mcp install --client cursor    # just one
nimbus mcp install --print            # the entry to paste elsewhere
nimbus mcp status                     # which clients are registered
nimbus mcp uninstall                  # take it out again

The entry carries the absolute path of your nimbus binary, because a GUI client's PATH is rarely the shell's. Symlinks are left alone, so a Homebrew upgrade does not break the registration. Only the nimbus entry is written: other servers, other projects and every unrelated setting in those files survive, and a second install rewrites nothing.

Each editor is registered once. VS Code keys its servers under servers rather than mcpServers, and Nimbus writes the shape that editor actually reads.

For a shared machine or CI, start it read-only — tests and Apex still run, but nothing they write survives the call and no files are written:

bash
nimbus mcp --read-only

Per-session ceilings (nimbus.mcp.max-test-runs-per-session, nimbus.mcp.max-apex-executions, nimbus.mcp.max-wall-clock) bound an unattended agent and are off until a project sets them. Full details under Safety caps.

Agentic Resource Discovery. Nimbus publishes an ARD catalog so compatible registries and coding agents can discover this MCP server from task intent. ARD only handles discovery: after selecting Nimbus, the client still installs the local binary and invokes nimbus mcp over stdio.

Stdio safety. Stdout is reserved for JSON-RPC framing. The runner writes nothing to stdout, and the process-global os.Stdout is rerouted to stderr for the server's lifetime so any stray prints elsewhere in the call tree can't corrupt the protocol stream. Use stderr for any out-of-band logging; --verbose is safe.

FlagDefaultDescription
--parallel0Worker count for run_apex_tests (0 = NumCPU). Set at server start; cannot be changed per call.
--coveragetrueCollect coverage so get_coverage works after every run. Set false in tight agent loops that never call get_coverage.
--read-onlyfalseRefuse any tool call that could leave a durable change behind — a committed write, or a file written into the project. Tests, anonymous Apex and method calls still run; they roll back.

Recommended skills

MCP exposes the primitives; skills are short playbooks that tell an agent when and how to use them. Install with nimbus skills install — the binary fetches from nimbus-skills and writes to the right path for your agent. Three skills to start:

  • fix-failing-apex-test — the inner loop. Read failure → narrow → edit → re-run, until green.
  • bootstrap-nimbus — set Nimbus up on a fresh SFDX project, including a CI snippet.
  • apex-coverage-uplift — raise coverage by writing targeted tests for uncovered branches.

nimbus skills

Install agent skills from the nimbus-skills repo. Skills are short, opinionated playbooks (one per workflow) that pair with MCP to give your agent both the primitives and the recipes to use them. The binary embeds no skill content — every install fetches from GitHub at runtime, so the skills repo evolves independently of nimbus releases.

Subcommands:

bash
nimbus skills list                    # available + which are installed locally
nimbus skills install fix-failing-apex-test
nimbus skills install all             # install every skill the agent supports
nimbus skills remove fix-failing-apex-test
nimbus skills path                    # print the install dir for the detected agent
nimbus skills list --json             # the same catalog, for editors and scripts

Agent detection. Run from your project root and Nimbus picks the right agent automatically:

  • .claude/ present → Claude Code, writes to .claude/skills/<name>/SKILL.md
  • .cursor/ present → Cursor, writes to .cursor/rules/<name>.mdc
  • .aider.conf.yml or CONVENTIONS.md present → Aider, writes a single CONVENTIONS.md bundle
  • AGENTS.md or opencode.json present → generic AGENTS.md (also covers OpenCode), writes a single AGENTS.md bundle
  • .kiro/ present → Kiro, writes to .kiro/steering/<name>.md with manual-inclusion frontmatter (load via #<name> in your Kiro prompt)
  • Nothing matched → defaults to Claude Code (use --global to write to ~/.claude/skills/ instead).

If two markers are present, Nimbus refuses to guess — pass --agent to disambiguate.

Idempotent. Re-running install on a skill whose local copy is byte-identical to upstream is a no-op. If the local copy differs, the install fails until you pass --force — protects hand edits.

FlagDefaultDescription
--agentautoTarget agent: claude-code, cursor, aider, agents-md, kiro
--globalfalseClaude Code only: install into ~/.claude/skills/ instead of <project>/.claude/skills/
--forcefalseOverwrite an existing local copy when its content differs
--jsonfalselist only: emit the catalog as JSON — each skill with the agents it ships for and the local copies already installed, plus the detected agent. An ambiguous detection still returns the catalog.

nimbus sync

Sync your project's schema (SObject definitions, fields, relationships) into the local database. nimbus test reconciles the schema automatically before every run, so an explicit sync is mainly for warming the database up front or after large metadata changes.

Syncing from an org (-o) also records the org user the CLI is signed in as — the user the platform runs your tests as — with its TimeZoneSidKey and LocaleSidKey, in .nimbus/org-user.json. Tests then run in that user's zone, which is what Datetime.format(), Date.today() and the date-literal windows answer in the org; nimbus.org.timezone overrides it, and a project that never synced runs in the machine's zone.

That automatic reconcile regenerates every schema from your metadata each run, then fingerprints each object against the table already in the database. Objects whose shape hasn't moved are skipped without touching the database; the ones that changed are rebuilt whole, so a column type change, a removed field or a newly unique field all land on the next nimbus test. On a database that survives between runs that is the difference between rebuilding a few tables and rebuilding all of them.

Sync is incremental by default: it creates missing tables and adds missing columns without dropping anything, so a re-sync with no changes is near-instant even on orgs with hundreds of objects. Use --rebuild to force a full drop-and-recreate, which applies every column type change but also wipes table data. Use -s to scope work to specific objects while iterating.

An incremental sync also checks the columns it already has. A table is created once, so its column types are frozen at whatever the Nimbus release that created it emitted — and a field whose type has moved since keeps the old column through every upgrade. Where the conversion cannot lose a value the column is converted in place; where it could, the sync says so and leaves it alone:

bash
⚠ account.is_priority__c is text but the schema declares boolean — the column
  keeps its old type. Run `nimbus sync --rebuild` to apply the change (it
  recreates the table and wipes its rows).

Left unsaid, that divergence surfaces much later and much further away, as a raw PostgreSQL operator does not exist: text = boolean at query time. Rebuilding the one object — nimbus sync -s Account --rebuild — is usually enough.

bash
nimbus sync                                        # Incremental sync of all objects
nimbus sync -s Account,Contact,Lead                # Sync only these objects
nimbus sync --rebuild                              # Full drop-and-recreate of all tables
nimbus sync -s My_Object__c --rebuild              # Rebuild just one object
nimbus sync -s WorkOrder -o myorg                  # Pull a standard object Nimbus doesn't ship a describe for

-s also reaches objects Nimbus has never heard of. Standard-object describes ship with Nimbus, and there is no .object-meta.xml a project could write for a standard object — so when one is missing, naming it explicitly alongside -o describes it from your org and caches it. It is remembered, so later syncs keep it, and a release that adds the describe quietly takes over.

FlagDefaultDescription
-s, --sobjects-Comma-separated list of objects to sync (others are left untouched). An object Nimbus has no describe for is fetched from the org named by -o.
--rebuildfalseDrop and recreate tables instead of incrementally reconciling. Applies every column type change, including the ones an incremental sync only warns about; wipes table data.
-o, --org-Target Salesforce org alias

nimbus init

Initialize Nimbus for the current project. Creates the .nimbus/ directory and starts the embedded database. Only needs to be run once per project.

bash
# Run from your project root (where sfdx-project.json is)
nimbus init

It also scaffolds what a project wants on day one: a commented nimbus.properties, a stubs/ directory with a README, and a .gitignore line for .nimbus/releases/. Nothing is overwritten — a file that already exists is left alone, and the ignore line is skipped when .nimbus/ is already covered, which it will be: any run of the runtime, init included, appends .nimbus/ to the .gitignore of a git repository the first time it starts. See Project Setup.

After init, run nimbus sync -o <org> to populate the schema, then nimbus test.

nimbus new

Scaffold Salesforce source — Apex, Lightning components, objects, fields, labels, custom metadata, and the admin surface from page layouts to approval processes — into your project's default package, following the standardforce-app/main/default/… layout. Every type writes its -meta.xml companion, and the API version comes from sourceApiVersion in sfdx-project.json.

bash
nimbus new apex-class AccountService --test
nimbus new apex-trigger AccountTrigger --object Account --events before-insert,after-update
nimbus new lwc orderPanel --expose --targets lightning__RecordPage
nimbus new lwc typedPanel --ts   # a .ts class module; automatic when lwc/ has a tsconfig.json
nimbus new custom-object Delivery_Area__c --label "Delivery Area"
nimbus new custom-field Zone__c --object Delivery_Area__c --type Text --length 80
nimbus new layout "Delivery Area Layout" --object Delivery_Area__c --field Name --field Zone__c
nimbus new lightning-page Delivery_Area_Record_Page --type RecordPage --object Delivery_Area__c

Types

TypeCreatesType-specific flags
apex-class.cls + -meta.xml--template, --test
apex-trigger.trigger + -meta.xml--object, --events
lwc.js or .ts, .html, .css, .js-meta.xml--expose, --targets, --ts, --label
visualforce-page.page + -meta.xml--controller, --scaffold-controller
aura.cmp, -meta.xml, controller, helper
custom-object.object-meta.xml--label, --plural, --name-field-type, --display-format, --sharing-model
custom-field.field-meta.xml--object, --type, plus the flags that type needs
custom-labela <labels> entry in CustomLabels.labels-meta.xml--value, --categories, --language, --protected
custom-metadata-type__mdt .object-meta.xml--label, --plural
custom-metadata-record.md-meta.xml--type, --set, --protected
permission-set.permissionset-meta.xml--label, --description
field-set.fieldSet-meta.xml--object, --label, --field
record-type.recordType-meta.xml--object, --label, --inactive
validation-rule.validationRule-meta.xml--object, --formula, --error-message, --error-display-field
list-view.listView-meta.xml--object, --label, --filter-scope, --column
layout.layout-meta.xml--object, --field, --related-list, --highlights-panel
lightning-page.flexipage-meta.xml--type, --object, --label, --template
compact-layout.compactLayout-meta.xml--object, --label, --field
quick-action.quickAction-meta.xml--type, --object, --target-object, --lwc, --page, --height
tab.tab-meta.xmlone of --object, --lwc, --aura, --page; --motif
app.app-meta.xml--tab, --nav-type, --utility-bar
path.pathAssistant-meta.xml--object, --picklist-field, --record-type, --inactive
profile.profile-meta.xml--user-license, --description
global-value-set.globalValueSet-meta.xml--picklist-value, --sorted, --label
queue.queue-meta.xml--supports, --email, --label
duplicate-rule.duplicateRule-meta.xml--object, --label, --inactive
approval-process.approvalProcess-meta.xml--object, --label, --description
email-template.email-meta.xml + the .email body--type, --subject, --folder
sharing-rules<Object>.sharingRules-meta.xmlnamed after the object
assignment-rules<Object>.assignmentRules-meta.xmlnamed after the object
escalation-rules<Object>.escalationRules-meta.xmlnamed after the object
matching-rules<Object>.matchingRule-meta.xmlnamed after the object

The older Apex spellings still work: class, test, trigger, triggerhandler, batch, queueable and schedulable route to the same generator, and --sobject remains an alias for --object.

New metadata is queryable immediately

Objects, fields, labels and custom metadata are picked up by the next nimbus test without a sync. Nimbus rescans the project's own metadata on every run, so a field you created a second ago is already a column. nimbus sync is for pulling schema from an org, which is a different job.

Field types use the metadata vocabulary

--type takes the name the Metadata API uses in <type>, which is not always the name a describe call answers with. Rich text is Html, not RichTextArea; a multi-select picklist is MultiselectPicklist, not MultiPicklist. Pass a describe spelling and Nimbus tells you the metadata one rather than writing a field that silently behaves as text.

bash
$ nimbus new custom-field Tags__c --object Account --type MultiPicklist
Error: "MultiPicklist" is a describe type name, not a metadata FieldType — use
"MultiselectPicklist" (metadata XML and describe results spell several types differently)

Accepted: AutoNumber, Checkbox, Currency, Date, DateTime, Email, Lookup, MasterDetail, Number, Percent, Phone, Picklist, MultiselectPicklist, Text, TextArea, LongTextArea, Html, Time, Url.

Roll-up summaries (Summary), geolocations (Location), encrypted text (EncryptedText), hierarchical relationships (Hierarchy) and metadata relationships (MetadataRelationship) are creatable too — each takes the extra flags listed below. External and indirect lookups are not, and neither are formula fields: an external lookup's target exists only through an external data source, and a local project has none to offer or check against. Naming one tells you that, rather than treating it as a typo.

Flags

FlagDefaultDescription
--dirTarget directory, bypassing the standard layout.
--api-versionsourceApiVersionAPI version for -meta.xml companions (falls back to 62.0).
--forcefalseOverwrite existing files. Without it, a collision names the file and nothing at all is written. On a custom label it replaces that one entry and leaves the rest of CustomLabels.labels-meta.xml alone.
--labelLabel. Defaults to a readable form of the API name.
--descriptionDescription, where the type supports one.
--templateclassApex class shape: class, test, batch, queueable, schedulable, triggerhandler.
--testfalseShorthand for --template test.
--objectA trigger's SObject, or the object a field belongs to.
--eventsall sevenTrigger events, e.g. before-insert,after-update.
--exposefalseSet isExposed on an LWC bundle.
--targetsLWC targets, e.g. lightning__RecordPage. Implies --expose.
--tsautoWrite the LWC class module as <name>.ts. Left off, a tsconfig.json in the destination lwc/ directory decides; --ts=false opts one component out. The bundle metadata is <name>.js-meta.xml either way.
--controllerApex controller for a Visualforce page.
--scaffold-controllerfalseAlso generate that controller class.
--pluralPlural label for an object.
--name-field-typeTextRecord name type: Text or AutoNumber.
--name-field-label"<Label> Name"Record name label.
--sharing-modelReadWriteReadWrite, Read, Private or ControlledByParent.
--display-formatAutoNumber sequence, e.g. ORD-{0000}.
--typeField data type; for custom-metadata-record, the __mdt type.
--lengthper typeLength for Text, Text Area (Long) and Rich Text.
--precision18Total digits for Number, Currency and Percent.
--scaleper typeDecimal places for Number, Currency and Percent.
--requiredfalseMark the field required.
--uniquefalseMark the field unique.
--external-idfalseMark the field an external ID.
--picklist-valueA picklist value. Repeatable.
--picklist-restrictedfalseRestrict the picklist to its value set.
--reference-toRelated object for a Lookup or Master-Detail.
--relationship-nameChild relationship name for a Lookup or Master-Detail.
--delete-constraintSetNullLookup delete behaviour: SetNull, Restrict or Cascade.
--checkbox-defaultfalseDefault a Checkbox to checked.
--help-textField help text.
--summarized-objectRoll-up: the child object being aggregated.
--summary-foreign-keyRoll-up: the master-detail field on the child pointing back at this object.
--summarized-fieldRoll-up: the child field to aggregate. Not used by count.
--summary-operationRoll-up: count, sum, min or max.
--summary-filterRoll-up filter as "Field__c operation value". Repeatable.
--location-notationdecimalGeolocation: decimal or degrees.
--mask-charasteriskEncrypted text: asterisk or X.
--mask-typeallEncrypted text: all, creditCard, ssn, sin, nino or lastFour.
--controlling-fieldMetadata relationship: the EntityDefinition field scoping a FieldDefinition target.
--fieldField set member, in display order. Repeatable.
--inactivefalseCreate a record type or validation rule inactive.
--formulaValidation rule errorConditionFormula — true rejects the save.
--error-messageValidation rule message shown when it fires.
--error-display-fieldthe recordField to show a validation error on.
--filter-scopeEverythingList view scope: Everything, Mine, Queue, Delegated, MyTerritory, MyTeamTerritory or Team.
--columnList view column, in display order. Repeatable.
--related-listLayout related list, e.g. Contact or Delivery__c.Account__c. Repeatable.
--highlights-panelfalseShow the highlights panel on a layout.
--target-objectthe owning objectObject a quick action creates or updates.
--lwcLightning web component a tab or quick action shows.
--auraAura component a tab shows.
--pageVisualforce page a tab or quick action shows.
--motifCustom53: BellTab icon.
--heightQuick action height in pixels.
--tabApp navigation item, in navigation order. Repeatable.
--nav-typeStandardApp navigation: Standard or Console.
--utility-barLightning page of type UtilityBar.
--picklist-fieldPicklist whose values are a path's steps.
--record-type__MASTER__Record type a path applies to.
--sortedfalseSort a global value set's values alphabetically.
--supportsObject a queue can own records of. Repeatable.
--emailQueue notification address.
--user-licenseSalesforceProfile user license.
--folderunfiled$publicEmail template folder.
--subjectEmail template subject line.
--valueLabel text (required for custom-label).
--categoriesLabel categories.
--languageen_USLabel language.
--protectedfalseMark a label or custom metadata record protected.
--setCustom metadata record value as Field__c=value. Repeatable.
--listfalseList every type and the inputs it takes, then exit.
--jsonfalseEmit the result as JSON (with --list, the capability data).

Data types are the Metadata API vocabulary, not the one a describe call answers with — rich text is Html, a multi-select picklist is MultiselectPicklist, encrypted text is EncryptedText. A describe spelling is refused with the metadata name given, rather than written into a file that would silently behave as text.

Summary, Location, EncryptedText, Hierarchy and MetadataRelationship are generated. ExternalLookup and IndirectLookup are not, and the refusal names the reason: both need a referenceTo pointing at an external object, which exists only through an external data source — a local project has none, so there is nothing to offer and nothing to validate a typed name against. Write those two by hand.

The four object children — field-set, record-type, validation-rule and list-view — require an object that already exists, in the project or on the platform; a name matching neither is refused rather than written one directory deep. A child lands beside its own object, so an object in a second package directory keeps its children there.

Nimbus evaluates validation rules during local DML, and skips any rule whose formula it cannot read. Creating a rule therefore parses its errorConditionFormula first and warns when the formula engine cannot — the rule still deploys and still fires in the org, but local runs let the save through, so a green suite would not mean what it looks like.

--list --json is what the IntelliJ plugin reads to build its New menu, so its dialogs always describe the CLI you actually have installed.

nimbus daemon Pro

The daemon is a long-lived background process that pre-parses your entire codebase, loads flows, record types, custom metadata, labels, and validation rules into memory at startup. Every subsequent test run — CLI or VS Code — connects to the already-warm daemon and starts executing immediately.

On a project with ~1,000 Apex files, cold-start parsing alone takes 10–15 seconds per run. With the daemon running, that startup cost drops to near zero.

See the daemon overview page for a full explanation of how the warm-up works and what stays in memory.

bash
nimbus daemon start          # Start the daemon (detaches to background)
nimbus daemon start --clean  # Start with a fresh database
nimbus daemon status         # Show uptime, files loaded, connection count
nimbus daemon list           # List running daemons across all projects
nimbus daemon stop           # Stop the daemon for the current project
nimbus daemon stop --all     # Stop every running daemon across all projects

Flags

FlagDefaultDescription
--cleanfalseDrop and recreate the database before starting
--foregroundfalseRun in foreground instead of detaching (useful for debugging)

Where the socket lives

On macOS and Linux the daemon listens on a Unix socket in a per-user runtime directory —$XDG_RUNTIME_DIR if set, otherwise ~/.nimbus/run/ — named after a hash of the project path (nimbus-<hash>.sock). It is not inside the project: a Unix socket path is capped at 104 bytes on macOS, and a project nested more than ~80 characters deep could not be bound at all. The running daemon writes its endpoint to .nimbus/daemon.endpoint in the project, which is what nimbus daemon status and the editor integrations read. Windows uses a named pipe and has no such file.

Override the directory with NIMBUS_RUNTIME_DIR if your environment needs it. A daemon started by an older nimbus listens on .nimbus/daemon.sock; newer clients still find and stop it, so an upgrade does not strand a running daemon.

Runtime coverage and tests-on-idle

Every test run the daemon performs records, per test, the lines it executed. That ledger is kept for the last few runs under .nimbus/history/line-coverage/ and is what answers “who covers this line?” — in the editor gutter, and over these RPCs.

A read walks back to the most recent run that actually touched the file, not simply the most recent run. A run narrowed to three tests says nothing about the files it never reached, and reading its silence as “no test covers this” would blank the gutter after every idle run.

FlagDefaultDescription
nimbus/coverage.lines{uri} or {path}Per line: the tests that executed it and whether each passed, plus the execution count when the run measured one. Carries runId, source, recordedAt and ageMs — a claim about what code did is only meaningful with the run it came from attached. hasRecord:false means no recorded run covers the file at all, which is not the same as no test covering it.
nimbus/watch.touch{uri, lines?}Tell the daemon a buffer changed, and on which 1-based lines. Each call restarts the quiet period. Omitting lines means "changed somewhere", which selects against the whole file. Accepted as a notification.
nimbus/watch.idleConfig{enabled?, idleMs?, fullSuite?}Read or set the idle trigger. Returns the effective configuration. Unset, it follows watch mode.

After idleMs (default 1500) with no further watch.touch, the daemon runs the tests that cover the changed lines and streams them through the usual nimbus/test.progress and nimbus/watch.complete notifications. A nimbus/watch.idle notification goes out first, carrying the exact selection and the evidence it came from — or, when nothing could be selected, the reason nothing is running.

Selection unions the line ledger with .nimbus/impact-map.json, under the cold-clone rule: a missing, stale or differently-versioned map is no map, never an empty one. With neither source, it falls back to the file's own test methods if it is a test class, then to the tests that executed any line of it, and then does nothing and says why. It never runs the whole suite unless fullSuite is on — an idle trigger that silently starts a four-minute run is one people disable within the hour.

A newer trigger cancels and supersedes a run still in flight, through the same slot nimbus/test.cancel operates.

Selecting tests over the RPC

Four daemon methods choose which tests to act on, and each accepts exactly the keys below. A request that carries a selector-shaped key the method does not read — className, methodName, testClass, program, suite and the like — is refused with an error naming the keys that do select, and the pattern to send instead. Nothing runs.

FlagDefaultDescription
nimbus/test.runpattern, tests"ClassName.methodName", "ClassName", a glob such as "*Test", or a comma-separated list. tests[] is the same thing as an array. Neither given runs the whole suite
nimbus/test.discoverpatternWhich test classes to list. Absent lists every test class in the project
nimbus/watch.startpatternWhat the watch re-runs on save. Absent watches the whole suite
nimbus/compare.testspatternWhat to run on both sides of a differential. Absent compares the whole suite
nimbus/method.runclassName, methodNameA single method invocation — not a pattern

An omitted selector still means "everything", which is what a client asking for a full run sends. The refusal is for the other case: a selector that was meant to narrow the run and would otherwise have been dropped by the decoder and silently widened to the whole project.

VS Code integration

The VS Code extension starts the daemon automatically when it detects a Pro license. You do not need to run nimbus daemon start manually. The status bar shows a Nimbus indicator — if it shows a slash, run Nimbus: Restart Daemon from the command palette.

Free-tier users can still run tests from the CLI with full coverage and governor limit support. The daemon and live editor integration (inline results, trace viewer, coverage gutters, history) require Pro.

nimbus login

Activate Nimbus on this machine via your browser. No license keys to copy or paste — sign in with the same account you use in the portal and the CLI registers itself.

bash
nimbus login       # Open the browser, sign in, register this machine
nimbus logout      # Deactivate this machine and free up a seat
nimbus whoami      # Show the current account and license tier
nimbus whoami --json # Structured status for editor and tooling integrations

Licenses are validated online and cached locally; a 7-day offline grace period is built in for outages and travel.

Re-running nimbus login is always safe: if nothing changed it says so, and if your account's license changed since this machine last signed in, the machine is moved to the current license automatically.

nimbus whoami --json returns plan, activation state, a shortened machine identifier, capability lists, and account URLs. It never returns a license key or full machine identifier.

Headless / CI: set NIMBUS_LICENSE_KEY in the environment instead of running nimbus login. Team plans include a dedicated CI runner license with its own machine pool.

nimbus status

Show which database this project will use, what decides that database's connection, and which org the org-facing commands will reach. For a health check with fixes attached, use nimbus doctor instead.

bash
nimbus status         # Human-readable summary
nimbus status --json  # The same facts, machine-readable

--json

Three objects with stable lowercase keys. project carries found and path; database carries provider, url_source (flag, environment or embedded) and data_dir for the embedded provider; org carries target and source (flag, config or none).

The connection URL is never included, only what decides it — a URL can carry credentials and this output is meant to be safe to paste into an issue. When no org is configured, target is empty rather than the placeholder the human-readable output prints.

bash
{
  "project":  { "found": true, "path": "/work/my-project" },
  "database": { "provider": "embedded", "url_source": "embedded", "data_dir": ".nimbus/db" },
  "org":      { "target": "my-sandbox", "source": "config" }
}

nimbus doctor

Run diagnostic checks on the current Salesforce project. Reports setup problems - missing configuration, no synced schemas, broken database, unknown config keys - with a concrete fix for each issue. Think brew doctor or gh auth status.

bash
nimbus doctor           # Run all checks
nimbus doctor --verbose # Show fix details for all checks, not just failures
nimbus doctor --json    # Output results as JSON

Checks

Check nameWhat it validatesSeverity
nimbus.properties foundConfig file exists in the project rootFail
Config syntaxAll keys in nimbus.properties are recognizedWarn
Apex parse errorsEvery .cls and .trigger file parses without syntax errorsFail
Schema files foundSObject schemas synced into .nimbus/schemas/Warn
Schema coverageEvery custom object defined in source has a synced schemaWarn
Stubs directory existsstubs/ present for managed package stubsWarn
Stub gapsAll managed-package namespace references in source have stub coverageWarn
Field metadata validityCustom field XML passes Salesforce deploy-time rulesWarn
Permission set coveragePermission sets and groups your tests resolve by name are defined in source or synced from the orgWarn
Apex test classes found@isTest classes exist under project source directoriesFail
Database connectivityThe database this project uses — the embedded PostgreSQL under .nimbus/db, or whatever --db-provider/--db-url/--db-dir select — starts and accepts connectionsFail
License statusCurrent tier (Free / Pro / Team) and expiry — key never printedWarn
FlagDefaultDescription
--verbosefalseShow fix details for all checks, not just warnings/failures
--suitefalseAlso run the test suite and report failure clusters, largest cause first
--jsonfalseOutput results as JSON

Exit codes

CodeMeaning
0All checks passed or warned - no hard failures
1One or more checks failed

CI usage

Gate your pipeline on a clean setup before running tests:

bash
nimbus doctor && nimbus test

nimbus orgs

List Salesforce orgs authenticated via SF CLI that Nimbus can use for sync and fallback operations.

bash
nimbus orgs

To put one question to every one of them at once, see nimbus orgs find and nimbus orgs query.

nimbus orgs find / nimbus orgs query

The same question, every authenticated org, at once. find takes a record Id and reports which orgs hold it; query takes a SOQL query and reports what each org returns. Both list hits first, then the orgs that gave no answer, then the orgs that were not asked, then the misses — an org that could not be checked is not an org where the record is absent.

bash
# A record Id someone handed you, org unknown
nimbus orgs find 00Q5g00000AbCdEfAA

# The same, but only these orgs, and give each one ten seconds
nimbus orgs find 00Q5g00000AbCdEfAA --orgs prod,uat --timeout 10s

# One query in every org
nimbus orgs query "SELECT Id, Name FROM Lead WHERE Email = 'ada@example.com'"

# For a script
nimbus orgs find 00Q5g00000AbCdEfAA --json

How an Id finds its object

SOQL needs an object name, and the object behind an Id is only knowable from its three-character key prefix — which is per org: a custom object's prefix is assigned when the object is created and differs from org to org. So each org is asked what the prefix means to it (one light EntityDefinition query), and the record is then selected from that object. A hit shows the record's Id and Name, or Id alone for an object with no Name field. A prefix that names nothing in an org is a miss with the reason stated.

Output

One line per org — org, HIT / miss / error / not connected, row count, time — then a verdict line, then the rows of every org that had any. Orgs are asked six at a time, each under its own timeout, through the same native-first org engine as nimbus soql -o. An org the Salesforce CLI reports as not connected is listed but not asked: an expired session would cost a slow failure to learn what sf org list already says. Naming it with --orgs asks it anyway.

bash
nimbus  HIT               1 row   1167 ms  Account
deved   not connected         —         —  not connected — run sf org login web -a deved

1 hit in 1 org · 1 not connected in 1167 ms

nimbus — Account
  Id                  Name
  001g500000H2GQIAA3  Edge Communications

Flags

FlagDefaultDescription
--orgsevery authenticated orgOrgs to ask, by alias or username, comma-separated. The default is the Salesforce CLI’s org list, the same one the IDE’s org pickers show; an org it reports as not connected is listed as such and not asked. An org named here is always asked.
--timeout30sHow long to wait for each org’s answer. An org past it is reported as "no answer within …" beside the others’ answers.
--toolingfalseQuery the org’s Tooling API (ApexClass bodies, debug logs, test results, coverage) in every org. Refused beside a record Id: an Id is resolved from its key prefix, which every org answers over the data API.
--jsonfalseEmit the structured result: the question, then per org — hit, count, object, the query that ran, columns, rows, error, skipped, ms — and the elapsed time.

Exit codes

CodeMeaning
0At least one org answered with a row
1No org has the record (or, for query, no org returned a row); orgs that gave no answer are listed as errors either way

The same engine backs the IDE's Find in All Orgs… (Data workbench and SOQL console toolbars, and the Org menu) and the daemon's nimbus/orgs.query RPC, which streams each org's answer as it arrives. The dialog adds a Tooling API checkbox, so "which of my orgs has this ApexClass" is one question rather than one per org; it applies to a query only, since a record Id is resolved from its key prefix over the data API everywhere.

nimbus org doctor

Report which Salesforce login Nimbus found, and which org operations it performs natively versus through the Salesforce CLI.

The native org engine

The org operations Nimbus makes most often go straight to the Salesforce APIs, using the login sf org login web already wrote to your sfdx auth store. No Node process starts, and Nimbus works on a machine whose Salesforce CLI is missing, broken, or mid-upgrade.

OperationHow
Anonymous Apex (nimbus exec -o, nimbus compare -c, org probes)Apex SOAP executeAnonymous, debug log included
SOQL against an org (nimbus soql -o, nimbus compare -q)REST query, every page followed
Org test runs (nimbus compare --tests)Tooling runTestsAsynchronous + ApexTestResult
Apex source (nimbus metadata retrieve, nimbus test --fetch-missing)Tooling query on ApexClass / ApexTrigger
Deploying a class or trigger (nimbus metadata deploy)Tooling MetadataContainer + ContainerAsyncRequest
Retrieving metadata (nimbus metadata retrieve with a wider manifest)Metadata API retrieve + checkRetrieveStatus, unzipped to source format
Deploying metadata (nimbus metadata deploy of a directory or manifest)Metadata API deploy + checkDeployStatus, zipped from source format
Listing what an org holds (the editors' org browser)Metadata API describeMetadata and listMetadata
The session for standard-object describes (nimbus sync -o, and the sync a run does for itself)The sfdx auth store read in place of sf org display and sf org auth show-access-token, refreshed once if the stored token has expired

The Metadata rows carry a boundary, and that boundary decides which of your commands is fast. Source format keeps most types in a single file, and those Nimbus converts and moves itself. It does not keep an object in one file — a Salesforce DX project splits an object across per-field, per-record-type and per-list-view files, and reassembling those is the Salesforce CLI's job. Workflows, sharing rules, Lightning and Aura bundles, and anything held in a folder are the same story. nimbus org doctor names the count and the exclusions on those rows rather than reporting a bare "native":

bash
Capabilities
  execute anonymous Apex     native
  SOQL query                 native
  run org tests              native
  read org test results      native
  retrieve Apex source       native  (Apex classes and triggers by name, over the Tooling API)
  deploy a class or trigger  native  (single Apex files, over the Tooling API's container flow)
  retrieve metadata          native  (29 metadata types; objects, workflows, bundles and folder contents retrieve via Salesforce CLI)
  deploy metadata            native  (29 metadata types; objects, workflows, bundles and folder contents deploy via Salesforce CLI)
  list org metadata          native  (describeMetadata and listMetadata, every type the org exposes)

The split is per call, not per component: a manifest naming even one excluded type runs through the Salesforce CLI whole. Two retrieves are two snapshots of an org that changed in between, and two deploys are two transactions, which makes rollback-on-error a promise a pair of them cannot keep.

Everything else — the nimbus deploy assurance cycle, org creation, package operations, nimbus sf passthrough — still runs through the Salesforce CLI, which stays a supported dependency.

Fallback, never replacement

Every native call falls back to the exact Salesforce CLI invocation it replaced if anything goes wrong — no login, an expired refresh token, an org that will not answer. You get one line saying so, once per capability:

bash
native org call failed (SOQL query: ...), using sf CLI

Nimbus never runs an OAuth flow of its own. If there is no sfdx login it says which command creates one, rather than authenticating an org that sf would not be able to see.

The Salesforce CLI encrypts the tokens in its auth store, so Nimbus reads the same key from your OS credential store (Keychain on macOS, libsecret on Linux, or the CLI's key.json where neither exists). If that key is unavailable — a locked keychain, a restricted CI runner — nimbus org doctor reports the tokens as encrypted (no key available) and every call falls back to the Salesforce CLI, which reaches the credential store through its own bindings. Nimbus will not send a secret it could not decode.

Reading the report

bash
nimbus org doctor                 # the default target org
nimbus org doctor -o my-sandbox   # a specific org
nimbus org doctor --probe         # also make one live call to prove reachability
nimbus org doctor --json          # the same report, structured

Everything except --probe is answered from local files, so this works offline and on an org whose session has expired. Access and refresh tokens are never printed — the fields shown are the ones sf org list already displays. The command exits non-zero when there is no usable login, or when --probe was passed and the org did not answer.

FlagDefaultDescription
--probefalseMake one live call to confirm the org answers
--jsonfalseEmit the report as JSON
-o, --orgdefault target orgWhich authenticated org to diagnose

Turning it off

Set org.native=false in nimbus.properties. Every org call then goes through the Salesforce CLI exactly as it did before — the fallback path is the old behaviour, so nothing else changes. That includes the session used to read standard-object describes, which goes back to sf org display. Nimbus falls back on its own too: a login it cannot read (an encrypted sfdx store with no key available, an alias with no login) is never a failure, only the slower route.

bash
# nimbus.properties
org.native=false

Tracing

NIMBUS_ORG_NATIVE_TRACE=1 logs a redacted summary of every native call to stderr: method, endpoint, status, timing and body size. Never a request body, never a query string's contents, and never a token.

bash
NIMBUS_ORG_NATIVE_TRACE=1 nimbus soql -o my-org "SELECT Id FROM Account LIMIT 1"

nimbus org status

Report whether the org's copy of your files changed since you took them. This is the read-only half of nimbus metadata deploy's conflict detection: same registry, same query, nothing written and nothing deployed.

bash
nimbus org status                    # everything tracked for the target org
nimbus org status AccountService     # one class
nimbus org status --json             # the same rows, structured
bash
Sync base — my-sandbox (00D5f000004XXXXEAA)
  .nimbus/org-base.json — this checkout only, not for git

  ApexClass  AccountService  current     synced 2026-08-26 09:15
  ApexClass  ContactService  local edit  synced 2026-08-25 14:00, edited since
  ApexClass  OrderService    ORG NEWER   changed 2026-08-26 11:02 by Dana Ruiz

  3 entities tracked, 1 newer in the org — `nimbus metadata deploy` will refuse those.
FlagDefaultDescription
currentThe org is where you left it, and so is your file.
local editThe org is where you left it; your file has changed since. A deploy will land.
ORG NEWERSomeone saved it in the org after you took it. A deploy is refused without --force.
goneThe org no longer has it. A deploy would recreate it.

A project with no rows has simply never retrieved or deployed through Nimbus — that is the brownfield starting state, and a deploy in it warns once and proceeds. The command exits non-zero when any row is ORG NEWER, which makes it usable directly as a pre-push check.

FlagDefaultDescription
--jsonfalseEmit the rows as JSON
--org, -orequiredOrg to ask

Distinct from nimbus org doctor on purpose: doctor answers "can Nimbus talk to this org natively" from local files and works offline, while this asks the org about data and needs a live connection.

nimbus org diff

Show where this project and the org disagree — what exists only in the org, only here, and what both have with different bodies.

bash
nimbus org diff                              # the default target org
nimbus org diff -o my-sandbox                # a specific org
nimbus org diff --type ApexClass,ApexTrigger # narrow it
nimbus org diff --all                        # list the matching components too
nimbus org diff --json                       # the whole tree, structured

Five states, not three

Every component comes back as one of five states. The fifth is what makes the other four worth trusting.

StateMeans
org onlyThe org has it and this project does not
local onlyThis project has it and the org does not
differsBoth have it and the bodies are not the same
= in syncBoth have it and the bodies match
? undeterminedSomething is in the way of a verdict, and the row says what

A row is undetermined when the org will not hand over a body in the same call that listed the name — a static resource's binary, an Aura or LWC bundle's files, source a managed package hides from a subscriber — or when this project defines the same API name in two package directories, in which case there is no single local copy to compare. Nimbus says so rather than guessing. A wrong "differs" costs you a diff; a wrong "in sync" is how a stale class ships.

What is compared, and how

Bodies are compared after trailing whitespace is removed from each line and from the end of the file, and after nothing else — the two differences a round trip creates on its own. No other normalization is applied, deliberately: every additional rule makes an "in sync" verdict easier to produce and less true.

TypeCompared
ApexClass, ApexTriggerPresence and body
ApexPage, ApexComponentPresence and markup
StaticResourcePresence only
AuraDefinitionBundle, LightningComponentBundlePresence only
CustomLabelPresence only

Managed-package members are excluded. A subscriber org cannot hold their source, so every one of them would report as org-only forever.

What it does not cover

Objects, fields, layouts, flows, permission sets, profiles, record types, validation rules and the rest of the long tail are not looked at. Those cannot be listed over the Tooling API at all — they need the Metadata API's listMetadata call, which this build does not make. The command prints them by name on every run, and the JSON carries them in pendingTypes, so a clean report is never mistaken for a complete one.

A type the org refuses to list is reported undetermined with the org's own error, never as empty. "The org has none of these" and "the org would not tell me" are different answers, and the second one must not silently turn this project's components into local-only rows.

Bounded output

An org with a few packages installed answers a listing with four figures, so the tree lists at most 200 components per type and counts the rest. The cut is stated on the row that replaces them. Components are ordered drift first, so the cap can only ever hide rows nobody needed to see; the listing behind it is never truncated, because a component dropped before the comparison would come back as a local-only verdict rather than as a missing row.

Matching components are counted but not listed unless you pass --all. The answer to "is my project current" is the rows that are not.

FlagDefaultDescription
--typeevery listed typeOnly these metadata types, comma-separated
--limit200How many components to list per type before counting the rest; -1 for all
--allfalseList the components that match too, not only the ones that drifted
--jsonfalseEmit the drift tree as JSON
-o, --orgdefault target orgWhich org to compare against

JSON

The payload is pinned by a schema field and carries its own caveats: supportedTypes is every type this build can list, pendingTypes is every type it cannot, and limits states what the run could not establish. Every array is present even when empty.

bash
nimbus org diff --json | jq '.types[].entities[] | select(.status == "bothDiffer") | .name'
bash
{
  "schema": "nimbus.orgdiff/v1",
  "org": "my-sandbox",
  "summary": { "orgOnly": 1, "localOnly": 0, "bothSame": 142, "bothDiffer": 2, "undetermined": 4 },
  "types": [
    {
      "type": "ApexClass",
      "status": "listed",
      "counts": { "orgOnly": 1, "localOnly": 0, "bothSame": 142, "bothDiffer": 2, "undetermined": 0 },
      "total": 145, "shown": 3, "hidden": 142, "hiddenInSync": 142, "hiddenCapped": 0,
      "entities": [
        {
          "name": "AccountService",
          "status": "bothDiffer",
          "localPath": "/projects/acme/force-app/main/default/classes/AccountService.cls",
          "orgId": "01p000000000001AAA",
          "localHash": "27ff45bbc204",
          "orgHash": "154c010ef21f",
          "actions": ["retrieve", "deploy", "open"]
        }
      ]
    }
  ],
  "pendingTypes": [
    { "type": "CustomObject", "reason": "typeNeedsListMetadata", "detail": "..." }
  ],
  "limits": ["Managed-package members are excluded. ..."]
}

actions names only the operations Nimbus judged unambiguous for that row — retrieve when the org has it, deploy and open when exactly one local file backs it. A name defined in two package directories gets neither, because "which file" has no answer.

In the IDE

The IntelliJ plugin renders the same tree on the Drift tab beside the Org browser, with Retrieve and Deploy on the rows that support them. Both hand off to the same paths the editor's right-click menu uses, so the git overwrite guard and the local Apex check before a deploy behave exactly as they do elsewhere.

nimbus schema

Open a visual schema explorer showing synced SObjects, their fields, relationships, and field types. Useful for verifying your local database matches your org metadata.

bash
nimbus schema

Machine-readable schema

--json skips the TUI and writes the full schema to stdout: every table with its columns (name, dataType, isNullable, isPrimary, isReference), its inferred foreign keys in both directions (references and referencedBy), and its rowCount. Tables are sorted by name so two dumps of the same database diff cleanly.

bash
# The whole schema
nimbus schema --json

# Which tables point at Account?
nimbus schema --json | jq -r '.tables[] | select(.references[]?.toTable == "account") | .name'

# Row counts, largest first
nimbus schema --json | jq -r '.tables[] | "\(.rowCount)\t\(.name)"' | sort -rn

The document is built through a runner rather than read straight off the connection, so the tables your project's metadata implies exist before they are described — the same document the editor plugins read from the daemon, so a script and an IDE see one schema.

Piping without --json is unchanged: it still prints the plain table list, because nimbus schema | grep Account is a table-name search and answering it with JSON would break every script that does it.

Flags

FlagDefaultDescription
--jsonfalseEmit the full schema (tables, columns, relationships, row counts) as JSON instead of opening the TUI

nimbus soql

Run a SOQL query and print the rows. By default it queries this project's local database — the same one your tests run against, seeded the same way — so you can check what a test will actually see before you write the assertion.

bash
# Query the local database
nimbus soql "SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%'"

# Show the PostgreSQL it translates to, without running it
nimbus soql --preview "SELECT Id, Account.Name FROM Contact WHERE CreatedDate = LAST_N_DAYS:7"

# Ask a real org the same question
nimbus soql -o my-org "SELECT Id, Name FROM Account LIMIT 5"

# Ask the org's Tooling API — class source, logs, test results, coverage
nimbus soql -o my-org --tooling "SELECT Id, Name, ApiVersion FROM ApexClass WHERE Status = 'Active'"

# Structured output for scripts and editors
nimbus soql --json "SELECT Id, Name FROM Account" | jq '.rows[].Name'

The local mode runs through the same runner configuration nimbus exec builds — seeded rows, org defaults and managed-package stub namespaces included. A console that answered differently from an @isTest method would be worse than no console at all.

Three modes, one query

  • Local (default) — executes against the embedded Postgres and prints an aligned grid with a row count and timing. No box drawing and no colour: the output is meant to survive a pipe into grep or awk.
  • --preview — translates the query to PostgreSQL and prints the SQL without running it. The fastest way to understand why a query returned what it did: you see the joins a relationship field produced and the window a date literal resolved to. It describes the local translation, so it cannot be combined with an org.
  • -o <alias> — forwards the query to that org instead, so the same command answers "and what does the org say?". It goes straight to the org's REST API (see nimbus org doctor) and is bounded by a two-minute timeout, so an expired session fails with an error rather than blocking forever on a login prompt.

The Tooling API

--tooling sends the query to the org's Tooling API instead of its data API. That is where a developer's own metadata lives as queryable tables: ApexClass.Body, ApexLog, ApexTestResult, ApexCodeCoverage, MetadataComponentDependency, TraceFlag, Flow. A data query cannot see any of it.

You rarely have to remember the flag. A query whose FROM object only exists in the Tooling API is routed there anyway, and the result says so — in the grid's footer, and as a note field under --json. The subquery in SELECT Id, (SELECT Id FROM ApexClasses) FROM Account does not count: the routing reads the outermost FROM, so an Account query stays an Account query.

Only an org has a Tooling API. Naming one of its objects without an org — nimbus soql "SELECT Id FROM ApexLog" — is refused with a sentence rather than run against a local table that does not exist. ApexClass and ApexTrigger are the exceptions: Nimbus seeds both locally, so they answer either way, and the org query goes through the Tooling API so that asking for Body works.

Flags

FlagDefaultDescription
--previewfalsePrint the translated PostgreSQL instead of running the query (local only)
--toolingfalseQuery the org's Tooling API (ApexClass bodies, debug logs, test results, coverage). Needs an org
--jsonfalseEmit the structured result as JSON
--org, -ounsetRun the query on this org instead of locally

--json carries columns, rows, count, the generated sql, the resolved object, the elapsed time, a tooling boolean saying which endpoint answered, and a note when Nimbus picked that endpoint for you. A failed query emits the document with an error field and exits non-zero — one thing to parse, and the verdict on the exit code rather than on whether output appeared.

nimbus cache

Manage the parsed AST cache. Nimbus caches parsed Apex classes to speed up subsequent test runs.

bash
nimbus cache stats   # Show cache hit rate, size, entry count
nimbus cache clear   # Clear all cached ASTs

nimbus config

Manage Nimbus configuration via the nimbus.properties file.

bash
nimbus config show         # Display current effective configuration
nimbus config init         # Create an example nimbus.properties file
nimbus config properties   # List all available configuration properties

nimbus reset

Reset the .nimbus/ directory for the current project - clears the database, AST cache, and trace files. Useful when things get into a bad state.

bash
nimbus reset

This is the destructive one. If you only want the rows gone — schema, daemon and synced metadata intact — use nimbus data reset instead and skip the nimbus sync that this makes necessary.

It is also how you get the disk back. The local database is kept between runs so the next one starts on the tables it already has instead of rebuilding all of them — that is worth a second or two per run and costs 60–90 MB per project, nearly all of it the .nimbus/db/ data directory, which this removes along with the synced schemas, the AST cache and the traces. See Project setup for the full breakdown.

What it does not remove is the shared PostgreSQL installation under ~/.nimbus/pg/. Those binaries are extracted once per machine and used by every project, so resetting one project never makes another one re-download anything.

nimbus data

Operate on the rows in the local database, as opposed to the database itself (nimbus db) or the whole project state (nimbus reset).

bash
# Empty every table; keep the schema, the daemon and the database
nimbus data reset

# No prompt (required when stdin is not a terminal)
nimbus data reset --yes

The prompt needs somebody to answer it, so a script, a cron job or a CI step has to pass --yes. Without it the command refuses and says so — including under < /dev/null, which looks like a terminal to the usual check and used to fail as though someone had declined.

nimbus data reset vs nimbus reset

The distinction is the point of the command, and reaching for the wrong one costs a nimbus sync.

FlagDefaultDescription
nimbus data resetnon-destructiveTruncates every table. The schema stays, the daemon keeps running, the postgres process keeps running, and .nimbus/ is untouched. Only the rows go.
nimbus resetdestructiveStops the daemon, kills the database process, and deletes the entire .nimbus/ directory — including the synced schema and the AST cache.

Seed rows (profiles, users, ApexClass records) are cleared along with everything else, but every run re-creates them before it executes anything — so the next nimbus test starts from a clean standard org rather than a broken one. Use it when scratch data has accumulated into a state you no longer trust.

Flags

FlagDefaultDescription
--yes, -yfalseSkip the confirmation prompt. Required when stdin is not a terminal (CI, a script).

Rows are deleted, so it asks first — the same rule the release commands apply to a production deploy: a non-interactive caller must have said so on the command line, because there is nobody there to ask.

nimbus data branch

Keep more than one state of the local org at once. A data branch is a copy of this project's local database, made with a PostgreSQL CREATE DATABASE … TEMPLATE inside the cluster Nimbus already runs for the project. At the size a local org reaches that is a file copy — tens to a few hundred milliseconds — and switching between branches changes one word in a connection string.

bash
# Copy the current data into a new branch and switch to it
nimbus data branch experiment

# Copy a specific branch rather than the current one
nimbus data branch --from default hotfix

# What exists, what it costs, which one is in use
nimbus data branches

# Move between them
nimbus data switch default
nimbus data switch experiment

# Remove one (refuses the branch in use)
nimbus data branch --delete experiment

What comes along

Everything in the database: tables, rows, the seed records a run creates, and any per-worker schemas the test runner built. There is no re-seed, no re-sync and no org round trip — the copy is of the database as it stands.

What this is not

This is local dev data, not a migration tool and not a backup. Branches live inside .nimbus/db with everything else, so nimbus reset deletes them along with the rest of the local database. They do survive a daemon shutdown and a machine restart — the database is stopped between runs, not deleted. Nothing here versions your schema, moves data between orgs, or survives a machine. It exists so you can run the destructive test and still have the state you had before it.

Branch names

Lowercase letters, digits, - and _, starting with a letter or digit, up to 40 characters. default is the project's original database: it is where nimbus data switch default goes back to, and it is not a branch you create or delete.

The daemon

PostgreSQL will not copy or drop a database while a session is connected to it, and the daemon holds one for its whole life. So a branch or a switch asks the daemon to move: it closes its pool, performs the operation and reopens on the result, in one round trip.

If a separate Nimbus process is mid-run against the branch being copied or deleted, the command refuses rather than severing it. If the daemon cannot be moved for some other reason, the switch still takes effect for everything started afterwards and Nimbus says so — a test run detects the mismatch and connects to the right branch itself rather than silently reading the previous one.

Flags

FlagDefaultDescription
--from <branch>current branchCopy this branch instead of the one in use.
--delete <branch>Delete a branch. Refuses the branch currently in use and refuses default.

Following the git branch

Off by default. Turn it on and Nimbus creates and switches to a data branch matching the checked-out git branch whenever it changes, printing one line to say what it did:

bash
# nimbus.properties
nimbus.data.branch.follow-git=true
FlagDefaultDescription
nimbus.data.branch.follow-gitfalseOn project open, create and switch to a data branch matching the git branch when it has changed.

The git branch name is lowercased and anything outside the allowed characters becomes a hyphen, so feature/NIM-42 becomes feature-nim-42. It is evaluated when a test run starts and when the daemon starts — the two moments that genuinely are opening the project. A detached HEAD names a commit rather than a branch and is left alone, and deleting a git branch never deletes its data.

It is off by default deliberately: a checkout silently moving your local org is a surprise nobody asked for, and every branch you touch costs another copy of the database on disk. nimbus data branches shows what that adds up to.

nimbus data clone

Copy a Salesforce org's data into a local data branch, so tests, anonymous Apex and the Data Loader's simulation run against the real dataset — without touching the org. Rows land with their org Ids, so a local query returns what the org would return.

bash
# A working set of the default org, into org-<alias>
nimbus data clone

# Every row of a sandbox, without personal data
nimbus data clone --org uat --thorough --mask default

# Just the objects a bug touches, with their children
nimbus data clone --org uat --objects Account,Contact --with-children

# Later, bring it back in step with the org
nimbus data switch org-uat
nimbus data refresh

Two speeds

Quick (the default) takes the most recently modified rows of each object and then everything they point at, following lookups to a bounded depth. That matters: a truncated dump gives you Opportunities whose Accounts are missing and every lookup resolving to null. Quick is minutes and a few thousand rows, and its lookups resolve.

--thorough takes every row of every object. Longer, and complete.

What a clone asks before it fetches

Both speeds start by asking the org how many rows each object holds — one batched call per 25 objects, so the whole question is a handful of round trips. Three things follow from the answer. An object the org holds no rows for costs no round trip at all. A small object is pulled with one query. Only an object past ten thousand rows gets a Bulk job, which has a fixed cost of about nine seconds whether it carries a million rows or none, and loads through PostgreSQL COPY.

An object the org declines to count — one the running user cannot query, one the local schema has and the org does not — is pulled, never skipped. Objects are pulled several at a time; --parallel sets how many.

FlagDefaultDescription
-o, --orgOrg to clone. Defaults to the configured target org.
--thoroughEvery row of every object.
--objectsObjects to clone — and the only ones replaced on the branch. Defaults to every object in the local schema, minus history, share, feed and log tables.
--maskFields to null on the way in. 'default' expands to the standard personal-data fields.
--seed-rowsRows per object to seed a quick clone with (default 500).
--depthHow far to follow lookups from the seed (default 3).
--with-childrenAlso pull one level of children of the seed.
--all-rowsInclude the recycle bin (--thorough only).
--branchBranch to land in. Defaults to org-&lt;alias&gt;; must be an org- branch.
--yesSkip the confirmation that the branch's data will be replaced.
--parallelHow many objects to pull at once (--thorough only; default 6, max 12).

The mirror branch

A clone lands in org-<alias> and does not switch you onto it. Your current branch keeps your data; working against the org's data is a deliberate nimbus data switch org-<alias>.

That branch is a mirror, and two rules follow. A test run never clears it — every other branch is emptied and re-seeded at the start of a run, which would destroy the clone. And a clone replaces the data it loads, so it refuses to run on anything that is not an org- branch. To experiment freely, fork it: nimbus data branch bug-1234 --from org-uat, and the mirror stays clean for the next refresh.

How much is replaced follows --objects. A clone with no --objects replaces the whole branch. A narrowed one — nimbus data clone --objects Account — truncates and reloads only those objects and leaves the rest of the mirror exactly as it was, so you can re-pull one object after a long --thorough run without repeating it.

nimbus data refresh

Asks the org what changed since the last clone or refresh and applies only that: records whose SystemModstamp advanced, and records the org deleted. A refresh of a large mirror is seconds where the clone was minutes. It runs on the branch you are on, and refuses a branch whose rows came from a different org.

FlagDefaultDescription
-o, --orgOrg to refresh from. Defaults to the configured target org.
--objectsObjects to refresh. Defaults to every object the clone recorded.

Masking is a starting point, not a compliance control

--mask default covers the standard personal-data fields — email, phone, the address streets, and the rest — so the common case is one flag. It cannot know your org's own sensitive fields: a custom SSN__c, a salary, a case body. Name those yourself. Cloning production onto a laptop is a decision about data, not only about disk.

Keep the daemon running

A clone or a thorough refresh is a long command, and without nimbus daemon running each command owns a short-lived local database cluster. Start the daemon first and the cluster stays up for the whole run.

What this is not

It is one-way. Nothing here writes to the org — that is the Data Loader's job, and it is guarded there. Deleted rows arrive as local soft deletes, hidden from ordinary SOQL and visible to ALL ROWS, exactly as in the org. Attachments, files and other blob content are not cloned.

nimbus db

Manage the embedded PostgreSQL database directly.

bash
nimbus db status   # Show database status (running, port, data size)
nimbus db start    # Start the embedded database
nimbus db stop     # Stop the embedded database
nimbus db reset    # Reset the database (equivalent to nimbus reset)

nimbus db stop stops the postgres process and leaves the cluster on disk, which is also what happens at the end of a run and when the daemon shuts down: the next start reuses the synced tables instead of rebuilding them. To delete the cluster itself, use nimbus reset or nimbus daemon start --clean.

nimbus db start leaves the database running for later commands and marks it as started on purpose. Every nimbus command sweeps for databases orphaned in other projects — a postgres left behind by a killed run — and a pre-started database, idle with no daemon and no client on it, would otherwise look exactly like one. The mark protects it from that sweep for 24 hours; nimbus db stop ends it early, and running db start again renews it. A database a test run started implicitly carries no mark and is stopped once nothing is using it.

nimbus metadata

Move metadata between the org and this project: pull what a run could not resolve, push files, a directory or a manifest back.

bash
# Retrieve everything the last run reported missing
nimbus metadata retrieve -o my-org

# Machine-readable summary
nimbus metadata retrieve -o my-org --json

# Push a class, a directory, or a manifest
nimbus metadata deploy AccountService -o my-sandbox
nimbus metadata deploy force-app/main/default/permissionsets -o my-sandbox
nimbus metadata deploy --manifest manifest/package.xml -o my-sandbox

How it travels

Both directions go through Nimbus's own Salesforce APIs where they can, and hand the identical sf invocation to the Salesforce CLI where they cannot. What decides which is one question: does source format keep the type in a single file?

SelectionTransport
Apex classes and triggers by nameTooling API — one round trip, no CLI process
Permission sets, permission set groups, profiles, layouts, Lightning pages, flows, labels, tabs, apps, quick actions, custom metadata records, custom permissions, static resources, Visualforce pages and components, named credentials, remote site settings, queues, groups, value sets, path assistants, duplicate rules, approval processes, message channels, notification typesMetadata API — retrieved and deployed natively, converted to and from source format
Objects and their fields, record types, validation rules, workflows, sharing and assignment rules, Lightning and Aura bundles, reports, dashboards, documents, email templates, single custom labelsSalesforce CLI — reassembling these from source format is its job

The split is per call, not per component: a manifest naming even one type from the third row goes to the Salesforce CLI whole. Two retrieves are two snapshots of an org that changed in between, and two deploys are two transactions — which makes rollback-on-error a promise a pair of them cannot keep.

nimbus org doctor prints the exact boundary for your org, and org.native=false in nimbus.properties sends everything through the Salesforce CLI as before.

nimbus metadata retrieve

Every run that hits a class, object, field, custom metadata type or label Nimbus has no source for reports it and writes the whole list to manifest/nimbus-missing.xml. This command reads that manifest and retrieves the items — and then stops. Not re-running the tests is the difference between it and nimbus test --fetch-missing.

Transport follows the table above. An Apex-only manifest comes back over the Tooling API and lands in the same files at the same paths, each with its -meta.xml companion. A manifest naming the single-file types comes back over the Metadata API as a package and is converted to source format on the way to disk. A manifest naming an object, a field or a single label goes through a single sf project retrieve start --manifest call instead.

Object and field schemas are separate again: they come back as REST describes written to .nimbus/schemas/, because that is what the runtime actually reads.

An org is required and must be explicit — -o <alias> or a default target-org. Run the tests again afterwards: retrieving one class often reveals the next missing reference behind it, so a project with deep unresolved chains takes a few rounds.

Flags

FlagDefaultDescription
--jsonfalseEmit the retrieval summary (retrieved, failed, errors) as JSON
--org, -orequiredOrg to retrieve the missing metadata from

Progress goes to stderr, so --json owns stdout and a plain run still shows what is happening during a retrieve that can take a minute. Any failed item exits non-zero.

nimbus metadata deploy

Push files, directories or a manifest into an org. An argument is a file path, a directory, or a bare Apex name resolved against the project's package directories.

bash
nimbus metadata deploy AccountService -o my-sandbox
nimbus metadata deploy force-app/main/default/classes/AccountService.cls -o my-sandbox
nimbus metadata deploy AccountService AccountServiceTest -o my-sandbox
nimbus metadata deploy force-app/main/default/permissionsets -o my-sandbox
nimbus metadata deploy --manifest manifest/package.xml -o my-sandbox
nimbus metadata deploy AccountService -o my-sandbox --check-only

A handful of .cls or .trigger files keeps the Tooling API's metadata-container flow — the same one the Salesforce IDE plugins use to save a file — because that is the path that reports the Apex compiler's own errors with line and column:

bash
Deploy failed: 1 component error.

  AccountService (line 7, column 13): Variable does not exist: bogus

Anything wider goes over the Metadata API as a package, and anything the third row of the table above names goes to the Salesforce CLI with the identical invocation. Only a bare name is Apex-specific: other types' member names are not unique across types — an "Account" is a layout, a tab and a profile — so pass a path for those.

A deploy rolls back as a unit, so one component's error means none of them landed. Any component error exits non-zero.

This command is the inner loop, not the release: it is for the twenty seconds between changing a file and wanting it in a sandbox. For the full assurance cycle — staged release, validation, tests, receipt — use nimbus deploy instead.

Conflict detection

A deploy replaces the org's copy, and the org does not ask whether yours was derived from theirs. So before anything is pushed, Nimbus asks the org one question: has this class changed since this working copy last took it?

It can answer that because every time Nimbus writes a file from an org — a retrieve — or pushes one to it, it records the org's LastModifiedDate, who saved it, and a hash of the bytes involved. That record is the sync base, and it lives in .nimbus/org-base.json, keyed by org id. It describes this working copy, so it belongs outside version control — committing it would hand a teammate a claim about their files that was never true of them.

There are four outcomes, and only one of them stops anything:

FlagDefaultDescription
Base matches the orgsilentNobody has touched it. The deploy runs and says nothing about it.
Org changed after your baserefusedNames who changed it and when, and exits non-zero. --force overrides.
No base recordedone warningNimbus has never retrieved or deployed it, so it can't tell. The deploy runs.
Your file already matchesskippedThe bytes are already in the org. Nothing is deployed, and it exits zero.
bash
Refusing to deploy: the org's copy is newer than yours.

  AccountService changed in the org on 2026-08-26 11:02 by Dana Ruiz, after your last retrieve.

Deploy anyway with --force, or take the org's copy with:
  nimbus sf project retrieve start -m ApexClass:AccountService -o my-sandbox

Brownfield projects start with no bases at all, which is why a missing one warns rather than refuses:

bash
No sync base for AccountService — nimbus can't tell whether the org copy changed. Deploying anyway.

That warning is self-limiting: a successful deploy records a base, so from the first one onward the file is checked. Warnings and notices go to stderr, so --json still owns stdout — under --json a refusal is a single document with "status": "Refused" and the same sentence in message.

Three deliberate limits. --check-only is never refused — it saves nothing, so it reports the conflict and compiles anyway. A check that cannot run (unreachable org, no login, org.native=false) prints one line and lets the deploy through; conflict detection is never a reason a deploy fails. And a class the org does not have yet is a create, so it is silent. Use nimbus org status to read the bases without deploying anything.

Flags

FlagDefaultDescription
--check-onlyfalseValidate without saving, to see the errors without changing the org
--dry-runfalseAlias for --check-only, matching the Salesforce CLI's spelling
--forcefalseDeploy even when the org's copy changed after your last retrieve
--manifest, -xDeploy the components a package.xml selects, instead of named paths
--jsonfalseEmit the deploy result as JSON, in the Salesforce CLI's deploy shape
--org, -orequiredOrg to deploy into

nimbus rename

Rename a custom field or a custom object and update every place this project refers to it — Apex, SOQL, metadata XML, LWC, Aura and Visualforce — with a preview you approve before anything is written.

bash
# Preview. Nothing is written.
nimbus rename field Account.Legacy_Code__c Account_Code__c

# Write the changes
nimbus rename field Invoice__c.Amt__c Amount__c --apply

# Rename an object: its references, its directory, and every file named after it
nimbus rename object Invoice__c Bill__c --apply

# The plan as JSON, for an editor to drive
nimbus rename field Account.Legacy_Code__c Account_Code__c --json

The preview is the product. It lists every file, every line, old above new, and the reason that line was believed to mean your field. Nothing is written without--apply, and an apply is all-or-nothing: every file is re-hashed against the plan first, and one file that changed underneath aborts the whole thing before a byte is written.

What gets updated, and why each one is safe

AttributionWhat it means
qualifiedThe source spells the owner out: Account.Old__c, Schema.Account.Old__c, @salesforce/schema/Account.Old__c, <field>Account.Old__c</field>, or a new Account(Old__c = …) initialiser.
path-scopedThe file is that object’s own metadata: objects/Account/…, layouts/Account-…, workflows/Account.….
soql-fromThe occurrence sits in a query clause whose FROM names the object — including the field read off a query’s result, [SELECT F__c FROM Obj].F__c.
relationshipIt sits behind a Lookup__r whose lookup points at the object.
sole-ownerExactly one object in this project declares a field by that name, so a bare mention has one possible meaning.
definitionThe component’s own metadata: the field-meta.xml, the object-meta.xml, and the file or directory carrying the API name.
type-positionObject renames only: a slot only a type can occupy — new Foo__c(), List<Foo__c>, FROM Foo__c, (Foo__c) x, Foo__c.SObjectType.

What it will not decide for you

A field and the object it looks up routinely share an API name —ServiceDelivery__c.ProgramEngagement__c pointing atProgramEngagement__c. So a field rename never touches a slot only a type can occupy, and an object rename never touches a slot only a field can occupy. Everything else that mentions the name and cannot be attributed is listed as an unresolved candidate for you to check by hand — never silently skipped, and never silently edited. A query assembled by string concatenation is the common case:'SELECT ' + fields + ' FROM Account' names no field this reading can attribute, so the fragment is reported.

Every plan also carries what it cannot see: Apex comments are ignored entirely, receiver types are resolved from declarations in the same file only,stubs/ is skipped because those API names belong to an installed package, and the whole thing covers your source — the org-side rename that moves the data and preserves history is still Setup → Object Manager.

nimbus rename field

The field is named with its object, because a bare field name is not a field: two objects can both declare Status__c, and which one you mean decides which mentions in your source are yours. The new name may be given bare or with the same object prefix.

nimbus rename object

Renames a custom object, custom setting or custom metadata type, along with the directory its metadata lives in and every file whose name leads with the API name — its layouts, its tab, its quick actions, its workflow, its object translations.

FlagDefaultDescription
--applyfalseWrite the changes. Without it nothing is written.
--yesfalseSkip the confirmation prompt. Required with --apply when there is no terminal.
--jsonfalseEmit the plan as JSON (schema: nimbus.rename/v1) - the same payload an editor drives a rename from: target, per-file changes with byte offsets and a SHA-256 digest, unresolved candidates, skipped mentions, and the limits.

nimbus record

Set up managed-package record/replay in one command: pull the packages' stubs from an org, then run the matching tests with recording on. After it finishes, plain nimbus test replays the recorded answers offline — no org, no flag.

bash
nimbus record -o my-org                # pull stubs, then record the whole suite
nimbus record -o my-org MyTestClass    # pull stubs, then record one class
nimbus test                            # from now on: replays offline

It is exactly nimbus stub pull followed by nimbus test --record, which both remain available individually — stub pull when you want the package's shape without running anything, test --record to re-record once stubs exist. The pull is additive (project metadata and existing stub files are left alone), so re-running after a package upgrade is safe; recording re-records each matched test method wholesale.

FlagDefaultDescription
--org, -o, --target-orgrequiredOrg to pull stubs from and forward recorded calls to. Must be explicit — the calls really execute there, so prefer a scratch or developer org.
--namespacesevery namespace foundComma-separated managed-package namespaces to pull.
--datafalseAlso record org data during the pull: custom-setting rows into nimbus.properties, custom-metadata records into stubs/customMetadata/.
--forcefalseOverwrite stub files that already exist (project metadata is never overwritten).

Recordings land in .nimbus/recordings/, one JSON file per test method, and are meant to be committed — the team and CI then run against the same answers with no org access. Details of the record/replay mechanics are in the stub documentation.

nimbus stub

Scaffold and inspect stubs/ — the directory Nimbus loads before the main source tree so tests can reference managed-package classes, custom objects and fields, and custom labels that aren't in the project. See also the User Stubs / Managed Packages section.

List existing stubs

bash
nimbus stub list    # Show every .cls under stubs/
nimbus stub path    # Print the absolute stubs/ directory

Scaffold a new stub class

bash
# Minimal stub with an invoke() no-op
nimbus stub add Logger

# Nebula-style stub with specific method signatures
nimbus stub add Logger \
  --method info:void \
  --method error:void \
  --method saveLog:void

# Typed arg lists (emits "public static void info(String arg0)")
nimbus stub add Logger --method 'info(String):void'

# Replace an existing stub
nimbus stub add Logger --method debug:Boolean --force

Each generated class exposes public static Integer callCount and public static List<String> calls so tests can assert on how the stub was exercised. Edit the file freely — it's plain Apex.

Namespace-scoped stubs

Managed-package classes accessed as ns.ClassName (e.g. mp.Logger.info('hi')) need both a simple class name and the ns. prefix to resolve. Nimbus writes namespace stubs to a subdirectory matching the namespace, and the runner registers both lookup keys automatically.

bash
# Either form is accepted — the dot is treated as a namespace prefix
nimbus stub add mp.Logger --method 'info(String):void'
nimbus stub add Logger --namespace mp --method 'info(String):void'

# Writes: stubs/mp/Logger.cls (class body still says "public class Logger")
# Test code can call: mp.Logger.info('hi');

If you prefer to hand-author, just put the file under stubs/<ns>/<Class>.cls. The first-level subdirectory is the namespace; deeper nesting is ignored.

Scaffold a custom label

nimbus stub label writes a label into stubs/labels/ so Apex that reads it resolves to a real value instead of an empty string. Unnamespaced labels land in CustomLabels.labels-meta.xml; a managed-package label lands in <ns>.labels-meta.xml, where the file name is what declares the namespace — the metadata format has nowhere to record another package's namespace, and Label.<ns>.<Name> is the only legal Apex spelling for one.

bash
# Unnamespaced → stubs/labels/CustomLabels.labels-meta.xml
nimbus stub label Welcome_Message --value "Hello there"

# Either form is accepted for a managed-package label
nimbus stub label npe03.RecurringDonationStageName --value Pledged
nimbus stub label RecurringDonationStageName --namespace npe03 --value Pledged

# Several at once (value defaults to the label name)
nimbus stub label Alpha Beta Gamma --namespace npe01
FlagDefaultDescription
--namespace-Managed-package namespace; selects stubs/labels/<ns>.labels-meta.xml. A ns.Name argument sets it too.
--valuethe label nameLabel value. Applies to a single label, so pass one name at a time when using it.

Re-running is safe: a label that already exists in the file has its value updated in place instead of being duplicated. Apex then reads Label.npe03.RecurringDonationStageName or System.Label.Welcome_Message as usual.

Pull labels, fields, and classes from an org

nimbus stub pull reads an org that has the managed packages installed and writes the parts your project is missing into stubs/. Labels come from the Tooling API's ExternalString entity — the only place another package's labels are readable — fields come from SObject describes, and classes come from ApexClass.SymbolTable, which exposes the exact global surface of an installed package: every method overload, constructor, property, inner class, and enum. It needs the Salesforce CLI (sf) on PATH and an authenticated org.

bash
# Everything the org has that the project doesn't
nimbus stub pull --org dev

# Just the packages you care about
nimbus stub pull --org dev --namespaces npe01,npe03,npo02

# See what it would do first (prints every sf command it runs)
nimbus stub pull --org dev --namespaces npe03 --dry-run

# Class stubs for one package, without running anonymous Apex
nimbus stub pull --org dev --namespaces Nebula --classes-only --no-exec

# Narrow the describes to specific objects
nimbus stub pull --org dev --objects Contact,Opportunity --fields-only
FlagDefaultDescription
--org, -o, --target-orgdefault target-orgOrg alias to read. Fails with the Salesforce CLI's own message if the alias is unknown.
--namespacesevery namespace foundComma-separated managed-package namespaces to pull.
--objectsdiscovered from the projectDescribe only these SObjects instead of deriving the list from the project.
--dry-runfalseQuery the org and print what would be written without touching disk.
--forcefalseOverwrite stub files that already exist. Project metadata is never overwritten.
--labels-onlyfalsePull custom labels only — no describes, so it finishes in one round trip.
--fields-onlyfalsePull fields and objects only.
--classes-onlyfalsePull managed-package classes only.
--no-execfalseSkip the anonymous Apex that records global constant values; constants stay null.
--datafalseAlso record org data: custom-setting rows as seed lines in nimbus.properties, custom-metadata records into stubs/customMetadata/.
--data-onlyfalseRecord org data only (implies --data).

What gets written. stubs/labels/<ns>.labels-meta.xml for labels, stubs/objects/<Obj>/fields/<ns__Field__c>.field-meta.xml for namespaced fields on objects your project references, stubs/objects/<ns__Obj__c>/<ns__Obj__c>.object-meta.xml plus its fields for namespaced objects the project uses but does not define, and stubs/<ns>/<Class>.cls for every global class of the pulled namespaces.

Class stubs record the org's behavior, not a guess. Signatures are the org's own, so the overloads your tests call resolve the way the package declared them. Bodies are stubbed with usable defaults: builder methods hand back a constructed object instead of null, collection returns come back empty instead of null, and enums keep their declaration order so ordinals survive. Global constants have no value in a SymbolTable, so their values are read by running one short anonymous Apex script per class against the org — pass --no-exec to skip that. A recorded constant keeps its final modifier; an unrecorded one loses it so your tests can assign it. Two fidelity limits are inherent to the source: the SymbolTable erases generics (a List<Contact> parameter arrives as List, rendered as List<Object>, and overloads that collapse under erasure are deduplicated and reported), and a @Deprecated class is invisible to anonymous Apex, so its constants stay null.

--data records the package's data, opt-in. Custom-metadata records become ordinary stubs/customMetadata/<Type>.<Record>.md-meta.xml files, so SOQL against the type and getInstance(name) return what the org returns. Custom-setting rows become seed lines in a fenced, regeneratable block of nimbus.properties: the org-wide default of a hierarchy setting as nimbus.seed.org-default.<Object>, list-setting rows as nimbus.seed.record.<Object>.<Name>. Nothing outside the fence is ever touched, and a value the property format cannot carry (commas, equals signs, line breaks) is skipped by name rather than written wrong. After a pull that adds schema, run nimbus sync so the next test run sees it — the pull reminds you.

Which objects get described. The objects your project ships objects/ metadata for, the standard objects packages usually extend (Account, Campaign, Contact, Lead, Opportunity), any namespaced object name your Apex mentions, and the lookup targets of the fields it pulls.

What it never touches. Anything the project already defines, and anything already under stubs/ unless --force is passed — so re-running after a package upgrade adds what's new and leaves your hand edits alone. Every file written and every skip is printed, and the summary line counts both.

A namespace the org refuses does not sink the pull. A timeout, a transient API failure, or a package whose SymbolTable the org will not project stops that namespace and nothing else: the ones that answered are written, and the run ends by naming each namespace it skipped with the org's own error and the exact command that pulls just those. The exit code is non-zero, and nothing on disk records a skipped namespace as pulled, so the re-run fills precisely the gap. A failure of namespace discovery itself is still fatal — no namespace was established, so there is nothing partial to keep.

Remove a stub

bash
nimbus stub remove Logger

Auto-generate stubs from project source Pro

nimbus stub auto walks every Apex source file, finds references to classes Nimbus can't resolve (managed packages, missing project files), and writes one .cls per class — methods, constructors, and fields scaffolded from how your code actually uses them. Return types are inferred from assignment LHS, return statements, casts, logical operators (Boolean), and string concatenation (String). Generic type arguments (List<MyType>) are preserved.

bash
# Preview what would be written
nimbus stub auto --dry-run

# Generate (skips existing files; safe to re-run)
nimbus stub auto

# Re-run after adding new project usage
nimbus stub auto --merge        # appends new methods, preserves hand edits
nimbus stub auto --force        # full rewrite (discards hand edits)

Or fold it into the test loop:

bash
nimbus test --write-stubs                       # generates after a green run
nimbus test --write-stubs --write-stubs-merge   # appends to existing stubs
nimbus test --write-stubs --write-stubs-force   # rewrites existing stubs

Layout. Auto-stub writes one file per class under the namespace folder — stubs/Nebula/Logger.cls with public class Logger { ... }. The stub loader registers both Logger and Nebula.Logger as aliases, so test code can call Nebula.Logger.info() the same way it would against the installed package. The hand-written nested-class convention (stubs/Nebula/Nebula.cls with Logger as an inner class) is equally valid; both resolve identically at runtime.

Standard objects are never stubbed. A reference to a standard Salesforce object Nimbus has no local describe for looks like an unresolved class, but writing an Apex class named after an SObject shadows the SObject — so auto-stub writes nothing for it and prints the command that does resolve it: nimbus sync -s AccountContactRelation -o <alias>. Objects the project has already pulled from its org are left out of that advice.

What's not inferred. Parameter names aren't carried by call sites — generated stubs use arg0, arg1. Method bodies are an audit-friendly default: callCount++ and calls.add('methodName'), returning a type-default. Hand-edit either after generation; --merge on subsequent runs leaves your edits alone.

Recording per-call behavior

A stub carries a package class's shape; its method bodies return type-defaults. When a test depends on what the real package actually returns, record it once against an org and replay it offline from then on:

bash
nimbus record -o my-org MyTest          # one command: pulls stubs, then records against the org
nimbus test MyTest                      # replays from .nimbus/recordings/ - no org connection
nimbus test MyTest --no-replay          # ignores the recordings, runs the stub bodies
nimbus test MyTest --record -o my-org   # re-records without re-pulling stubs

Recordings are plain JSON, one file per test method, written as each method finishes so an interrupted run keeps what it captured. They are meant to be committed: the rest of the team and CI then run against the same answers with no org access. Re-recording a method replaces its file wholesale; other methods are untouched.

Fallback is always the stub body. A call that was never recorded, an argument with no JSON form, an org that refuses the forwarded call because the installed package version drifted — each prints one line and runs the stub body. Adding recordings to a suite cannot break it, and --record is never harder to complete than an ordinary run.

Scope. Static methods only — a local stub instance has no org counterpart to forward to, so instance methods keep their stub bodies. Callouts are deliberately out: the platform refuses a callout in a test without Test.setMock, so a deployable test already ships its own mocks. Forwarding is capped at 200 org round trips per run.

See the User Stubs page for the full workflow including dual-layout details.

nimbus upgrade

Check for a newer version of nimbus and upgrade the binary in-place.

bash
nimbus upgrade          # Download and install the latest version
nimbus upgrade --force  # Re-install even if already on the latest version

Nimbus also prints a one-line notice after every command when a newer version is available. Set NIMBUS_NO_UPDATE_CHECK=1 to disable this check.

Configuration

Global Flags

These flags work with every command:

FlagDefaultDescription
-o, --org, --target-org-Target Salesforce org alias (passed to SF CLI for sync/fallback). All three spellings are the same flag on every command — --target-org matches the Salesforce CLI, so scripts written against either tool work unchanged.
-v, --verbosefalseVerbose output - useful for debugging
-h, --help-Help for any command
--version-Print version and exit

Database Options

By default, Nimbus uses an embedded PostgreSQL instance in .nimbus/db/. For CI/CD or advanced setups, you can point at an external database.

FlagDefaultDescription
--db-providerembeddedDatabase provider: embedded, external
--db-url-Full PostgreSQL connection string (overrides provider)
--db-dir.nimbus/dbEmbedded Postgres data directory
--db-name-Database name override
--db-user-Database user override
--db-password-Database password override

External database

bash
# Use a Neon, RDS, or any managed Postgres
nimbus test --db-url "postgresql://user:pass@host:5432/mydb"

CI with external DB

bash
# In CI: use a service container or Neon ephemeral branch
nimbus test --db-url "$NIMBUS_DB_URL" --coverage --coverage-output json

Platform Fidelity Mode

Nimbus runs a compat validator pass against every program it loads. Inner-class restrictions, missing master-detail parents, exhausted governor limits, and similar rules surface as diagnostics tagged with a compat rule ID.

nimbus.compat.mode decides how diagnostics surface.

FlagDefaultDescription
strict(default)Emit diagnostics as errors and fail the test run with a non-zero exit code. Intended for CI.
warn-Print diagnostics to stderr but keep the run passing. Useful for local dev on a project that can’t close every gap at once.
off-Suppress diagnostics entirely. Opt-out; not recommended.
bash
# nimbus.properties
nimbus.compat.mode=strict

# Relaxed for day-to-day dev, strict in CI:
%dev.nimbus.compat.mode=warn
%ci.nimbus.compat.mode=strict

Governor Limits

Nimbus enforces Salesforce governor limits during execution and exposes them through the Apex Limits class. nimbus.governor.modecontrols how a limit breach is handled.

FlagDefaultDescription
strict(default)Throw System.LimitException when a limit is exceeded, matching the platform.
warn-Print a warning the first time a limit is exceeded but keep executing.
off-Do not enforce limits. Counters still accumulate for the Limits class.

Limits follow the platform by execution context. Synchronous code gets 100 SOQL queries; asynchronous code (Queueable, @future, Batch) gets 200. CPU time and heap likewise rise in async contexts. Both enforcement and the Limits class observe the context-appropriate value.

Orgs with Salesforce-raised limits can raise the ceilings to match. An explicit override is authoritative and applies in every context — it drives both enforcement and the Limits.getLimitQueries() / Limits.getLimitDmlStatements() return values. Leave a key unset to keep the context-aware platform default.

FlagDefaultDescription
nimbus.governor.modestrictEnforcement mode: strict | warn | off
nimbus.governor.soql-queries100 / 200Override SOQL queries per transaction. Unset: 100 sync, 200 async.
nimbus.governor.dml-statements150Override DML statements per transaction
nimbus.governor.heap-size0Override heap size in bytes. 0 keeps the platform default.
nimbus.governor.heap-limitswinter27Which platform heap ceiling applies when heap-size is unset: winter27 | summer26
bash
# nimbus.properties — match an org with Salesforce-raised limits
nimbus.governor.mode=strict
nimbus.governor.soql-queries=300
nimbus.governor.dml-statements=300

Heap in Winter ’27

Winter ’27 (API 68.0) raised the Apex heap ceiling org-wide: 6 MB to 10 MB synchronous, 12 MB to 25 MB asynchronous. Nimbus uses the new pair by default. The raise is not gated on a class’s apiVersion — it lands on the whole org at upgrade — so it is a project setting here, not a per-class one.

Salesforce gives non-production orgs a transition switch back to the Summer ’26 ceiling, so code cannot come to depend on headroom an org that has not been upgraded yet does not have. nimbus.governor.heap-limits=summer26 is that switch. Limits.getLimitHeapSize() reports whichever ceiling is in force.

bash
# nimbus.properties — hold the pre-Winter '27 ceiling
nimbus.governor.heap-limits=summer26

Integration Tests

Winter ’27 (API 68.0) added an integration-test lifecycle to Apex. Nimbus runs all three annotations:

FlagDefaultDescription
@IntegrationTestclass + methodCollected, run and reported like @isTest. Additionally opts the method into real callouts.
@BeforeClassstatic voidRuns once before the class’s test methods; its data is shared by all of them.
@TearDownstatic voidRuns after every test method, pass or fail. Several are allowed; Nimbus runs them in name order.
bash
@IntegrationTest
public class OrderSyncIT {
    @BeforeClass
    static void seed() {
        insert new Account(Name = 'IT_OrderSync_Acct');
    }

    @IntegrationTest
    static void syncsAnOrder() {
        // ...
    }

    @TearDown
    static void cleanup() {
        delete [SELECT Id FROM Account WHERE Name = 'IT_OrderSync_Acct'];
    }
}

Two differences from the platform are deliberate. First, a Nimbus test always runs inside a transaction that rolls back, integration tests included — test isolation is the property the local runner exists to provide. @TearDown still runs on the same schedule, so cleanup code is exercised rather than skipped.

Second, an unmocked callout inside an @IntegrationTest method does not reach the network unless the project says so. A test runner you install to work without an org should not open connections on a downloaded test class’s behalf. With the setting off, the callout raises a System.CalloutException naming the key; a registered Test.setMock still wins when there is one.

FlagDefaultDescription
nimbus.test.integration-calloutsmockWhat an unmocked @IntegrationTest callout does: mock | live
bash
# nimbus.properties
nimbus.test.integration-callouts=live

SOQL additions

FORMULA('<expr>') filters on an expression without a formula field on the object. Nimbus compiles arithmetic — field references, numeric literals, + - * /, parentheses and unary minus. Formula functions (IF, TEXT, ISBLANK), string literals and relationship hops are reported as unsupported by name rather than guessed at.

Division is decimal, matching the formula language rather than Postgres integer division, and is guarded so an empty divisor drops the row instead of failing the query. Subtracting two Date fields gives whole days; subtracting two DateTime fields gives fractional days. Mixing a DateTime with a number is refused.

bash
List<Order__c> quick = [
    SELECT Id, Name
    FROM Order__c
    WHERE Revenue__c > 600
      AND FORMULA('ShipDate__c - OrderDate__c') <= 2
];

SET OPTIONS :queryOptions binds a Database.QueryOptions to one query. Its explicitNamespace property is accepted and validated; it resolves an ambiguity between two same-named fields installed side by side in one subscriber org, and a local project has one definition per field, so there is nothing to disambiguate. A bind of any other type raises System.QueryException, as the platform’s compiler would.

Both require a class saved at apiVersion 68.0 or later. Below that, Nimbus raises System.QueryException naming the version — the platform rejects the same query at save time.

In the IDE

The VS Code and IntelliJ extensions discover an @IntegrationTest class exactly as they discover an @isTest one — gutter icons, the Tests panel, and the Run, Debug and Coverage lenses — and the run target is the test method, so @BeforeClass and @TearDown get no Run button, the same as @TestSetup. All three annotations complete after @. Database.QueryOptions completes its constructor and explicitNamespace, and after SET OPTIONS the popup offers the Database.QueryOptions binds in scope rather than the object’s fields. FORMULA and OPTIONS colour as the clause words they are — in a .soql file, and inside a query in a class, without recolouring a variable that happens to share the name.

Parallel Isolation

Every test runs inside its own Postgres transaction that rolls back at the end, so per-test state is always clean. When running in parallel, however, all workers share the same public schema by default and can hit row-level lock contention on hot tables.

Opt in to per-worker-schema to give each parallel worker its own cloned schema. On setup Nimbus replicates every public table (structure + seed rows) into a worker_N schema; every worker pins its transactions’ search_path there so row writes, locks, and visibility are physically confined per worker.

FlagDefaultDescription
shared(default)All workers share the public schema. Per-test transaction rollback still isolates state.
per-worker-schema-Each worker gets its own Postgres schema cloned from public. Adds one-time clone cost at startup; eliminates cross-worker lock contention.
bash
# nimbus.properties
nimbus.test.isolation=per-worker-schema

# Typical pattern: shared locally for speed, isolated in CI for safety
%ci.nimbus.test.isolation=per-worker-schema

Org Simulation

Nimbus simulates the running org via UserInfo and the singleton Organization SOQL row. These values default to en_US / USD / Developer Edition / not-sandbox, which works for most tests. When your code branches on locale, currency, sandbox flag, or org features (Multi-Currency, Person Accounts), set the matching key in nimbus.properties or pass the equivalent CLI flag.

FlagDefaultDescription
nimbus.org.currencyUSDReturn value for UserInfo.getDefaultCurrency().
nimbus.org.localeen_USReturn value for UserInfo.getLocale(). Also set as languageLocaleKey on Organization.
nimbus.org.language(falls back to locale)Return value for UserInfo.getLanguage().
nimbus.org.timezone(org user's zone, else this machine's)The zone tests run in: UserInfo.getTimeZone(), Datetime.format(), Date.today() and the date-literal windows. nimbus sync -o records the org user's TimeZoneSidKey and that is used; a project that never synced runs in the machine's zone. Set the key to pin a zone regardless.
nimbus.org.sandboxfalseReturn value for Organization.IsSandbox. Toggle when your code branches on sandbox vs production.
nimbus.org.features(none)Comma-separated feature toggles. MultiCurrency adds the CurrencyIsoCode field to standard and custom objects and makes UserInfo.isMultiCurrencyOrganization() return true; without it the field does not exist, matching a single-currency org. PersonAccounts is reserved for future schema-level behaviour.
nimbus.org.faketime(real clock)ISO-8601 instant that pins Date.today() / DateTime.now() / System.now() for deterministic time-dependent tests.
bash
# nimbus.properties
nimbus.org.currency=EUR
nimbus.org.locale=de_DE
nimbus.org.sandbox=true
nimbus.org.features=MultiCurrency
nimbus.org.faketime=2030-01-15T12:00:00Z

Equivalent CLI flags on nimbus test override the config file when both are set: --sandbox, --feature MultiCurrency,PersonAccounts, --faketime 2030-01-15T12:00:00Z.

MultiCurrency changes the shape of your schema, not just what UserInfo reports. On a single-currency orgCurrencyIsoCode does not exist at all — the describe omits it and SOQL rejects it by name — so Nimbus leaves it out by default. Toggling the feature re-syncs the schema cache automatically; you do not need to clear anything.

Permission Seam Visibility

Nimbus runs tests as a sysadmin-equivalent default user. Outside System.runAs(), FLS checks return true, WITH SECURITY_ENFORCED passes through, and $Permission.X / FeatureManagement.checkPermission() resolve against the default mock user (005000000000000AAA). This is faithful to Salesforce's "tests run as the configured user" model — but it creates a silent divergence: a test can pass under Nimbus and fail in a real org when the org's running user isn't assigned the perm.

Permission seams make every such case visible per-test. When a passing test consulted a permission outside a runAs block, the runner records a seam: the kind of check (FLS, object perm, SOQL user-mode, custom permission), the subject, and how it resolved (default-allow for FLS/object/SOQL; default-mock-user for custom perms granted via seeded nimbus.mock.permission-sets or test-inserted PSAs). Custom-permission seams use a distinct CUSTOM PERM prefix because they look like business logic, not security, in the test code — the most damaging variant of the divergence.

By default the runner shows a single end-of-run summary line with totals. --show-permission-seams expands it to a per-test list. --strict-permissions fails any test whose passing path consulted a permission outside runAs — opt-in for CI users who want to enforce the discipline.

bash
# Default summary (no flags) — single line under the test summary block:
# Permission seams: 12 tests consulted permissions outside System.runAs()
#   8 custom permission checks ($Permission / FeatureManagement.checkPermission)
#   4 FLS / object / SOQL user-mode checks

# Per-test detail:
nimbus test --show-permission-seams

# CI gate — fail tests with seams:
nimbus test --strict-permissions

# nimbus.properties (committable, profile-aware):
nimbus.test.permission-seams.show=true
%ci.nimbus.test.permission-seams.strict=true

Tests that explicitly use System.runAs() produce zero seams — the test author opted in to an explicit identity, so any divergence is by design. The fix when strict mode flags a test is to wrap the assertion in System.runAs(user) where user has the required PSA.

Under --json, a run that crossed at least one seam adds a top-level permission_seams block — { total, customPermission, flsObject, seams: [{ kind, subject, path, file?, line?, custom }] } — so agents and the release gate can read the divergence without scraping human output. It is omitted entirely on a clean run (zero seams). This is the block a release profile's permissionSeamPolicy reads to block a release that passed only through the default-allow path.

Read-only Mode

Refuse any DML inside tests via --readonly. Useful for CI stages that should only observe — e.g. post-merge sanity checks that re-run the suite against frozen data. When enabled, any insert, update, delete, orupsert statement inside a test throws System.DmlException with a diagnostic message.

bash
# nimbus.properties
nimbus.test.readonly=true

# Or per-profile:
%ci.nimbus.test.readonly=true

# Or as a one-off CLI flag:
# nimbus test --readonly

Setup-time DML (seeding profiles, list views, custom setting records) still succeeds — only DML inside an @IsTest method is rejected.