CLI Reference
Complete reference for the nimbus command-line tool.
Getting Started
Installation
Single static binary. No JVM, no Node, no Docker. macOS, Linux, and Windows (native, no WSL) are all supported.
# macOS / Linux
curl -fsSL https://install.testnimbus.dev | sh
# Windows — works in cmd.exe, Windows Terminal, or any shell
powershell -c "irm https://install.testnimbus.dev/win | iex"
# Windows alternative: Scoop
scoop bucket add nimbus https://github.com/nimbus-solution/scoop-nimbus
scoop install nimbus
# Verify
nimbus --versionThe installer verifies a SHA256 checksum and drops the binary in ~/.local/bin (macOS/Linux) or %LOCALAPPDATA%\Programs\Nimbus (Windows). Re-run any time to upgrade. See the Quickstart guide for platform-specific notes.
Quick Start
From a fresh install to a passing test in five commands:
# 1. Initialize Nimbus in your SFDX project root
cd path/to/your/sfdx-project
nimbus init
# 2. Verify setup
nimbus doctor
# 3. Run all tests
nimbus test
# 4. Run a specific class or method
nimbus test AccountTriggerTest
nimbus test AccountTriggerTest.testInsertCreatesContact
# 5. Run with coverage
nimbus test --coverageThe first run parses your Apex into an AST cache — a few seconds. Subsequent runs are milliseconds. See the full Quickstart guide for the starter project, Dev UI, and org sync steps.
Project Setup
Nimbus stores its state in a .nimbus/ directory at your project root:
.nimbus/
├── db/ # Embedded PostgreSQL data
├── cache/ # Parsed AST cache
└── traces/ # Execution traces (when --trace is used)Add .nimbus/db/ and .nimbus/cache/ to your .gitignore. Traces can optionally be committed for sharing with your team.
# .gitignore
.nimbus/db/
.nimbus/cache/User Stubs / Managed Packages
Sometimes Nimbus doesn't natively support a class or API: managed packages (Nebula Logger, FSL, Vlocity), unsupported built-in APIs (ConnectApi, Metadata), or custom code you want to mock. Use stubs to provide your own implementation.
What are stubs?
Stubs are user-provided Apex classes that implement APIs Nimbus doesn't know about. Instead of blocking on missing code, you implement just enough for your tests to run - then commit stubs to git so your team shares the same behavior.
How to use stubs
Create a stubs/ directory in your project root, with one folder per managed package. Each package folder holds everything for that package — Apex classes and namespaced custom objects/fields. Nimbus loads it all with the lowest priority; anything in force-app/ with the same name takes precedence.
my-salesforce-project/
├── force-app/
├── stubs/ # User-provided implementations
│ ├── Nebula/ # one folder per package
│ │ ├── Nebula.cls # Apex surface (Nebula.Logger, ...)
│ │ └── objects/ # namespaced custom objects
│ │ └── Nebula__LogEntryEvent__e/...
│ └── ConnectApi/
│ └── ConnectApi.cls # ConnectApi.FeedItem, etc.
└── sfdx-project.jsonAdding another package later is just stubs/<NewPackage>/ next to the others. Surfaces stay cleanly separated, no cross-package collisions.
Writing a stub: example approach
Managed packages use namespaced classes like Nebula.Logger. In Apex, the namespace becomes the outer class and the package class becomes an inner class. Create stubs/Nebula.cls:
// stubs/Nebula/Nebula.cls
public class Nebula {
public class Logger {
public static LogEntryEventBuilder info(String message) {
System.debug('INFO: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder info(String message, SObject record) {
System.debug('INFO: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder info(String message, List<SObject> records) {
System.debug('INFO: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder info(String message, Id recordId) {
System.debug('INFO: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder info(String message, List<Id> recordIds) {
System.debug('INFO: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder error(String message) {
System.debug('ERROR: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder error(String message, SObject record) {
System.debug('ERROR: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder error(String message, List<SObject> records) {
System.debug('ERROR: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder error(String message, Id recordId) {
System.debug('ERROR: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder error(String message, List<Id> recordIds) {
System.debug('ERROR: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder warn(String message) {
System.debug('WARN: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder warn(String message, SObject record) {
System.debug('WARN: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder warn(String message, List<SObject> records) {
System.debug('WARN: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder warn(String message, Id recordId) {
System.debug('WARN: ' + message);
return new LogEntryEventBuilder();
}
public static LogEntryEventBuilder warn(String message, List<Id> recordIds) {
System.debug('WARN: ' + message);
return new LogEntryEventBuilder();
}
public static void saveLog() {
// Stub: no-op
}
}
public class LogEntryEventBuilder {
public LogEntryEventBuilder addTag(String tag) {
return this;
}
public LogEntryEventBuilder setRecord(Object record) {
return this;
}
public LogEntryEventBuilder setException(Exception ex) {
return this;
}
public LogEntryEventBuilder setExceptionDetails(Exception ex) {
return this;
}
}
}How stubs are resolved
Stubs are loaded as regular Apex classes — no special namespace mapping. When your production code calls Nebula.Logger.info(), the interpreter resolves Nebula as a class and Logger as an inner class, finding the stub naturally.
If a test calls a managed package class that doesn't have a stub, Nimbus degrades gracefully: it logs a warning and returns null instead of failing the test. Stubs are opt-in — you only need one when you require specific return values or side effects.
Namespaced custom objects
Some managed packages ship custom objects with a namespace prefix (Nebula__LogEntryEvent__e, fflib__Setting__c, etc.). If your tests do DML or SOQL against them, drop the schema XML inside the same package folder under objects/:
stubs/
└── Hoplog/
├── Hoplog.cls # Apex surface (Hoplog.Logger, ...)
└── objects/
└── Hoplog__LogEntry__c/ # namespaced custom object
├── Hoplog__LogEntry__c.object-meta.xml
└── fields/
├── Hoplog__Severity__c.field-meta.xml
├── Hoplog__EventType__c.field-meta.xml
└── Hoplog__Message__c.field-meta.xmlNimbus reads the object/field XML the same way it reads any custom object in force-app/ — creates a matching table in the embedded Postgres so insert new Hoplog__LogEntry__c(...) and SELECT ... FROM Hoplog__LogEntry__c work end-to-end without the package being installed in any org. By convention, capitalize the namespace folder and class name; Apex itself is case-insensitive (so hoplog.Logger and Hoplog.Logger both resolve), but consistent capitalization is clearer on grep and in code review.
Equivalent layout: one file per class
The nested layout above (one file per package, classes as inner classes) co-locates the full Apex surface alongside any namespaced custom objects. An equivalent layout is one file per class — stubs/<ns>/<Class>.clswith public class <Class> { ... } directly, no outer wrapper. The stub loader registers <ns>.<Class> as an alias automatically, so both mp.Logger.info() and Logger.info() resolve to the same stub. Both layouts are first-class — pick whichever fits your editing style. nimbus stub add and nimbus stub auto (Pro) write the per-class layout; the nested layout is the hand-written convention.
Auto-null fallback behavior
When Nimbus encounters an unresolved class, it emits a warning and continues:
[warn] nebula.logger.Logger not found - returning null (add a stub to control behavior)
[warn] fsl.FieldServiceAPI not found - returning nullTests that don't assert on the return value of those calls will pass. Tests that do assert on the value will fail with a clear message pointing at the missing class. This means you can get your first green run without writing any stubs - add them incrementally as you need control over return values.
Suppressing entire namespaces
For closed managed packages where you don't need specific return values - and can't see the source anyway - the auto-null fallback handles most cases silently. If you want to explicitly declare a namespace as intentionally unsupported (suppressing the warnings), use nimbus.properties:
# nimbus.properties
# Comma-separated list of namespaces to treat as opaque - no warnings, methods
# return null. Matching is case-insensitive.
nimbus.stubs.namespaces=Nebula,fflib,fslThis is especially useful in CI where you want a clean output: warnings from auto-null suppressed, behavior consistent, no stub files cluttering the repo for namespaces your tests don't assert on.
Best practices
- One folder per package: Drop everything for a managed package under
stubs/<Pkg>/— Apex classes plus any namespaced custom objects. Inside, the outer class name matches the namespace and inner classes match the API - Keep stubs minimal: Only implement methods your tests actually call
- Match signatures: Parameters, return types, and access modifiers should match the real API
- Use sensible defaults: Return empty lists, null, or debug output to help test behavior
- Commit to git: Stubs are small and help team members reproduce tests locally without external dependencies
For truly closed packages (no public source), the auto-null fallback and nimbus.properties noop declarations are your tools - or the future recording mode.
Roadmap: recording mode
The only fully-automated path for truly closed managed packages - where no source exists and no stub registry can help - is recording mode. Run nimbus test --record -o my-org once against a real org; Nimbus captures the actual runtime return values. All future local test runs replay those values without org access. Same pattern as VCR (Ruby), WireMock (Java), and Polly (JavaScript).
HTTP Mocking
There are two separate contexts for HTTP in Nimbus, and they work differently:
- In tests - Salesforce does not allow real callouts inside
@isTestmethods. UseTest.setMock()with a standardHttpCalloutMockimplementation. Nimbus enforces the same rule as a real org. - In
nimbus exec- Apex runs outside of a test context, soTest.setMock()is not available. Use--mocksto provide a config file that Nimbus uses to intercept and respond to anyHttp.send()calls.
Test Callouts
Salesforce does not allow real HTTP callouts inside @isTest methods - any call to Http.send()without a registered mock throws System.CalloutException. Nimbus enforces the same rule.
The solution is the standard Salesforce pattern: implement HttpCalloutMock and register it with Test.setMock(). Nimbus supports this natively - no special setup required.
Basic example
@isTest
private class UserServiceTest {
private class UsersMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setHeader('Content-Type', 'application/json');
res.setBody('[{"id": 1, "name": "Alice"}]');
return res;
}
}
@isTest
static void testFetchUsers() {
Test.setMock(HttpCalloutMock.class, new UsersMock());
Test.startTest();
List<User__c> users = UserService.fetchUsers();
Test.stopTest();
Assert.areEqual(1, users.size());
Assert.areEqual('Alice', users[0].Name);
}
}This code deploys and runs identically on Salesforce and in Nimbus. No Nimbus-specific classes or imports needed.
Multiple endpoints
A single mock class handles all callouts made during the test. Use the request method and endpoint to route responses:
private class ApiMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setHeader('Content-Type', 'application/json');
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/users')) {
res.setBody('[{"id": 1}]');
} else if (req.getMethod() == 'POST' && req.getEndpoint().contains('/orders')) {
res.setStatusCode(201);
res.setBody('{"id": "ord-001"}');
}
return res;
}
}Exec Mocking
When running Apex outside of a test context with nimbus exec, Test.setMock() is not available. Any Http.send() call needs to be intercepted at the Nimbus level.
Pass a mock config file with --mocks. Nimbus intercepts all Http.send() calls and returns the matching configured response - no real network request is made.
Using a mock file
nimbus exec MyClass.run --mocks nimbus.mocks.yaml# nimbus.mocks.yaml
- method: GET
url: https://api.example.com/v1/users
status: 200
body: '[{"id": 1, "name": "Alice"}]'
contentType: application/json
- method: POST
url: https://api.example.com/v1/orders
status: 201
body: '{"id": "ord-001", "status": "created"}'
headers:
X-Request-Id: abc-123Inline mocks
For quick one-offs, define mocks directly on the command line:
# method:url:status:body
nimbus exec MyClass.run --mock "GET:https://api.example.com/v1/users:200:[]"
# multiple --mock flags
nimbus exec MyClass.run \
--mock "GET:https://api.example.com/v1/token:200:{"token":"abc"}" \
--mock "POST:https://api.example.com/v1/submit:201:{"ok":true}"Unmatched callouts
If Apex calls an endpoint with no matching mock entry, Nimbus throws an error and halts execution. Use --allow-unmocked to let unmatched calls fall through to the real network instead.
nimbus exec MyClass.run --mocks nimbus.mocks.yaml --allow-unmockedSequential responses
List multiple responses for the same endpoint to return them in order. The last response repeats once the list is exhausted.
# First call → 429, second call onward → 200
- method: GET
url: https://api.example.com/v1/items
responses:
- status: 429
body: '{"error": "rate limited"}'
contentType: application/json
- status: 200
body: '{"items": [{"id": 1}]}'
contentType: application/jsonConfig Reference
Full schema for entries in nimbus.mocks.yaml:
| Flag | Default | Description |
|---|---|---|
method | - | HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, TRACE |
url | - | Full URL or URL pattern to match against |
status | 200 | HTTP status code returned in the response |
body | "" | Response body string |
contentType | application/json | Value for the Content-Type response header |
headers | {} | Map of additional response headers (key: value) |
responses | - | List of sequential responses (overrides body/status/contentType for that entry) |
Response object (used inside responses:)
| Flag | Default | Description |
|---|---|---|
status | 200 | HTTP status code for this response |
body | "" | Response body string |
contentType | application/json | Content-Type for this response |
headers | {} | Additional response headers |
Committing mock files
Commit nimbus.mocks.yaml alongside your code so the whole team uses the same mock definitions. Use multiple files to separate concerns - one per integration, one per feature area:
nimbus exec MyClass.run --mocks mocks/auth.yaml --mocks mocks/orders.yamlURL Patterns Pro
Match endpoints by glob pattern instead of exact URL. Useful when your code constructs URLs with dynamic IDs or path parameters.
Wildcard syntax
| Flag | Default | Description |
|---|---|---|
* | - | Matches any single path segment - no slashes |
** | - | Matches any path, including multiple segments and slashes |
# * matches one segment: /api/v1/users/123/profile, /api/v1/users/abc/profile
- method: GET
url: https://api.example.com/v1/users/*/profile
status: 200
body: '{"name": "Alice"}'
# ** matches any path: /api/v1/orders/123/items/456
- method: DELETE
url: https://api.example.com/v1/orders/**
status: 204Priority
Exact URLs always win over patterns. Among patterns, longer (more specific) ones win. If two patterns are equally specific, the first entry in the file wins.
# Priority: low → high (top to bottom in this list)
- method: GET
url: https://api.example.com/v1/users/** # broadest - lowest priority
body: '{"fallback": true}'
- method: GET
url: https://api.example.com/v1/users/*/settings # wins for .../users/123/settings
body: '{"theme": "dark"}'
- method: GET
url: https://api.example.com/v1/users/me # exact URL - highest priority
body: '{"id": "me", "admin": true}'Commands
nimbus test
Run Apex test classes locally. This is the core command you'll use every day.
Pattern matching
nimbus test # Run all tests
nimbus test MyTestClass # All methods in a class
nimbus test MyTestClass.testMyMethod # Single test method
nimbus test Calculator* # Classes starting with "Calculator"
nimbus test *Test # Classes ending with "Test"
nimbus test *Calculator* # Classes containing "Calculator"Path patterns
nimbus test force-app/main/default/classes/MyTest.cls # Specific file
nimbus test force-app/main/default/classes/ # All in directoryRunning nimbus test with no pattern from inside a package subdirectory (e.g. after cd’ing intoforce-app/main/default/classes/billing) auto-scopes to that subdirectory instead of the whole project.
Parallel execution Pro
nimbus test -p 8 # 8 parallel workers (Pro)
nimbus test -p 16 # Push it harder on fast machines
nimbus test # Sequential (Free tier default)Coverage
nimbus test --coverage # Print summary to console
nimbus test --coverage --coverage-output json # JSON to console (for scripting)
nimbus test --coverage --coverage-report out.json # Save JSON to file
nimbus test --coverage --coverage-report out.html # Save HTML report
nimbus test --coverage --coverage-report out.xml # Save Cobertura XML (Team)Org fallback Pro
nimbus test -f -o my-org # Fall back to SF CLI when Nimbus can't handle a test
nimbus test --fetch-missing -o my-org # Fetch missing metadata and re-runWhen Nimbus surfaces missing metadata (Apex classes, custom fields, custom labels, custom objects, custom metadata types), it also writes a standard SFDX manifest at manifest/nimbus-missing.xml — regardless of whether --fetch-missing is set. You can hand it to sf directly: sf project retrieve start --manifest manifest/nimbus-missing.xml -o <alias>. The filename is distinct from package.xml so it won't clobber a user-managed deployment manifest.
Tracing / debugging
nimbus test --trace # Enable execution tracing
nimbus test --trace --trace-level debug # Verbose trace
nimbus test --trace --trace-output ./traces # Save traces to custom dirOrg defaults (custom settings)
Seed custom setting org-default records before tests run. Can be specified via CLI flag or committed to nimbus.properties for repeatability.
# CLI flag (one field per flag, or comma-separated fields)
nimbus test --org-default "TriggerSettings__c.IsEnabled__c=true"
nimbus test --org-default "MySettings__c.Field1__c=foo,Field2__c=bar"
# nimbus.properties (committable, profile-aware)
nimbus.seed.org-default.TriggerSettings__c=IsEnabled__c=true
nimbus.seed.org-default.MySettings__c=Field1__c=foo,Field2__c=barExclusions
nimbus test -e __tests__ # Exclude paths containing "__tests__"
nimbus test -e legacy -e wip # Multiple exclusions| Flag | Default | Description |
|---|---|---|
-p, --parallel | 1 | Number of parallel test workers (Pro) |
--shard | - | Run a subset of tests: --shard N/M. Use with CI matrix to parallelize across jobs. |
--local-shards | - | Run N shards concurrently on this machine, each with its own embedded Postgres: a number (e.g. 4) or "auto" (Pro) |
--impacted | false | Run only tests affected by source changes since the impact map was last built. Selects tests whose coverage touched a changed class, plus changed and never-before-seen test classes. Falls back to a full run when no map exists or a schema file changed (sfdx-project.json, objects/). The map is rebuilt after each run. |
-n, --dry-run | false | List matching tests without executing them |
--timeout | 60 | Per-test timeout in seconds (catches infinite loops) |
--sandbox | false | Make Organization.IsSandbox return true during tests |
-P, --assign-perms | - | Assign PermissionSet(s) to the mock running user by API name (repeatable, comma-separated) |
--show-permission-seams | false | Expand the permission-seam summary into a per-test list. Custom-permission seams ($Permission, FeatureManagement.checkPermission) are flagged distinctly from FLS / object-perm seams. |
--strict-permissions | false | Fail tests whose passing path consulted a permission check outside System.runAs() (CI gate) |
--feature | - | Enable org features: MultiCurrency, PersonAccounts (repeatable, comma-separated) |
--faketime | - | Pin Date.today() / DateTime.now() to this instant (e.g. 2030-01-15T12:00:00Z) |
--coverage | false | Enable code coverage collection |
--coverage-output | console | Coverage output format: console, json |
--coverage-report | - | Path to save coverage report - format auto-detected from extension: .json, .html, .xml Cobertura (Team). Implies --coverage. |
--results-xml | - | Path to save JUnit XML test results (Team) |
-f, --fallback | false | Fall back to SF CLI for unsupported features (Pro) |
--fetch-missing | false | Retrieve missing metadata and re-run |
--org-default | - | Seed a custom setting org default before tests: ObjectName__c.Field=value (repeatable) |
-e, --exclude | - | Exclude paths containing pattern (repeatable) |
-i, --interactive | false | Interactively select package scope |
-q, --quiet | false | Minimal output — show only the summary and failures |
--json | false | Output results as JSON (for CI/agent consumption) |
--trace | false | Enable execution tracing |
--trace-level | normal | Trace verbosity: minimal, normal, verbose, debug, system |
--trace-output | .nimbus/traces | Directory for trace output files |
Parallel sharding
Split your test suite across multiple CI jobs with --shard N/M. Job N runs only its assigned slice of test classes. All N jobs run concurrently in a CI matrix, cutting wall-clock time proportionally.
Classes are distributed by recorded test duration (not just count), so shards finish in roughly equal wall time even when a few classes dominate the runtime. Duration data is recorded automatically by full test runs and persisted in your project; a project with no history yet falls back to an even split by class name. All methods within a class are always kept together so @testSetup runs correctly.
When combined with --results-xml, each shard's JUnit XML output includes a shard="N/M" attribute on the root <testsuites> element, allowing CI aggregators to merge results across shards.
# GitHub Actions matrix example
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: nimbus test --shard ${{ matrix.shard }}/4 --format junitTuning parallel execution
The default of 8 workers is a safe starting point for most machines and projects. You can push higher, but there's a ceiling — and it's not CPU or memory.
Why conflicts happen
Each parallel test worker runs inside its own database transaction. When two workers insert, update, or delete rows in the same table at the same time, PostgreSQL acquires row-level locks. If worker A holds a lock that worker B needs (and vice versa), you get a deadlock or a lock timeout.
Nimbus handles this automatically: transient database errors (deadlocks, lock timeouts, constraint violations) are retried up to 3 times with exponential backoff. If retries are exhausted, the test is re-run sequentially after the parallel phase completes. Your test still passes — it just takes longer. To disable this sequential re-run fallback, set nimbus.test.rerun-failed=false in nimbus.properties.
The problem is probability. With 4 workers, lock conflicts are rare. With 8, they're occasional and retries handle them. With 16+, conflicts become frequent enough that retries are exhausted, sequential re-runs pile up, and total wall-clock time can actually increase.
Finding your sweet spot
The optimal worker count depends on your codebase, not your hardware. Projects where tests touch many different objects can run more workers. Projects where most tests insert into the same few objects (Account, Contact, Lead) hit contention earlier.
# Start with 8 workers
nimbus test -p 8
# If you see 0 sequential re-runs, try pushing higher
nimbus test -p 12
# If you see many sequential re-runs, back off
nimbus test -p 4
# CI: use nimbus.properties so the settings are committed
%ci.nimbus.test.parallel=8
# Disable sequential re-run fallback if you want hard failures instead
%ci.nimbus.test.rerun-failed=falseWatch the test summary for "re-run sequentially" messages. If you see more than a handful, your worker count is too high for the contention profile of your project. Back off until re-runs drop to near zero.
Scaling beyond the single-machine limit
If you've maxed out parallel workers on one machine and still want faster runs, use --shard to distribute across multiple CI jobs. Sharding splits tests across entirely separate processes — each with their own database — so there's zero lock contention between shards. This scales linearly.
Local shards Pro
--local-shards N runs the same sharding model on one machine: N concurrent nimbus test processes, each with its own embedded Postgres instance, splitting the suite the same way --shard does. Pass a number, or auto to let Nimbus decide: it picks a shard count based on available cores, but won't shard at all if the suite's recorded run time (from the last full run, tracked in .nimbus/test-durations.json) is too short to be worth the per-shard process overhead — or if there's no recorded run yet. Tune the cutoff with nimbus.test.local-shards-auto-threshold-ms (default 8000).
nimbus test --local-shards 4 # 4 concurrent shards, 4 separate databases
nimbus test --local-shards auto # Let Nimbus choose (by cores and suite length)Each shard gets a persistent background daemon (visible in nimbus daemon list) that stays warm across runs, so repeat invocations skip database startup. Results, JUnit XML, and JSON output are merged into a single report as if the suite had run in one process. --local-shards is mutually exclusive with --shard, requires the embedded database provider, and does not currently support --coverage, --dry-run, --interactive, --write-stubs, --fetch-missing, or --impacted.
Each shard is a separate OS process, so it pays its own startup cost — Go runtime init, daemon handshake, config parsing — once per invocation. On short suites that fixed cost can outweigh the benefit of a second Postgres instance; on longer-running suites it's a smaller fraction of total wall time, so the separate-database win has more room to show through. If you're not seeing a win, try a lower shard count before a higher one — splitting workers across more processes also means fewer workers per shard, which can offset the extra database parallelism.
Per-worker isolation (opt-in)
Set nimbus.test.isolation=per-worker-schema to give each worker its own cloned Postgres schema. Row-level locks and writes stay inside the worker, so -p can scale to your CPU count without cross-worker contention. Default is shared to keep startup cost low — see Parallel Isolation for details.
nimbus test:watch Pro
Continuous testing mode. Watches your Apex files and automatically re-runs tests whenever you save.
nimbus test:watch # Watch all tests
nimbus test:watch -i # Select package scope interactivelyWhile watching:
r # Manually trigger a re-run
c # Clear the output
q # Quit (or Ctrl+C)Watch mode uses test impact selection automatically once an impact map exists: the first run runs everything and builds the map, and each later run narrows to the tests affected by the files you changed (e.g. 3 of 412 tests impacted by AccountService.cls), falling back to a full run when a schema file changes. Pass --no-impact to run the full pattern on every change.
| Flag | Default | Description |
|---|---|---|
-i, --interactive | false | Select package scope interactively before watching |
--no-impact | false | Disable automatic test impact selection; run the full pattern on every change |
nimbus validate
Check Apex files for syntax errors and apiVersion mismatches without running tests. Fast — no database or runtime needed. Errors come from the parser; warnings come from the apiVersion check, which reads each file’s *-meta.xml for the declared apiVersion and flags any referenced symbol whose introduction version is newer than that. Catches mistakes like a v52 class callingDatabase.getCursor (v66) before the deploy does.
Beyond warnings, validate flags a handful of deploy blockers — things Salesforce rejects at compile/deploy — and exits non-zero when any are found: REST resource methods with an unsupported return type; setting an AutoNumber field in an SObject constructor (new Log__c(Name = 'x') — AutoNumber fields aren’t writeable); and a concrete class that declaresimplements but doesn’t define every method of the interface. The field and interface checks resolve schema and interfaces from the local project, and skip conservatively when a type can’t be resolved (e.g. a managed-package or cross-file interface) so they never block a deploy they can’t be sure of.
nimbus validate # Validate all Apex files
nimbus validate MyClass # Files matching "MyClass"
nimbus validate *Service* # Files matching "*Service*"
nimbus validate -e __tests__ # Exclude paths containing "__tests__"
nimbus validate --json # Machine-readable JSON output (for editor integration)Path patterns
nimbus validate force-app/main/default/classes/MyClass.cls # Specific file
nimbus validate force-app/main/default/classes/ # All files in directory
nimbus validate src/tests/ # All files under path| Flag | Default | Description |
|---|---|---|
-e, --exclude | - | Exclude paths containing pattern (repeatable) |
-i, --interactive | false | Select package scope interactively |
--json | false | Output results as JSON, including apiVersion warnings under a per-file warnings[] array. |
Suppress an individual apiVersion warning with // nimbus:ignore on the same line as the call (or the line directly above). Add a comma-separated rule list to scope to specific rules.
Run from inside a package subdirectory (e.g. after cd’ing intoforce-app/main/default/classes/billing) to auto-scope to that subdirectory instead of the whole project — no pattern argument needed. Same behavior applies to nimbus test.
For CI gates, the standalone apex-version-lint CLI runs the same check against an SFDX project tree and exits non-zero with -strict:
apex-version-lint # warn-only, exit 0
apex-version-lint -strict # exit 1 if any diagnostic was emitted
apex-version-lint path/to/src # walk a specific subtreenimbus exec
Execute anonymous Apex - like Salesforce's "Execute Anonymous", but locally.
# From a file
nimbus exec script.apex
# Inline expression (auto-wrapped in System.debug)
nimbus exec -c "Calculator.add(1, 2)"
nimbus exec -c "new Account(Name = 'Test').Name"
# Multi-statement inline
nimbus exec -c "Integer x = 5; Integer y = 10; System.debug(x + y);"| Flag | Default | Description |
|---|---|---|
-c, --code | - | Inline Apex code to execute |
nimbus serve Pro
Start a local Salesforce-compatible API server. See the Local Server section for full documentation.
nimbus serve| Flag | Default | Description |
|---|---|---|
--addr | 127.0.0.1:8080 | Address to listen on |
--api-version | 60.0 | Salesforce API version to expose |
--username | admin@nimbus.local | Email for the default admin user |
--password | password | Password for OAuth flow |
--grpc-addr | 127.0.0.1:7443 | gRPC address for Pub/Sub API |
nimbus app Pro
Run a Salesforce Multi-Framework UI bundle (React + Vite app) against the local Nimbus runtime. 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 React app hits Nimbus's Apex interpreter and embedded Postgres instead of a real org.
What it does:
- Discovers UI bundles under
force-app/**/uiBundles/<name>/(marker:*.uibundle-meta.xml) - Spawns the bundle's dev server (default:
npm run dev) — Vite, HMR, source maps, the lot - Starts a proxy on
127.0.0.1:4545(same default assf ui-bundle dev) that injectswindow.SFDC_ENV(basePath,accessToken,instanceUrl,apiVersion,userId) into the served HTML - 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)
- Opens the browser
# Auto-discover the single bundle in this project and run it
nimbus app
# Pick a specific bundle by name
nimbus app reactRecipes
# Don't spawn vite — connect to one you started yourself
nimbus app reactRecipes --no-spawn
# List every bundle nimbus can see
nimbus app list
# 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 reactRecipesAPI surface served (full coverage of what Salesforce Multi-Framework production apps reach for):
- OAuth —
POST /services/oauth2/token,GET /services/oauth2/userinfo(OIDC identity) - SObject REST —
/services/data/v{ver}/sobjects/<Object>full CRUD;POST /sobjects/ContentVersionhandlesmultipart/form-datafile uploads - SOQL —
GET /query?q=... - SOSL (Search) —
GET /search?q=FIND ... RETURNING ...,/parameterizedSearch— translated to per-object SOQL with LIKE clauses across text fields - GraphQL UIAPI —
POST /graphql. Queries:first/aftercursor pagination, multi-fieldorderBywithNULLS FIRST/LAST, fullwhere(eq/ne/lt/lte/gt/gte/like/nlike/in/nin+and/or/notcombinators that nest),byIdshortcut, 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 withpageInfo(hasNextPage,hasPreviousPage,startCursor,endCursor). Directives:@skip,@include,@optional. Variables: resolved from request payload. Mutations:<Object>Create,<Object>Update,<Object>Deleteper SObject. Introspection:__schemaserved 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 API —
POST /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 Actions —
POST /actions/custom/apex/<ClassName>dispatches to the class's@InvocableMethodvia the interpreter - Apex REST —
/services/apexrest/<path>for your@RestResourceclasses - 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 —
/limitsreturns 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, multi-bundle simultaneous serve, 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.
Bundle selection:
- One bundle in the project → used automatically
- Multiple bundles, name passed as argument → that bundle is used
- Multiple bundles, no argument, terminal → interactive numbered picker
- Multiple bundles, no argument, non-TTY (CI) → errors with the bundle list
--all→ every 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--primaryto pick which one opens in the browser by default.
| Flag | Default | Description |
|---|---|---|
--addr | 127.0.0.1:4545 | Address the proxy listens on (matches sf ui-bundle dev) |
--api-version | 60.0 | Salesforce API version exposed via SFDC_ENV |
--no-spawn | false | Don’t run npm run dev — assume vite is already running at the bundle’s dev.url |
--no-open | false | Don’t open the browser on startup |
--all | false | Serve every discovered bundle simultaneously (path-based routing under /lwr/application/<name>/) |
--primary | | In --all mode, which bundle to open in the browser by default (first discovered if unset) |
--install | false | Run 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:
| Flag | Default | Description |
|---|---|---|
list | | Print every bundle nimbus can see in this project |
schema | | Print a GraphQL SDL derived from your local SObject metadata — byte-identical to what the running server advertises via introspection |
build | | Run the bundle’s production build (npm run build). Useful for CI: build once, deploy the artifact separately |
test | | Run the bundle’s test suite (npm test). Wraps Vitest or whatever test runner the bundle uses |
preview | | Serve the bundle’s production build locally (npm run preview). Same API proxy on top of the optimised build |
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.
Tools exposed:
run_apex_tests— run tests by pattern ("*", class name,Class.method, or path). Returns pass/fail counts, failure objects with file/line, and total duration. Optional arguments:timeout_seconds(default 300, 0 disables) caps wall time and surfaces a structured error on overrun;max_failures(default 50, 0 disables) caps the failure list returned to the agent —total_failuresalways reflects the true count andfailures_truncatedsignals when the cap dropped entries. Each failure now carries adiagnosticsobject built for one-shot fixing:exception_type,message, the exactassert_file/assert_line, structuredexpected/actualfor assertion failures (is_assertion), andrecent_operations— the tail of SOQL queries and DML statements the test ran before it broke.get_coverage— per-class line coverage from the most recent run.list_test_classes— every@isTestclass discovered in the project, returned as{name, file}pairs so the agent can jump straight to the source file. Does not run tests.get_governor_usage— per-test governor-limit usage (SOQL queries, DML statements, CPU, heap, …) from the most recent run, plus the peak per limit. Optionalclassfilter. Lets an agent spot a limit that scales with record count — the signature of un-bulkified code — without re-running.execute_anonymous— run a block of anonymous Apex (likenimbus exec) in a fresh, rolled-back transaction. Argument:apex. Returnssuccess, captureddebugoutput, and the exceptionerroron failure.query— run a read-only SOQLSELECTagainst the local database and get the records back as JSON plus arow_count. Argument:soql. DML and non-SELECTstatements are rejected — useexecute_anonymousfor writes.describe_schema— describe an SObject: its fields (name,type,required,unique,reference_to,relationship_name, picklist values) and child relationships. Argument:sobject(optional — omit to list every SObject the project has schema for). Reads loaded metadata; runs nothing.run_mutation_tests(Pro) — run mutation testing and return the mutation score plus surviving mutants (the changes your tests failed to catch). Arguments:pattern,class(mutate one class — recommended),timeout_seconds(per mutant, default 30),survivors_only(default true, returns just the actionable set).
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.
Register with Claude Code:
claude mcp add nimbus -- nimbus mcpStdio 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.
| Flag | Default | Description |
|---|---|---|
--parallel | 0 | Worker count for run_apex_tests (0 = NumCPU). Set at server start; cannot be changed per call. |
--coverage | true | Collect coverage so get_coverage works after every run. Set false in tight agent loops that never call get_coverage. |
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:
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 agentAgent 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.ymlorCONVENTIONS.mdpresent → Aider, writes a singleCONVENTIONS.mdbundleAGENTS.mdoropencode.jsonpresent → generic AGENTS.md (also covers OpenCode), writes a singleAGENTS.mdbundle.kiro/present → Kiro, writes to.kiro/steering/<name>.mdwith manual-inclusion frontmatter (load via#<name>in your Kiro prompt)- Nothing matched → defaults to Claude Code (use
--globalto 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.
| Flag | Default | Description |
|---|---|---|
--agent | auto | Target agent: claude-code, cursor, aider, agents-md, kiro |
--global | false | Claude Code only: install into ~/.claude/skills/ instead of <project>/.claude/skills/ |
--force | false | Overwrite an existing local copy when its content differs |
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.
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 (the only mode that applies column type changes — it also wipes table data). Use -s to scope work to specific objects while iterating.
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| Flag | Default | Description |
|---|---|---|
-s, --sobjects | - | Comma-separated list of objects to sync (others are left untouched) |
--rebuild | false | Drop and recreate tables instead of incrementally reconciling. Applies column type changes; 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.
# Run from your project root (where sfdx-project.json is)
nimbus initAfter init, run nimbus sync -o <org> to populate the schema, then nimbus test.
nimbus new
Scaffold a new Apex class or trigger (and its -meta.xml) into your project's default package — the SFDX main/default/classes (or /triggers) convention. The API version comes from sourceApiVersion in sfdx-project.json.
nimbus new test AccountServiceTest # @IsTest class with a starter method
nimbus new queueable SyncContactsJob # Queueable implementation
nimbus new batch NightlyRollup # Database.Batchable<SObject>
nimbus new trigger AccountTrigger --sobject AccountKinds: class, test, trigger, triggerhandler, batch, queueable, schedulable.
| Flag | Default | Description |
|---|---|---|
--sobject | | SObject for a trigger (required for the trigger kind). |
--dir | | Target directory (default: the package's main/default/classes or /triggers). |
--api-version | sourceApiVersion | API version written to the -meta.xml (falls back to 62.0). |
--force | false | Overwrite an existing file. |
nimbus fixture
Generate a TestDataFactory with a create<SObject>() method per object, with required fields pre-filled from the local schema. Standard objects work with zero sync (Nimbus ships their describes); custom objects need a sync. Required lookup fields become TODOs since they need a parent record's Id. Prints to stdout by default.
nimbus fixture Account Contact # print a TestDataFactory to stdout
nimbus fixture My_Object__c --write # save it to the default package| Flag | Default | Description |
|---|---|---|
--class | TestDataFactory | Generated class name. |
--write | false | Write the class to the default package instead of stdout. |
--api-version | 62.0 | API version for the -meta.xml when --write. |
nimbus coverage diff
Compare two JSON coverage reports (from nimbus test --coverage-report cov.json) and report the overall coverage delta, per-file regressions and improvements, and the exact lines that went from covered to uncovered. Built for PR coverage gates.
# baseline on main, then the branch:
nimbus test "*" --coverage-report base.json
nimbus test "*" --coverage-report pr.json
nimbus coverage diff base.json pr.json --fail-on-drop| Flag | Default | Description |
|---|---|---|
--fail-on-drop | false | Exit non-zero when overall coverage decreased (CI gate). |
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.
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 projectsFlags
| Flag | Default | Description |
|---|---|---|
--clean | false | Drop and recreate the database before starting |
--foreground | false | Run in foreground instead of detaching (useful for debugging) |
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.
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 tierLicenses are validated online and cached locally; a 7-day offline grace period is built in for outages and travel.
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 dev
Launch the interactive Dev UI in your default browser. Provides a visual interface for running tests, viewing coverage, exploring schema, and inspecting traces.
nimbus dev # Launch Dev UI on default port
nimbus dev --port 9000 # Launch on custom portnimbus status
Show the current Nimbus status - database state, project info, daemon status, and license info at a glance.
nimbus statusnimbus 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.
nimbus doctor # Run all checks
nimbus doctor --verbose # Show fix details for all checks, not just failures
nimbus doctor --json # Output results as JSONChecks
| Check name | What it validates | Severity |
|---|---|---|
nimbus.properties found | Config file exists in the project root | Fail |
Config syntax | All keys in nimbus.properties are recognized | Warn |
Apex parse errors | Every .cls and .trigger file parses without syntax errors | Fail |
Schema files found | SObject schemas synced into .nimbus/schemas/ | Warn |
Schema coverage | Every custom object defined in source has a synced schema | Warn |
Stubs directory exists | stubs/ present for managed package stubs | Warn |
Stub gaps | All managed-package namespace references in source have stub coverage | Warn |
Field metadata validity | Custom field XML passes Salesforce deploy-time rules | Warn |
Apex test classes found | @isTest classes exist under project source directories | Fail |
Database connectivity | Embedded PostgreSQL starts and accepts connections | Fail |
License status | Current tier (Free / Pro / Team) and expiry — key never printed | Warn |
| Flag | Default | Description |
|---|---|---|
--verbose | false | Show fix details for all checks, not just warnings/failures |
--json | false | Output results as JSON |
Exit codes
| Code | Meaning |
|---|---|
0 | All checks passed or warned - no hard failures |
1 | One or more checks failed |
CI usage
Gate your pipeline on a clean setup before running tests:
nimbus doctor && nimbus testnimbus orgs
List Salesforce orgs authenticated via SF CLI that Nimbus can use for sync and fallback operations.
nimbus orgsnimbus 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.
nimbus schemanimbus cache
Manage the parsed AST cache. Nimbus caches parsed Apex classes to speed up subsequent test runs.
nimbus cache stats # Show cache hit rate, size, entry count
nimbus cache clear # Clear all cached ASTsnimbus config
Manage Nimbus configuration via the nimbus.properties file.
nimbus config show # Display current effective configuration
nimbus config init # Create an example nimbus.properties file
nimbus config properties # List all available configuration propertiesnimbus 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.
nimbus resetnimbus db
Manage the embedded PostgreSQL database directly.
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 stub
Scaffold and inspect stubs/ — the directory Nimbus loads before the main source tree so tests can reference managed-package classes that aren't in the project. See also the User Stubs / Managed Packages section.
List existing stubs
nimbus stub list # Show every .cls under stubs/
nimbus stub path # Print the absolute stubs/ directoryScaffold a new stub class
# 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 --forceEach 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.
# 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.
Remove a stub
nimbus stub remove LoggerAuto-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.
# 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:
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 stubsLayout. 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.
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.
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.
nimbus upgrade # Download and install the latest version
nimbus upgrade --force # Re-install even if already on the latest versionNimbus 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.
nimbus lsp
Start the Nimbus Language Server on stdio. Any editor that speaks LSP — VSCode, JetBrains IDEs, Neovim, Zed, Helix, Emacs — can launch this command to get Apex completion, hover, go-to-definition, inline coverage hints, mutation-score annotations, and live SOQL-column validation, all backed by the same parser, schema cache, and test data that powers nimbus test.
See the Language Server section for per-editor setup and a full capability list. This page covers only the CLI entry point.
nimbus lsp # Start on stdio (what editors launch)
nimbus lsp --log /tmp/nimbus-lsp.log # Write a protocol trace to a file
nimbus lsp --stdio # Accepted for client compatibility (stdio is the default)| Flag | Default | Description |
|---|---|---|
--log | - | Write LSP protocol trace to this file. stderr-safe — stdout is reserved for JSON-RPC. |
--stdio | false | Accepted for compatibility with clients that inject this flag. No-op; stdio is the only transport. |
The server reads JSON-RPC 2.0 messages with Content-Length framing from stdin and writes responses to stdout. Never invoke this directly in an interactive shell — there is no TUI, just protocol bytes.
nimbus dap
Start a Debug Adapter Protocol server on stdio. Any editor that speaks DAP — VSCode, Neovim DAP, IntelliJ DAP — can launch this command to step through Apex tests with real breakpoints, a call stack view, and expandable local-variable inspection.
nimbus dap # Editors launch this; don't run in a shellSupported DAP requests: initialize, launch, setBreakpoints, configurationDone, threads, stackTrace, scopes, variables, continue, next, stepIn, stepOut, stepBack, reverseContinue, disconnect, terminate. Emits initialized, stopped, thread, output, and terminated events.
Launch arguments (set in .vscode/launch.json or equivalent):
| Flag | Default | Description |
|---|---|---|
program | - | Test pattern, e.g. "CalculatorTest.addsPositive" or "*Test" (live mode) |
projectPath | CWD | Project root containing sfdx-project.json |
orgAlias | default | Salesforce org alias |
stopOnEntry | false | Pause at the first statement of the program |
mode | launch | Session kind: "launch" runs the test live; "replay" steps a recorded trace |
traceFile | - | Path to a trace.jsonl (or its run dir); implies replay mode. Omit to use the newest trace |
Trace replay (time-travel)
Replay mode steps through a recorded test run instead of executing anything. Record once with tracing on, then scrub through the captured execution — forwards and backwards — with the same breakpoints, call stack, and variable panels as a live session. Because nothing re-runs, a replay session is instant and perfectly deterministic.
First, record a trace at verbose (or higher) so statement-level steps are captured:
nimbus test "CalculatorTest.*" --trace --trace-level verboseThen point a DAP launch config at it:
{
"type": "nimbus",
"request": "launch",
"name": "Replay last trace",
"mode": "replay",
"traceFile": ".nimbus/traces/2026-07-03T02-11-09_da70fda6/trace.jsonl"
}In replay mode the debugger advertises supportsStepBack, so stepBack and reverseContinue walk the timeline in reverse; breakpoints stop the cursor at the next matchingfile:line. next, stepIn, and stepOut each advance one recorded step (the trace is line-granular). Variable values come straight from the trace, so fidelity depends on the level you recorded at — verbose captures locals; a trace recorded below verbose has no steppable events and replay returns a clear error asking you to re-record.
nimbus mutate Pro
Run mutation testing against your Apex code. Nimbus introduces small changes (mutations) to your production code - flipping operators, negating conditions, changing return values - then runs your tests against each mutant. If your tests catch the change, the mutant is "killed." If all tests still pass, the mutant "survived," revealing a gap in your test suite.
This is a technique widely used in Java (PITest), JavaScript (Stryker), C# (Stryker.NET), and Python (mutmut) - but has never been possible for Apex because org-based test execution is too slow. Nimbus runs tests locally in milliseconds, making hundreds of mutations feasible in seconds.
The positional pattern selects which production classes to mutate. If it resolves only to test classes (e.g. AccountServiceTest), Nimbus mutates the class under test instead — derived by stripping conventional Test prefixes/suffixes — and runs that test class against each mutant. This makes nimbus mutate AccountServiceTest answer the natural question: "is this test suite actually any good?"
# Mutate all production classes, run all tests
nimbus mutate
# Mutate a specific class
nimbus mutate AccountService
# Pass a test class — Nimbus mutates the class under test and runs that test
nimbus mutate AccountServiceTest
# Mutate a class, only run specific tests per mutant
nimbus mutate --class AccountService --test AccountServiceTest
# Increase per-mutant timeout to 60 seconds
nimbus mutate --timeout 60
# Only print surviving mutants during the run (kills suppressed)
nimbus mutate --survivors-only
# Fail the run (exit code 1) if mutation score is below 80%
nimbus mutate --min-score 80Mutation Types
| Type | Example | What it tests |
|---|---|---|
| Arithmetic | + → -, * → / | Your tests verify calculation results, not just that code runs |
| Comparison | < → <=, > → >= | Boundary conditions are checked |
| Negate conditional | == → !=, < → >= | Conditional branches are actually tested |
| Logical | && → || | Compound conditions aren't over-simplified |
| Boolean literal | true → false | Boolean flags affect behavior |
| Increment/decrement | ++ → -- | Loop and counter logic is validated |
| Negate prefix | Remove ! | Negation logic is tested |
| Return value | return x → return null | Return values are used by callers |
| Integer literal | N → N+1 | Off-by-one and boundary constants are asserted, not just threaded through |
| String literal | "foo" → "" | String content is asserted, not just presence (SOQL fragments and sObject type names are skipped) |
| DML verb | insert → update, delete → undelete | The right persistence operation is being invoked, not just any DML |
| Call removal | service.doThing(); → no-op | Side-effecting calls are asserted (assertion helpers on System/Assert/Test are skipped to keep the score honest) |
| Loop boundary | i < n → i < n + 1 / i < n - 1 | Loops iterate the intended number of times — catches off-by-one against variable bounds, which integer ±1 and boundary swaps miss |
Flags
| Flag | Default | Description |
|---|---|---|
--class | - | Only mutate this class (default: all non-test classes) |
--test | * | Test pattern to run against each mutant |
--timeout | 30 | Per-mutant timeout in seconds |
--survivors-only | false | Only print surviving mutants during the run (kills, timeouts, and errors are suppressed). The final summary still lists totals and surviving mutants. |
--min-score | 0 | Exit with code 1 if the final mutation score is below this threshold (0-100). Useful for CI gating. |
Understanding the Score
The mutation score = killed mutants / total mutants × 100%. A project with 95% line coverage but 40% mutation score has tests that execute code without verifying behavior. Think of it as the difference between "I walked through every room" and "I checked every room for problems."
nimbus bench Pro
Run a test method N times and report timing statistics: mean, median, p95, p99, min, max. Useful for catching slow SOQL, comparing performance before and after refactors, and setting performance budgets in CI.
While the benchmark runs, a live progress view updates with rolling stats after each sample. Press q to stop early and show results from completed runs.
# Benchmark a test method (100 runs, warmup enabled by default)
nimbus bench AccountServiceTest.testInsertAccount
# 50 runs, no warmup
nimbus bench AccountServiceTest.testInsertAccount --runs 50 --warmup=false
# JSON output - pipe to jq for CI integration
nimbus bench AccountServiceTest.testQuery --format json
# CSV output - one row per sample
nimbus bench AccountServiceTest.testQuery --format csv > results.csvCI integration
JSON output pipes cleanly to jq. Fail your pipeline if p95 exceeds a threshold:
nimbus bench AccountServiceTest.testQuery --runs 50 --format json \
| jq 'if .p95_ms > 100 then error("p95 exceeded 100ms") else . end'Flags
| Flag | Default | Description |
|---|---|---|
-n, --runs | 100 | Number of times to run the test method |
--warmup | true | Discard the first 10% of runs to exclude cold-start overhead |
-f, --format | table | Output format: table, json, csv |
Bench history
After each run, results are appended to .nimbus/bench-history.jsonl - one JSON line per benchmark with timestamp, method, and all timing stats. Use this file to track performance trends over time in external tooling.
Configuration
Global Flags
These flags work with every command:
| Flag | Default | Description |
|---|---|---|
-o, --org | - | Target Salesforce org alias (passed to SF CLI for sync/fallback) |
-v, --verbose | false | Verbose 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.
| Flag | Default | Description |
|---|---|---|
--db-provider | embedded | Database provider: embedded, external |
--db-url | - | Full PostgreSQL connection string (overrides provider) |
--db-dir | .nimbus/db | Embedded Postgres data directory |
--db-name | - | Database name override |
--db-user | - | Database user override |
--db-password | - | Database password override |
External database
# Use a Neon, RDS, or any managed Postgres
nimbus test --db-url "postgresql://user:pass@host:5432/mydb"CI with external DB
# In CI: use a service container or Neon ephemeral branch
nimbus test --db-url "$NIMBUS_DB_URL" --coverage --coverage-output jsonPlatform 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.
| Flag | Default | Description |
|---|---|---|
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. |
# nimbus.properties
nimbus.compat.mode=strict
# Relaxed for day-to-day dev, strict in CI:
%dev.nimbus.compat.mode=warn
%ci.nimbus.compat.mode=strictGovernor 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.
| Flag | Default | Description |
|---|---|---|
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.
| Flag | Default | Description |
|---|---|---|
nimbus.governor.mode | strict | Enforcement mode: strict | warn | off |
nimbus.governor.soql-queries | 100 / 200 | Override SOQL queries per transaction. Unset: 100 sync, 200 async. |
nimbus.governor.dml-statements | 150 | Override DML statements per transaction |
nimbus.governor.heap-size | 12000000 | Override heap size in bytes |
# nimbus.properties — match an org with Salesforce-raised limits
nimbus.governor.mode=strict
nimbus.governor.soql-queries=300
nimbus.governor.dml-statements=300Parallel 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.
| Flag | Default | Description |
|---|---|---|
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. |
# 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-schemaOrg 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.
| Flag | Default | Description |
|---|---|---|
nimbus.org.currency | USD | Return value for UserInfo.getDefaultCurrency(). |
nimbus.org.locale | en_US | Return 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 | America/Los_Angeles | Organization.TimeZoneSidKey and UserInfo.getTimeZone() display name. |
nimbus.org.sandbox | false | Return value for Organization.IsSandbox. Toggle when your code branches on sandbox vs production. |
nimbus.org.features | (none) | Comma-separated feature toggles. MultiCurrency enables UserInfo.isMultiCurrencyOrganization()=true; 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. |
# 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:00ZEquivalent CLI flags on nimbus test override the config file when both are set: --sandbox, --feature MultiCurrency,PersonAccounts, --faketime 2030-01-15T12:00:00Z.
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.
# 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=trueTests 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.
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.
# nimbus.properties
nimbus.test.readonly=true
# Or per-profile:
%ci.nimbus.test.readonly=true
# Or as a one-off CLI flag:
# nimbus test --readonlySetup-time DML (seeding profiles, list views, custom setting records) still succeeds — only DML inside an @IsTest method is rejected.
CI/CD Integration
Nimbus runs without a Salesforce org, which means your CI pipeline has no JWT certificates, no DevHub dependency, and no scratch org pool to exhaust. Install the binary, run tests, collect output. See the CI/CD guide for full GitHub Actions, GitLab CI, SonarQube, and Codecov examples.
CI requires Pro Pro
Running nimbus test in a CI environment requires a Pro license. The Free tier is for local development only. Nimbus auto-detects GitHub Actions, GitLab CI, CircleCI, Travis, Jenkins, Buildkite, Azure Pipelines, Bitbucket, Drone, TeamCity, CodeBuild, AppVeyor, Semaphore, and any environment that sets CI=true.
Set NIMBUS_LICENSE_KEY as a secret in your CI platform. AI coding agents (Claude Code, Cursor, Aider, Codex, Continue) are detected and allowed to run Nimbus locally on the Free tier even if CI env vars leak through.
Pull requests from forks: GitHub does not expose repository secrets to pull_request runs that originate from a fork, so NIMBUS_LICENSE_KEY is empty there and the run fails with Set NIMBUS_LICENSE_KEY. Add a preflight step (shown below) so this surfaces clearly, and run tests on push or restrict the job to same-repo PRs.
GitHub Actions
Minimal working example. Set NIMBUS_PROFILE=ci to activate CI-specific settings from your nimbus.properties file.
name: Apex Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Check license secret
# Fails clearly if the secret is missing or withheld (fork PR),
# instead of a confusing "Set NIMBUS_LICENSE_KEY" later.
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
run: |
if [ -z "$NIMBUS_LICENSE_KEY" ]; then
echo "::error::NIMBUS_LICENSE_KEY is empty — secret not set or withheld (fork PR)."
exit 1
fi
- name: Run tests
run: |
nimbus test \
--coverage --coverage-report coverage.xml \
--results-xml results.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ci
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: results.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}For GitLab CI, SonarQube integration, external database setup, and caching, see the full CI/CD guide.
Coverage Reports
Nimbus auto-detects the coverage format from the file extension passed to --coverage-report.
Console summary (Free)
nimbus test --coverageJSON - custom tooling, dashboards (Free)
nimbus test --coverage --coverage-report coverage.jsonHTML - visual report, browsable in your browser (Pro)
nimbus test --coverage --coverage-report coverage.html
open coverage.htmlXML Outputs Pro
XML formats integrate with SonarQube, Codecov, GitHub quality gates, and CI dashboards. Both flags can be combined in the same run.
Cobertura XML - coverage for SonarQube / Codecov / GitHub Actions
nimbus test --coverage --coverage-report coverage.xmlJUnit XML - test results for CI dashboards and PR annotations
nimbus test --results-xml results.xmlBoth together
nimbus test \
--coverage --coverage-report coverage.xml \
--results-xml results.xmlReferences
Salesforce Profiles
Profiles are a legacy Salesforce concept - they define permissions per object and field and are assigned to every user. Unlike most metadata, they are not deployable as discrete records and they live in a separate namespace from permission sets.
Nimbus models Profile as a standard SOQL-queryable object backed by a local database table. A set of standard profiles is always seeded so common test patterns work out of the box:
Profiles in Nimbus are lookup-only. Nimbus does not enforce profile-based object or field permissions (FLS/CRUD). Tests run with full data access regardless of which profile the running user is assigned to. Field-level writeability comes from synced schema metadata (nimbus sync), not from profile assignments.
| Flag | Default | Description |
|---|---|---|
System Administrator | 00e000000000002AAA | Standard user type, Salesforce license |
Standard User | 00e000000000003AAA | Standard user type, Salesforce license |
Read Only | 00e000000000005AAA | Standard user type, Salesforce license |
Chatter External User | 00e000000000004AAA | CsnOnly user type, Chatter External license |
Minimum Access - Salesforce | 00e000000000006AAA | Standard user type, Salesforce license |
Running user profile
Every test runs as a mock user (ID 005000000000000AAA). The profile assigned to that user - what UserInfo.getProfileId() resolves to - defaults to Standard User. Override it with nimbus.mock.user-profile.
This matters when your code branches on the running user's profile name, e.g.:
// Example: trigger framework checking profile to decide whether to fire
String profileName = profileMapById.get(UserInfo.getProfileId()).Name;
if (profileName == 'My Profile') { return; }# nimbus.properties
nimbus.mock.user-profile=My ProfileProject-specific profiles
If your tests query for profiles not in the built-in set, seed them with nimbus.seed.profiles. Comma-separated list of profile names. Seeded profiles get a deterministic ID and are assigned Standard user type with the Salesforce license - sufficient for the vast majority of test patterns.
# nimbus.properties
nimbus.seed.profiles=Field Sales,Partner CommunityThese profiles are then findable via SOQL:
Profile p = [SELECT Id FROM Profile WHERE Name = 'Field Sales' LIMIT 1];
User u = new User(profileId = p.Id, ...);Default record types
In Salesforce, records inserted without an explicit RecordTypeId get the running user's profile default. Nimbus defaults to the Master record type. If your org uses custom record types and flows or triggers branch on RecordTypeId, configure per-object defaults:
# nimbus.properties
nimbus.default-record-type.Lead=Applicant_Record_Type
nimbus.default-record-type.Case=Support_CaseList views
Nimbus auto-discovers .listView-meta.xml files from your project and seeds them into the database. For standard object list views that aren't in your repo (Salesforce includes built-in views like "All Leads"), seed them via config:
# nimbus.properties — format: nimbus.seed.list-view.<Object>.<DeveloperName>=<Label>
nimbus.seed.list-view.Lead.AllLeads=All Leads
nimbus.seed.list-view.Lead.MyLeads=My Leads
nimbus.seed.list-view.Case.AllCases=All CasesStandard reference data
Some standard objects are org-managed reference data that always exists on a real org but isn't carried by nimbus sync(which brings schema, not rows). Nimbus seeds the common ones with their standard out-of-box values so test-setup idioms that read them work without per-repo stubs:
| Flag | Default | Description |
|---|---|---|
OpportunityStage | 10 stages | Standard sales process — Prospecting … Closed Won / Closed Lost, with IsClosed / IsWon / DefaultProbability / ForecastCategory |
TaskStatus | 6 statuses | Not Started, In Progress, Completed, … |
CaseStatus | 4 statuses | New, Working, Escalated, Closed |
BusinessHours | Default row | The always-present "Default" business hours record |
These match a default org. If your org customises the picklist (e.g. custom opportunity stages), the seeded labels may differ from your StageName values — seed the records you need explicitly with nimbus.seed.record (below) to override.
Tabs (Schema.describeTabs())
Schema.describeTabs() returns a single tab set whose tabs are read from your project's *.tab-meta.xml (CustomTab) metadata — source-driven, like the rest of the schema. An object tab resolves its getSobjectName() and label from the object; web/VF/Lightning tabs carry their own label. Code that resolves a tab or icon for an SObject finds it; a project with no tab metadata gets a present-but-empty set.
Named SObject records (list custom settings)
List Custom Settings are accessible in real Salesforce tests as org-level data. In Nimbus, the local database starts empty, so code that reads them via CustomSetting__c.getAll() gets nothing. Seed the records you need with nimbus.seed.record:
# nimbus.properties — format: nimbus.seed.record.<Object>.<Name>=<Field=value,...>
nimbus.seed.record.CountryCodeSetting__c.de=Land__c=Deutschland,Liefergebiete__c=Berlin
nimbus.seed.record.CountryCodeSetting__c.nl=Land__c=Niederlande,Liefergebiete__c=AmsterdamThe Name in the key becomes the record's Name field. Existing records (same name) are skipped, so tests that insert their own records during @testSetup are unaffected.
Sites
Code that queries Site or SiteDetail (e.g. SELECT Id FROM Site WHERE Name = '...') will find no rows unless you seed them via config:
# nimbus.properties — format: nimbus.seed.site.<Name>=<secureUrl>
nimbus.seed.site.My Portal=https://myportal.my.site.com
nimbus.seed.site.Partner Community=https://partners.my.site.comEach entry inserts a Site row (status Active) and a matching SiteDetail row with the given secure URL. IDs are generated deterministically from the name so they are stable across runs.
Public Groups
Standing Public Groups (regional or team groups like Sales - EMEA or Support Tier 1) are usually created once by an admin and never checked into repo metadata. Code that queries them by name — often user-trigger automation that adds users to groups on country/team change — comes up empty in Nimbus. Seed them via config:
# nimbus.properties — format: nimbus.seed.group.<Name>=<Type>
# Type defaults to "Regular" when omitted
nimbus.seed.group.Sales - EMEA=Regular
nimbus.seed.group.Support Tier 1=RegularEach entry inserts a Group row with a deterministic 00G-prefix Id from the name. The key is written to both Name and DeveloperName — test factories that look groups up either way (WHERE Name = 'X' or WHERE DeveloperName = 'X') resolve the same row. Idempotent across runs.
Queues
Queues are Group rows with Type='Queue' plus QueueSobject children mapping each queue to the SObjectTypes it routes. Case/lead/custom assignment automation looks them up by DeveloperName and joins through QueueSobject to find owner candidates. Seed both halves in one entry:
# nimbus.properties — format: nimbus.seed.queue.<DeveloperName>=<SObject1>,<SObject2>
nimbus.seed.queue.Lead_Triage_DE=Lead,Case
nimbus.seed.queue.High_Value=OpportunityEach entry inserts a Group row (Type=Queue) plus a QueueSobject row per listed SObjectType. Empty value still creates the queue Group row without any routing rows.
UserRoles
Role-hierarchy lookups and user-factory routines that assign UserRoleId need rows to exist; UserRole metadata is rarely checked into the repo. Seed by DeveloperName with the human label as the value:
# nimbus.properties — format: nimbus.seed.role.<DeveloperName>=<Name>
# Value defaults to the DeveloperName when omitted
nimbus.seed.role.EMEA_Manager=EMEA Manager
nimbus.seed.role.APAC_Sales=APAC SalesWhen at least one role is seeded the built-in CEO mock UserRole steps aside — WHERE DeveloperName = '...' and other clauses run against the real seeded rows. Tests that don't seed any role still receive the mock for backward compatibility.
Networks (Experience Cloud)
Code that gates on a community resolves it by name — [SELECT Id FROM Network WHERE Name = 'Member Hub']. The Network object is org-resident and rarely checked into repo metadata. Seed by Name; the value is the Status (defaults to Live):
# nimbus.properties — format: nimbus.seed.network.<Name>=<Status>
nimbus.seed.network.Member Hub=Live
nimbus.seed.network.Partner Portal=UnderConstructionNamed Users
Factories that look up integration or system users by name — [SELECT Id FROM User WHERE Name = 'System Salesforce'] — need those rows to exist. Seed by the full Name; the value is an optional Username (derived from the Name when omitted):
# nimbus.properties — format: nimbus.seed.user.<Name>=<Username>
nimbus.seed.user.System Salesforce=
nimbus.seed.user.Integration Bot=integ.bot@example.comThe seed Name is split on the first space into FirstName / LastName so the generated Name field reconstructs exactly.
Pulling setup data from the org
Instead of hand-editing seed entries for every Group/Queue/UserRole/Network the tests rely on, run sync with --include-setup-data to populate them from the connected org:
nimbus sync --include-setup-dataThe command queries the org for Group, QueueSobject,UserRole, and Network rows and writes the matching nimbus.seed.{group,role,queue,network}.* entries into nimbus.properties under a delimited block. Re-running refreshes the block; hand-authored seed lines outside the block are preserved. Named Users are not bulk-synced — a large org has thousands — so seed them explicitly with nimbus.seed.user.<Name>.
Each object type is pulled independently. If the org can't query one — an org without Experience Cloud has no queryable Network, for example — that type is skipped with a warning and the rest still land. The pull only fails outright when no setup object can be queried at all (usually a bad org alias or no connection).
Custom Label overrides
Some repos ship Custom Labels with placeholder values (PLACEHOLDER,TBD) that admins replace post-deploy with environment-specific IDs — profile Ids, webhook URLs, signing keys. Tests that branch onLabel.X resolve to the repo value in Nimbus and diverge from prod. Override them at runtime:
# nimbus.properties — format: nimbus.seed.label.<FullName>=<value>
nimbus.seed.label.StandardUserProfileId=00e000000000003AAA
nimbus.seed.label.WebhookEndpoint=https://test.example.com/hookThe override wins over any value parsed from .labels-meta.xml files and over language-specific translations. The labels XML parser also tolerates bare <labels> fragments that ship without the <?xml?> header or <CustomLabels> wrapper.
Excluding seed records from specific tests
By default every test receives the seeded records. Use nimbus.seed.record.exclude to opt specific test classes (or individual methods) out of seed injection. When a test matches, getAll() returns only test-inserted records — as if the org has no data for that custom setting type. Useful for selector tests that assert getAll() returns empty.
# nimbus.properties
# Comma-separated ClassName or ClassName.method patterns
nimbus.seed.record.exclude=CountryCodeSettingSelectorTest,OtherTest.specificMethodFull profile example
# nimbus.properties
# Running user's profile (UserInfo.getProfileId() target)
nimbus.mock.user-profile=My Profile
# Additional profiles your tests query against
nimbus.seed.profiles=Field Sales,Partner Community
# Default record types for inserts without explicit RecordTypeId
nimbus.default-record-type.Lead=Applicant_Record_Type
# Seed list views not in repo metadata
nimbus.seed.list-view.Lead.AllLeads=All Leads
# Seed Site records for SOQL queries on Site/SiteDetail
nimbus.seed.site.My Portal=https://myportal.my.site.com
# Seed list custom setting org data
nimbus.seed.record.CountryCodeSetting__c.de=Land__c=Deutschland,Liefergebiete__c=BerlinCustom Permissions
Custom permissions are boolean flags that code checks at runtime via FeatureManagement.checkPermission('My_Permission'). They are defined as .customPermission-meta.xml files and granted to users via permission sets (or profiles).
Nimbus loads all .customPermission-meta.xml files from the project automatically. It also parses the <customPermissions> blocks in every .permissionset-meta.xml file and wires up the underlying SetupEntityAccess records. All that remains is assigning the right permission sets to the running test user.
Assigning permission sets to the running user
Use nimbus.mock.permission-sets to specify which permission sets the running user holds. These must be API names matching your .permissionset-meta.xml filenames (without the extension).
# nimbus.properties
nimbus.mock.permission-sets=Inventory_Manager,Support_AgentWith this set, FeatureManagement.checkPermission() returns true for any custom permission granted by those sets:
// In production code:
if (FeatureManagement.checkPermission('Manage_Inventory')) {
// This branch now executes in tests when Inventory_Manager PS is assigned
}How it works end-to-end
Nimbus models the full Salesforce permission chain without hitting an org:
.customPermission-meta.xml→ CustomPermission record seeded in DB.permissionset-meta.xml→ PermissionSet record + SetupEntityAccess linking PS → CustomPermissionnimbus.mock.permission-sets→ PermissionSetAssignment linking running user → PermissionSetFeatureManagement.checkPermission(name)→ queries this chain and returnstrue
Full permissions example
# nimbus.properties
# Running user's profile
nimbus.mock.user-profile=My Profile
# Permission sets assigned to the running user
# API names match .permissionset-meta.xml filenames
nimbus.mock.permission-sets=Inventory_Manager,Support_Agent
# Additional profiles to seed for user creation in tests
nimbus.seed.profiles=Field Sales,Partner CommunitySupported Metadata
Nimbus reads metadata directly from your SFDX project directory. No org sync required for most types — just point at your repo and run. This page lists every metadata type Nimbus understands today and what it does with each one.
Code
| Flag | Default | Description |
|---|---|---|
.cls | Supported | Apex classes — parsed, compiled, and executed. Includes interfaces, enums, inner classes, and all annotations. |
.trigger | Supported | Apex triggers — registered and fired automatically on DML operations. |
Automation
| Flag | Default | Description |
|---|---|---|
.flow-meta.xml | Supported | Flows — record-triggered, autolaunched, subflows, scheduled, and platform event flows are parsed and executed. |
.validationRule-meta.xml | Supported | Validation rules — evaluated during DML operations. |
Process Builder | Planned | Process Builder (.process-meta.xml) is not yet supported. We recommend migrating to flows. |
Workflow Rules | Planned | Workflow rules (.workflow-meta.xml) are not yet supported. We recommend migrating to flows. |
Salesforce has been deprecating Process Builder and Workflow Rules in favor of Flow since 2022. We recommend converting them to flows using Salesforce's Migrate to Flow tool. Nimbus support for both is planned but not a priority.
Data & Schema
| Flag | Default | Description |
|---|---|---|
Standard objects | Supported | Common standard objects ship with embedded schemas (Account, Contact, Lead, Opportunity, Case, User, and more). No sync required. |
.object-meta.xml | Supported | Custom objects — fields, record types, and relationships are used to build the local database schema. |
.object-meta.xml (__mdt) | Supported | Custom metadata types — object definitions discovered automatically from __mdt suffixed objects. |
.object-meta.xml (__c settings) | Supported | Custom settings (hierarchy and list) — object definitions loaded, records seedable via nimbus.properties. |
.field-meta.xml | Supported | Custom fields — all standard field types including formula fields, lookups, and master-detail. |
.fieldSet-meta.xml | Supported | Field sets — parsed and available for FieldSet describe calls. |
.recordType-meta.xml | Supported | Record types — seeded into the database and available via SOQL. |
.listView-meta.xml | Supported | List views — auto-discovered and seeded for ListView SOQL queries. |
.globalValueSet-meta.xml | Supported | Global picklist value sets — loaded and referenced by picklist fields. |
.standardValueSet-meta.xml | Supported | Standard picklist value sets (e.g. LeadSource, Industry). |
.territory2-meta.xml | Supported | Territory2 records — seeded and queryable via SOQL. |
Security & Access
| Flag | Default | Description |
|---|---|---|
.permissionset-meta.xml | Supported | Permission sets — parsed for custom permission grants and assignable to the running user. |
.customPermission-meta.xml | Supported | Custom permissions — seeded and checkable via FeatureManagement.checkPermission(). |
.profile-meta.xml | Partial | Profiles — standard profiles are pre-seeded. Custom profiles can be seeded via config. FLS/CRUD not enforced. |
Configuration
| Flag | Default | Description |
|---|---|---|
.labels-meta.xml | Supported | Custom labels — loaded and accessible via System.Label. |
.translation-meta.xml | Supported | Translations — label translations loaded per language. |
.md-meta.xml | Supported | Custom metadata type records — seeded into __mdt tables and queryable via SOQL. |
.resource-meta.xml | Supported | Static resources — file content available for Test.loadStaticResource() and callout mocks. |
Language Server (LSP)
Overview
Nimbus ships an LSP 3.17 server for Apex. Any editor that speaks the Language Server Protocol — VSCode, JetBrains IDEs, Neovim, Zed, Helix, Emacs — gets the same experience: schema-aware completion, hover documentation, go-to-definition, live SOQL column validation, per-line coverage hints, mutation score annotations, and code lenses that let you run or debug any @IsTest method inline.
Unlike Salesforce's stock Apex Language Server, which makes network round-trips to an org for metadata, Nimbus's LSP runs fully offline against the local .nimbus/schemas/ cache and surfaces platform truth — coverage %, surviving mutants, test flakiness — from your last nimbus test run.
The server is a thin layer over the same parser, schema provider, and test-data infrastructure that powers the CLI. Reparsing a file takes a few milliseconds; diagnostics land before your hand leaves the keyboard.
# Start the server (editors launch this for you)
nimbus lsp
# Debug: write every JSON-RPC message to a log
nimbus lsp --log /tmp/nimbus-lsp.logFeatures
The server advertises these LSP capabilities at initialize. Exactly what shows up in your editor depends on the client — VSCode renders everything; leaner clients may render a subset.
Editing intelligence
| Capability | What you see |
|---|---|
| Diagnostics | Red squiggles on parse errors. Yellow squiggles on SOQL column typos ([SELECT Namee FROM Account]), including relationship fields (Owner.Namee) once the chain resolves to a synced object. Faded-text hints on dead code (methods with zero references and zero coverage). Source labels nimbus/parse, nimbus/soql, or nimbus/dead-code. |
| Completion | After a. where a is an Account: 71+ real fields from .nimbus/schemas/Account.json. Inside a SOQL literal: the target SObject's columns; relationship traversal — typing Owner. or Account.Parent. resolves the relationship chain and completes the related object's columns; child subqueries ((SELECT … FROM Contacts)) complete the child object's columns; and SOSL RETURNING Account(…) completes that object's fields. Also: stdlib (System.*, Test.*, Database.*, UserInfo.*, Limits.*, Assert.*), keywords, in-file classes, and 12 Apex snippets (test, debug, soql, trycatch, …) with Tab-through placeholders. |
| Hover | Method signatures with return types, SObject field metadata, class mutation scores, test flakiness rate. |
| Signature help | Floating parameter tooltip while typing a method call. Active parameter bolded, comma-counted even through generics like List<Map<String, Object>>. |
| Semantic tokens | Theme-aware highlighting: annotations, keywords, stdlib namespaces, types, numbers, comments, strings. Custom required modifier reserved for <required>true</required> fields. |
| Document formatting | Conservative opinionated formatter: normalises indentation (4 spaces), trims trailing whitespace, collapses 3+ blank lines to 2. Preserves strings, SOQL, and comments verbatim. Idempotent on clean input. |
Navigation & refactoring
| Capability | What you see |
|---|---|
| Go-to-definition | Jumps from any identifier to its declaration. Cross-file lookup across force-app/, src/, and stubs/. |
| Type definition | Jumps from a variable reference to the .cls of its declared type (Account a → Account.cls). |
| Implementation | From an interface or base class name, returns every class that extends/implements it across the workspace. |
| Find all references | Workspace-wide caller lookup for any symbol. Powered by a reference index rebuilt incrementally on every keystroke. |
| Rename | Workspace-wide rename applied as a single atomic WorkspaceEdit. Keyword guard prevents renaming to reserved words. |
| Workspace symbol search | Cmd+T finds any class, method, field, or enum across the repo. Ranks exact match → prefix → substring, capped at 500 results. |
| Document symbols | Full outline tree: classes → methods, fields, properties, inner classes, enums. Powers the editor's sidebar outline and sticky scroll. |
| Document highlight | Same-symbol highlighting of every occurrence of the identifier under the cursor. |
| Folding ranges | Brace-based code folds for classes, methods, and block comments. |
| Selection ranges | Expand-selection (Ctrl+W in JetBrains, Shift+Alt+→ in VSCode): word → line → file. |
| Call hierarchy | Method-level caller tree. Incoming and outgoing calls resolved through the workspace symbol index. |
Code actions (quick fixes and refactors)
| Action | Where it shows |
|---|---|
| SOQL column auto-fix | Yellow bulb next to an unknown-field diagnostic — offers up to 3 Levenshtein-ranked candidates. Fix: 'Namee' → 'Name'. |
Run nimbus sync | Offered as the fix for an unknown-sobject diagnostic. |
| Generate test stub | On any non-test method: scaffolds ClassNameTest.test_method() with schema-aware constructor calls (new Account(Name='Test') from the real required-fields set). |
Extract to @TestSetup | On insert statements inside a test method. |
Add @IsTest annotation | On classes whose name ends in Test but lack the annotation. |
| Generate constructor | On a class declaration line: emits a constructor assigning every uninitialised instance field from a like-named parameter. |
| Generate getters and setters | On a class declaration line: accessor methods for the instance fields that lack them (skips existing accessors and final-field setters). |
| Implement interface methods | On a class declaration line with an implements clause: stubs every not-yet-implemented method. Resolves interfaces same-file, workspace-wide, and for common system interfaces (Queueable, Schedulable, Comparable, Database.Batchable). |
| Override superclass methods | On a class declaration line with an extends clause: override stubs for the superclass's virtual/abstract methods not yet overridden (virtual delegates to super.x(), abstract gets a TODO). |
Platform-data overlays (Nimbus-exclusive)
| Capability | What you see |
|---|---|
| Inlay hints | Ghost text next to each method: hit count from coverage (3×, not covered), surviving mutant descriptions (☠ survived: Shifted loop bound +1), flaky-test warnings (⚠ flaky (12%)), per-class mutation score. |
| Code lenses | Inline buttons above every class and method: ▶ Run test, ◉ Debug, 🪄 Coverage, ≡ View Trace on @IsTest methods; ▶ Run class, ⏱ Bench class on test classes; ☣ Mutate on production classes. |
| Dead code detector | Hint-severity diagnostic on methods with zero workspace references and zero coverage hits. Skips global, virtual, abstract to avoid false positives on public APIs. |
Still on the roadmap: inline values while debugging (blocked on LSP ↔ DAP co-hosting), coverage-filtered call hierarchy, extract- variable / inline-variable / inline-method refactors, and long- tail protocol polish (pull-model diagnostics, completion resolve, cross-workspace monikers). See LSP_ROADMAP.md in the source for the full plan.
VSCode
The Nimbus VSCode extension auto-starts the LSP when the extension activates on a Salesforce project. No extra configuration needed. Toggle it off with the nimbus.lsp.enabled setting if you want to fall back to Salesforce's stock Apex LSP.
// .vscode/settings.json
{
"nimbus.lsp.enabled": true, // default
"nimbus.binaryPath": "nimbus" // if 'nimbus' is not on PATH, point here
}Check that the server is running: View → Output, pick Nimbus Apex Language Server from the dropdown. You should see JSON-RPC messages for each keystroke. The Nimbus LSP Trace channel has the protocol-level detail if you need to debug further.
Neovim
Neovim 0.11+ has native LSP support. Drop this into your config:
-- ~/.config/nvim/init.lua (or any loaded file)
vim.lsp.config.nimbus = {
cmd = { 'nimbus', 'lsp' },
filetypes = { 'apex', 'apexcode' },
root_markers = { 'sfdx-project.json' },
-- Optional: write a protocol trace for debugging
-- cmd = { 'nimbus', 'lsp', '--log', '/tmp/nimbus-lsp.log' },
}
vim.lsp.enable('nimbus')
-- .cls and .trigger don't have a built-in filetype; teach Neovim:
vim.filetype.add({
extension = {
cls = 'apex',
trigger = 'apex',
},
})For older Neovim (0.8–0.10) using nvim-lspconfig:
require('lspconfig.configs').nimbus = {
default_config = {
cmd = { 'nimbus', 'lsp' },
filetypes = { 'apex' },
root_dir = require('lspconfig.util').root_pattern('sfdx-project.json'),
settings = {},
},
}
require('lspconfig').nimbus.setup({})Zed
Zed uses per-language server configuration in settings.json:
// ~/.config/zed/settings.json
{
"lsp": {
"nimbus": {
"binary": { "path": "nimbus", "arguments": ["lsp"] }
}
},
"languages": {
"Apex": { "language_servers": ["nimbus"] }
},
"file_types": {
"Apex": ["cls", "trigger"]
}
}Helix
Helix wires LSP via languages.toml:
# ~/.config/helix/languages.toml
[language-server.nimbus-lsp]
command = "nimbus"
args = ["lsp"]
[[language]]
name = "apex"
scope = "source.apex"
file-types = ["cls", "trigger"]
roots = ["sfdx-project.json"]
language-servers = ["nimbus-lsp"]JetBrains (IntelliJ, WebStorm, etc.)
The simplest path is the dedicated Nimbus JetBrains plugin — it wires up the language server, test running, debugging, coverage, and tool windows for you. Install Nimbus — Local Apex Runtime from the Marketplace and you’re done.
If you’d rather wire the language server by hand, JetBrains 2023.2+ supports generic LSP servers via the LSP4IJ plugin (or built-in LSP support in paid editions). After installing LSP4IJ:
- Settings → Languages & Frameworks → Language Servers → +
- Name:
Nimbus Apex - Command:
nimbus lsp - Mappings: file type
*.clsand*.trigger
Code lenses, inlay hints, completion, and hover all render natively in the JetBrains UI. Go-to-definition uses the standard Cmd+B / Ctrl+B shortcut.
Emacs
With lsp-mode:
(use-package lsp-mode
:hook ((apex-mode . lsp)))
(with-eval-after-load 'lsp-mode
(lsp-register-client
(make-lsp-client :new-connection (lsp-stdio-connection '("nimbus" "lsp"))
:major-modes '(apex-mode)
:server-id 'nimbus-lsp)))With eglot (built-in since Emacs 29):
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(apex-mode . ("nimbus" "lsp"))))Troubleshooting
The server fails to start
- Run
nimbus --versionin a terminal to confirm the binary is onPATH. - Start the server manually:
nimbus lsp --log /tmp/nimbus-lsp.log. Type a few characters and hitCtrl+C. The log should show JSON-RPC messages; if the file is empty, the binary isn't being found by the editor.
Diagnostics don't appear
- Check the editor's LSP status — most editors surface a connection indicator. In VSCode, View → Output → Nimbus Apex Language Server.
- Verify the file extension is mapped to
apexlanguage. Neovim in particular doesn't auto-detect.cls.
Completion is empty after `a.`
- The LSP needs the variable's declared type. If
ais declared in a different file, cross-file type resolution isn't wired yet — declare it locally, or use the fully-qualified class name:Account.Nameinstead ofa.Name. - Custom SObjects appear after running
nimbus synconce to populate.nimbus/schemas/.
Coverage / mutation inlay hints don't show
- Inlay hints require a recent
nimbus test --coveragerun so.nimbus/coverage/latest.jsonexists. Mutation hints neednimbus mutateto have populated.nimbus/mutations/latest.json. - Some editors hide inlay hints by default — check the "Editor: Inlay Hints" toggle in your settings.
The Salesforce LSP and Nimbus LSP are both running in VSCode
The Salesforce Apex extension and the Nimbus extension can coexist, but you'll get duplicate completion entries. Set nimbus.lsp.enabled: false to fall back to Salesforce's LSP, or disable the Salesforce Apex extension to use only Nimbus.
VSCode Extension
Overview
The Nimbus VSCode extension brings the full test runner into your editor — inline results, coverage gutters, a debugger, execution traces, governor limit tracking, and more. It communicates with the nimbus daemon over a local JSON-RPC connection, so test runs feel instant: no cold-start, no reloading metadata on every run.
The extension also auto-starts the Nimbus Language Server for editing intelligence — completion, hover, go-to-definition, SOQL column validation, and coverage inlay hints. The LSP is a sibling subsystem to the test runner, independent of the daemon socket. Toggle it with nimbus.lsp.enabled in settings.
The extension activates automatically when it detects a Salesforce project (sfdx-project.json, force-app/, or any .cls file). Check the status bar — a Nimbus indicator shows whether the daemon is connected. If it shows a slash through it, run Nimbus: Restart Daemon from the command palette and check the Nimbus output channel for details.
The daemon requires a Pro license. Free-tier users can still run tests from the CLI with full coverage and governor limit support, but the live editor integration (inline results, trace viewer, history) requires Pro.
Running Tests
Inline buttons (CodeLens)
Every @isTest class shows action buttons directly above the code — no menus needed.
| Location | Actions |
|---|---|
| Class declaration | Run All Tests, Validate |
Each @isTest method | Run Test, Debug Test, View Trace |
| Non-test class declaration | Validate |
| Non-test public method | Run, Debug |
Test Explorer
Nimbus integrates with VSCode's native Test Explorer. Tests are discovered automatically when the daemon connects — no configuration needed. Run, debug, and filter tests from the standard VSCode testing UI.
Understanding results
After a test run:
- A notification shows the pass/fail summary and total time
- The Nimbus output channel shows results grouped by class and method
- Inline pass/fail decorations appear on executed lines in the editor
- The status bar shows live progress during the run (
3/10 tests...)
Debugging
Nimbus supports the full VSCode debug protocol — breakpoints, stepping, variable inspection, call stack.
Debug a test method
- Open a test class
- Set breakpoints by clicking the gutter on any line
- Click Debug Test above an
@isTestmethod - Use the debug toolbar: Step Over (
F10), Step In (F11), Step Out (Shift+F11), Continue (F5) - Inspect locals in the Variables panel; view the call stack in Call Stack
Debug any public method
Not limited to tests. Click Debug above any public method in a non-test class. If the method has parameters, Nimbus prompts you to enter values before starting.
Launch configurations
You can also configure debug targets in .vscode/launch.json:
{
"type": "nimbus-apex",
"request": "launch",
"name": "Debug My Test",
"test": "MyTestClass.myTestMethod",
"stopOnEntry": false
}Coverage
Enable coverage collection by setting nimbus.coverage.enabled to true, or pass --coverage from the CLI. After a test run with coverage enabled:
Gutter icons
Colored icons appear in the editor gutter next to each line:
| Icon | Meaning |
|---|---|
| Green circle | Line was executed during the test |
| Red circle | Line was not executed |
| Yellow circle | Branch partially covered (e.g. only the true path of an if) |
Toggle gutter icons with the nimbus.coverage.showGutterIcons setting.
Method coverage annotations
An inline annotation appears after each method signature:
public static void processRecords(List<Account> accounts) // 75% (3/4 lines)Run Nimbus: Show Coverage from the command palette for the full coverage report in the output channel.
Trace Viewer Pro
The trace viewer shows a detailed execution trace for every method call, branch decision, and variable assignment during a test run. Click View Trace above any test method after running it, or run Nimbus: View Execution Trace from the command palette.
The trace viewer opens as a side panel with three tabs:
| Tab | What it shows |
|---|---|
| Call Tree | Indented tree of all method invocations. Each node shows name, duration, and pass/fail status. Click any node to inspect it. |
| Timeline | Horizontal waterfall chart. Wider bars = slower methods. Useful for spotting bottlenecks at a glance. |
| Log | Filterable list of all trace events with timestamps. Use the search box to filter by method name or event type. |
Selecting any span in any tab updates the right sidebar with the captured local variables,System.debug() output, and span metadata (source file, line number, status).
Watch Mode Pro
Automatically re-run tests whenever Apex files change.
- Click the Watch: off button in the status bar, or run Nimbus: Toggle Watch Mode
- If your project has multiple package directories, choose All Packages or a specific package
- The status bar updates to show the active scope (e.g.
Watch: force-app) - Save any
.clsor.triggerfile — tests re-run automatically - After each watch-triggered run the status bar briefly shows the result (
Watch: all passedorWatch: 2 failed) - Click the button again to disable
Diagnostics & Validation
Nimbus validates Apex syntax in real time and surfaces errors in the Problems panel (Cmd+Shift+M).
- On save — when
nimbus.validateOnSaveis enabled (default:true), every.clsand.triggerfile is checked automatically - Manual — run Nimbus: Validate Current File to check the active file
- Quick fixes — some parse errors offer a lightbulb with suggested fixes (e.g. adding a missing semicolon)
Errors appear as red squiggles in the editor and as entries in the Problems panel.
Execute Anonymous & Run Any Method
Execute Anonymous Apex
Run arbitrary Apex without creating a class. Run Nimbus: Execute Anonymous Apexfrom the command palette. If text is selected, only the selection runs — otherwise the entire file runs. Output (including System.debug()) appears in theNimbus output channel.
Run any public method
Click Run above any public method in a non-test class. If the method has parameters, input boxes appear for each one with type hints. The return value and execution time appear as an inline annotation next to the method signature.
Auto-retrieve missing metadata
When tests fail due to missing SObjects, fields, or other metadata not in your local project, Nimbus detects what's missing and shows a warning notification. Choose:
| Option | What happens |
|---|---|
| Fetch & Re-run | Retrieves the missing metadata from your connected org and re-runs the failed tests |
| Show Details | Opens the output channel with the missing items and the sf CLI commands to retrieve them manually |
| Dismiss | Ignores for now |
You can also trigger this manually with Nimbus: Fetch Missing Metadata & Re-run.
Commands
All commands are available from the command palette (Cmd+Shift+P) with the Nimbus: prefix.
| Command | Description |
|---|---|
Run All Tests | Run every @isTest class in the project |
Run Tests in Current File | Run all tests in the active editor file |
Run Test Class | Run all tests in a specific class by name |
Run Test Method | Run a specific test method |
Debug Test Method | Start a debug session for a test method |
Cancel Test Run | Stop the currently running tests |
Validate Current File | Check Apex syntax, report errors to Problems panel |
Execute Anonymous Apex | Run selected or all Apex code in the active file |
Show Coverage | Display the full coverage report in the output channel |
Toggle Watch Mode | Enable or disable auto-run on file save |
Show Status | Show daemon version, uptime, and database status |
Restart Daemon | Restart the background daemon process |
View Execution Trace | Open the trace viewer for a test method |
Show History Trends | Open pass rate chart and flaky test table |
Compare Test Runs | Diff two historical runs side by side |
Refresh Schema | Reload the Schema Explorer |
Fetch Missing Metadata & Re-run | Retrieve missing metadata from the org and re-run failed tests |
Settings
All settings are under the nimbus.* namespace in VSCode settings.
| Flag | Default | Description |
|---|---|---|
nimbus.binaryPath | "nimbus" | Path to the nimbus CLI binary |
nimbus.parallel | 4 | Number of parallel test workers |
nimbus.lsp.enabled | true | Run the Nimbus Language Server for Apex editing (completion, hover, inlay hints, SOQL validation). Disable to fall back to Salesforce’s stock Apex LSP. |
nimbus.coverage.enabled | false | Collect coverage data on test runs |
nimbus.coverage.showGutterIcons | true | Show green/red/yellow coverage icons in the editor gutter |
nimbus.validateOnSave | true | Validate Apex syntax automatically on save |
nimbus.autoRun.onOpen | false | Auto-run tests when a test file is opened |
nimbus.autoRun.onSave | false | Auto-run tests when a test file is saved |
nimbus.performance.showInlineTimings | true | Show method execution time annotations in the editor |
nimbus.governor.showCodeLens | true | Show governor limit CodeLens above methods |
JetBrains Plugin
The Nimbus plugin brings the full local Apex runtime to IntelliJ IDEA and every other JetBrains IDE — both Community and the paid editions. It shares the same two engines as the VSCode extension: the nimbus lsp language server (completion, hover, navigation, rename, diagnostics, code lenses, inlay hints) and the nimbus daemon JSON-RPC server (test running, debugging, coverage, traces, history, schema, governor limits).
Editor intelligence is delivered through LSP4IJ, which the plugin pulls in automatically from the JetBrains Marketplace.
The plugin also bundles native Apex/SOQL live templates — type an abbreviation and press Tab to expand, then Tab through the placeholders. A few: sysd → System.debug(), tm → an @IsTest method, tc → a test class, soqlf → a bulk-safe SOQL for-loop, bulkt → a 200-record test-data block, mapid → Map<Id, SObject> from a list. The full set lives under Settings → Editor → Live Templates → Nimbus Apex.
Install
Install Nimbus — Local Apex Runtime from Settings → Plugins → Marketplace. The Marketplace resolves the LSP4IJ dependency for you. You also need the nimbus CLI on your PATH:
curl -fsSL https://install.testnimbus.dev | shOn first open of a Salesforce project the plugin starts the daemon and warms the language server. If the CLI isn’t found, set its location in Settings → Tools → Nimbus.
Running & Debugging
Code lenses appear above @IsTest classes and methods — Run, Debug, Coverage, Trace, Bench, and Mutate. Results stream live into the standard IntelliJ test runner tree, with inline ✔ passed / ✖ failed markers and timings on each test method, return values, and coverage gutter bars in the editor.
The debugger is a native IntelliJ debug session: set line breakpoints in.cls/.trigger files, then Debug a test — step over/into/out, inspect variables and the call stack, all driven by the local runtime (no org, no replay log).
Coverage delta. Run a coverage pass, then Set Coverage Baseline(Nimbus menu) to snapshot it. Toggle Coverage Delta overlays the change vs that baseline in the gutter — green for newly-covered lines, orange for regressions. The same overlay and commands ship in the VSCode extension.
You can also create a Nimbus Apex Test run configuration directly (Run → Edit Configurations) to run a glob pattern or an explicitClass.method list.
Tool Window
The Nimbus tool window (right dock) has four tabs:
- Tests — every discovered
@IsTestclass → method; double-click to jump to source, or run/debug the selection from the toolbar. - History — past runs → classes → methods, with a Trends chart and run comparison.
- Schema — the local PostgreSQL tables, columns, and PK/FK relationships.
- Governor — per-method SOQL/DML usage vs. limits from the last run.
Trace, Trends, and run-diff visualizations open in embedded browser panels (JCEF), the same views shipped in the VSCode extension.
Settings
Settings live under Settings → Tools → Nimbus (stored per project).
| Flag | Default | Description |
|---|---|---|
Nimbus binary path | "nimbus" | Path to the nimbus CLI binary |
Parallel test workers | 4 | Number of parallel test workers |
Run the Nimbus Language Server | true | Completion, hover, inlay hints, diagnostics via nimbus lsp |
Collect coverage on test runs | false | Collect coverage data on test runs |
Show coverage gutter icons | true | Show covered/uncovered bars in the editor gutter |
Validate Apex on save | true | Validate Apex syntax automatically on save |
Show inline method timings | true | Show method execution annotations after runs |
Show governor usage inline | true | Show SOQL/DML usage after test runs |
Run tests when a test file is opened | false | Auto-run on open |
Run tests when a test file is saved | false | Auto-run on save |