AI agents
Give a coding agent the one thing it cannot get anywhere else: a place to actually run the Apex it writes, and evidence that it works.
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.
Namespaced custom labels
Managed packages ship custom labels too, and Apex reads them as Label.npe03.RecurringDonationStageName — the dotted form is the only spelling the platform compiles for a packaged label. Because a retrieved CustomLabels file carries no namespace element, Nimbus puts the namespace in the file name:
stubs/
└── labels/
├── CustomLabels.labels-meta.xml # unnamespaced labels → Label.MyLabel
├── npe03.labels-meta.xml # → Label.npe03.<Name>
└── npo02.labels-meta.xml # → Label.npo02.<Name>The file contents are ordinary CustomLabels metadata — the same XML sf project retrieve writes:
<?xml version="1.0" encoding="UTF-8"?>
<CustomLabels xmlns="http://soap.sforce.com/2006/04/metadata">
<labels>
<fullName>RecurringDonationStageName</fullName>
<language>en_US</language>
<protected>false</protected>
<shortDescription>RecurringDonationStageName</shortDescription>
<value>Pledged</value>
</labels>
</CustomLabels>Stub labels only fill gaps: a label the project itself defines always wins the unnamespaced lookup. A label that resolves nowhere still degrades to an empty string with a warning naming the ns.Name it could not find, so adding label stubs never introduces a new hard failure.
Write them by hand, scaffold one with nimbus stub label, or pull an entire package's labels out of an org that has it installed with nimbus stub pull.
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}'AI agents
Overview
Your agent can write Apex anywhere. Here it can also run it — instantly, locally, with no org — and get evidence back: which tests passed, what each line cost, who wrote every field on every record.
That is the whole difference. Salesforce ships an MCP server for metadata; several tools will read your source. None of them can execute the code. An agent that cannot execute is guessing, and an agent that guesses about Apex guesses about triggers, order of execution, governor limits and null semantics — the four things it is worst at.
nimbus mcp is a Model Context Protocol server over stdio. It starts one runner — database, project sources, schema — and holds it open for the session, so a tool call costs a test run, not a cold start.
What an agent gets that it does not have today:
- Execution. Run the suite, one class, one method, or a block of anonymous Apex. Sub-second on a warm runner.
- Evidence. Not a log to parse — structured failure diagnostics with the assertion's operands, the SOQL/DML tail before the break, and the source around it.
- A recording. When a test fails, Nimbus re-runs it with tracing on. The agent gets a span tree of what actually executed and what each step cost.
- Causality. Every field write of that run, with the layer that made it — Apex, trigger, flow, roll-up, formula, platform default — and the exact frame. Why is this value what it is stops being a guess.
- Safety. Everything is rolled back unless the agent explicitly asks to commit.
Nothing here reaches a Salesforce org. There is no sandbox to refresh, no deploy to wait for, and no shared org for the agent to break.
Setup
The server discovers your project from its working directory, so point cwd at the folder containing sfdx-project.json.
Claude Code — one command, run from the project root:
claude mcp add nimbus -- nimbus mcpCursor — .cursor/mcp.json in the project root:
{
"mcpServers": {
"nimbus": {
"command": "nimbus",
"args": ["mcp"]
}
}
}Any other MCP client — the same shape, with an explicit working directory when the client does not launch the server from your project:
{
"mcpServers": {
"nimbus": {
"command": "nimbus",
"args": ["mcp", "--parallel", "4"],
"cwd": "/absolute/path/to/your/salesforce/project"
}
}
}| Flag | Default | Description |
|---|---|---|
--parallel | 0 | Worker count for run_apex_tests (0 = NumCPU). Fixed at server start; the runner sizes its connection pool and worker fan-out from it, so it cannot change per call. |
--coverage | true | Collect coverage so get_coverage works after every run. Set false in tight agent loops that never read coverage. |
--read-only | false | Refuse any call that could leave a durable change behind. Tests and Apex still run — they roll back. The safe default for a shared machine or a CI job; see Safety caps. |
--verbose | false | Raise the stderr log to debug. Safe: stdout is reserved for the protocol. |
First call. Have the agent call nimbus_capabilities. It returns the tool catalog, the versioned payload schemas, the licence tier, and what this particular server can answer right now — cheaper than discovering the surface by trying tools and reading errors.
Stdio safety. Stdout carries JSON-RPC framing and nothing else. The runner writes to io.Discard, and the process-global stdout is rerouted to stderr for the server's lifetime so a stray print anywhere in the call tree cannot corrupt the stream. Tail stderr to watch the agent work: every tool call logs a structured start/complete line.
Tool catalog
Grouped by what an agent is trying to do. Every payload is bounded — a truncated list always reports the true count alongside — and every payload states what it does not establish, because an agent that reads a filtered result as the whole picture is the failure mode worth engineering against. For the authoritative list at any version, have the agent call nimbus_capabilities.
Run things
| Method | Description |
|---|---|
run_apex_tests | Run tests by pattern ("*", a class, Class.method, or a path). Returns totals and per-failure detail — message, file, line, structured diagnostics — plus a replayable recording of the failures. Bound with max_failures; disable the recording with record_trace=false. |
execute_anonymous | Execute a block of anonymous Apex. Rolled back by default; commit=true keeps the writes. Returns success, captured System.debug output, and the exception on failure. |
run_method | Invoke one Apex method with typed JSON arguments — the tool renders the Apex literals, so no argument is a quoting exercise. Returns a typed result. Rolled back by default. |
run_mutation_tests | Pro. Mutate the code and report which mutants survive — the holes in your assertions, not just the uncovered lines. |
Understand a failure
| Method | Description |
|---|---|
get_test_failure | Everything about one failure in one call: message, stack, structured diagnostics, the source lines around it, and the replay pointer. Answers from the last run when it covered that test, otherwise runs just that test. |
explain_failure | The same failure through the shared nimbus.explain/v1 contract, identical to `nimbus explain --json`, so a human and an agent never read divergent accounts. redact=true masks runtime values for output that will be pasted elsewhere. |
triage_failures | Group a run’s failures by the cause the engine recorded when each happened — not by matching message text. Use it on a wall of red to find how many distinct problems there actually are. Failures the engine could not classify come back individually rather than folded into a plausible-looking group. |
get_execution_trace | A recorded run’s spans: the call tree with timing, parent links, per-span governor cost, and SOQL/DML/trigger/flow summaries. Frozen schema, see below. |
query_field_history | Pro. Every field write of a recorded run, with what it replaced, the layer that wrote it, and the exact frame. |
get_governor_usage | Per-test governor consumption from the last run, plus the peak per limit. A limit that scales with record count is the signature of un-bulkified code. |
Read the project
| Method | Description |
|---|---|
query | Run a read-only SOQL SELECT against the local database. DML and non-SELECT statements are rejected — use execute_anonymous for writes. |
describe_schema | An SObject’s fields (type, required, unique, references, relationship name, picklist values) and child relationships. Omit the argument to list every SObject the project has schema for. |
get_coverage | Line coverage from the last run. Narrow it to one class to also get the uncovered executable lines in its file and per-method figures — a method with calls=0 was never executed. Uncovered lines are omitted from a whole-project report, where they would be thousands of numbers nobody can act on. |
list_test_classes | Every @isTest class with its source file. Runs nothing. |
query_graph | What a change to one class can reach: dependents, dependencies, and the tests that actually executed it. Read the returned limits — this is reachability for navigation, not a set of affected tests, and a trigger dispatching through custom metadata is invisible to it. |
Write metadata
| Method | Description |
|---|---|
scaffold_metadata | Generate valid-by-construction source: Apex classes and triggers, LWC, Aura, Visualforce, objects, fields, labels, custom metadata, permission sets, field sets, record types, validation rules, list views. Call it with no arguments for the catalog of kinds and the options each accepts; dry_run=true previews without writing. |
Discover
| Method | Description |
|---|---|
nimbus_capabilities | The catalog above, plus the versioned schemas and their compatibility promise, the licence tier, and this server’s current state — coverage on or off, which runs have been recorded, what the last run did. |
Why scaffold rather than write the XML. The metadata file vocabulary is not the describe vocabulary: a file wants Html, EncryptedText, MultiselectPicklist where a describe answers TEXTAREA, ENCRYPTEDSTRING, MULTIPICKLIST. An agent writing from memory reaches for the describe spelling, the field is created, it deploys, and it behaves wrongly. Every kind scaffold_metadata emits has a round-trip test proving Nimbus can read back what it wrote.
Resources
A tool call is a verb an agent spends a turn on. A resource is a noun it can attach: a stable URI, an unchanging shape, no arguments to get wrong, and cacheable by the client — so re-reading your project's schema costs nothing.
Everything under nimbus:// is read-only and executes nothing: no test run, no Apex, no DML, no files written. That is what makes them safe to fetch speculatively, and it is why neither a safety cap nor read-only mode ever refuses one. An agent that has spent its entire budget can still read every resource below.
| Method | Description |
|---|---|
nimbus://capabilities | The same payload nimbus_capabilities returns — the tool and resource catalog, the frozen schemas, the licence tier, the active caps and what has been spent against them. Built by the same code as the tool, so the two can never disagree. |
nimbus://project | Package directories from sfdx-project.json, class and trigger counts, and the @isTest class inventory with source paths. |
nimbus://parity | The published compatibility scorecard shipped with this binary: per-repository pass rates over the open-source corpus and how many failures were verified against a real org. Note what it is not — a measurement of your project. |
nimbus://coverage | Line coverage from the most recent run, overall and per class. |
nimbus://coverage/{class} | One class, including the executable lines no test reached — the list you need to write the missing test. |
nimbus://schema | Every SObject this project has schema for. |
nimbus://schema/{object} | One SObject’s fields (type, required, unique, references, picklist values) and child relationships. |
The two templated URIs are RFC 6570 resource templates, and each one's list endpoint enumerates its members — so an agent never has to guess a valid substitution. Every payload is bounded and publishes the true count beside a possibly-truncated list.
An empty answer always says why. Read nimbus://coverage before anything has run and you get available: false with a reason, not a zero-percent report. An empty payload that reads like a real measurement is the one wrong answer an agent acts on with total confidence.
No subscriptions. Nimbus does not advertise resources/subscribe. Coverage does change after a run, so a subscription would be genuinely useful — but the transport cannot service one, and advertising a capability that answers method-not-found is worse than not having it. Each payload states the staleness instead: coverage is the last run's, and re-reading after a run is how you get the new one.
The agent loop
The tools are shaped for one sequence, and it is worth putting in your agent's instructions:
run_apex_tests { "pattern": "AccountServiceTest" }
-> 2 failed; each failure carries trace_run_id
get_test_failure { "test": "AccountServiceTest.testInsert" }
-> the assertion, its operands, the SOQL/DML tail, the source lines
... edit the class ...
run_apex_tests { "pattern": "AccountServiceTest.testInsert" }
-> greenWhen the failure is not obvious from the assertion, the evidence chain goes one level deeper:
get_execution_trace { "run_id": "<from the failure>",
"test": "AccountServiceTest.testInsert",
"kinds": ["soql", "dml", "trigger"] }
-> what actually ran, in order, and what each step cost
query_field_history { "run_id": "<the same>", "field": "Account.Rating" }
-> Rating was "" -> "Hot", written by AccountTrigger.beforeInsert:7On an unfamiliar codebase where the first run returns hundreds of failures, start with triage_failures instead. Grouping a wall of messages is exactly the task a model does confidently and wrongly; the engine already recorded the cause of each one.
Start narrow. Every payload reports the true count of what it capped, so an agent can always widen deliberately. The defaults are chosen so a first call is cheap.
Writes and rollback
An agent is allowed to execute arbitrary Apex here because by default it can observe anything and change nothing.
execute_anonymous and run_method run inside an isolating transaction that is discarded the moment the call finishes. Every response carries a committed field saying which happened, so the agent never has to infer it. Passing commit: true is a separate, explicit act, and the response's limits then say the writes are still there.
Two boundaries worth knowing, both stated in the payloads themselves:
- The isolating transaction defers constraint checks, so an insert that would violate a unique or foreign-key constraint on commit reports success under rollback.
- Seed data is committed deliberately, before the transaction opens. Rollback never reverts seeding.
Test runs are unaffected either way: each test already runs in its own transaction and is rolled back after.
scaffold_metadata is the one tool that writes to your source tree. It refuses to overwrite an existing file unless asked, refuses to write outside the project root, and dry_run: true reports what it would create without creating it.
Safety caps
An MCP session hands an autonomous agent a warm runner and no natural stopping point. A loop that re-runs the suite after every edit, or retries a failing block with a slightly different guess, costs the agent nothing it can feel — so nothing stops it. These are the ceilings that make an unattended session bounded.
All of them are off by default. An interactive developer should never meet a limit they did not set. Turn them on where a session is unattended or shared.
| Flag | Default | Description |
|---|---|---|
nimbus.mcp.max-test-runs-per-session | 0 | Cap on test-executing calls: run_apex_tests, triage_failures, get_test_failure, explain_failure, run_mutation_tests. 0 disables. |
nimbus.mcp.max-apex-executions | 0 | Cap on agent-supplied Apex: execute_anonymous and run_method. Metered separately — it is the surface where the agent writes the code. 0 disables. |
nimbus.mcp.max-wall-clock | 0 | Total session wall time after which metered calls are refused, e.g. 30m. 0 disables. |
nimbus.mcp.read-only | false | Refuse any call that could leave a durable change behind. Also available as `nimbus mcp --read-only`. |
A refusal always names itself. Exceeding a cap is a tool error stating which cap, what it was set to, how much was used, and the exact property that set it. Nothing stalls, and nothing quietly returns less than it was asked for — an agent that cannot see what refused it retries against a wall it does not know is there.
Reads are never metered. The caps apply only to calls that run something. Every read tool and every nimbus:// resource keeps working after a budget is spent, so an agent can always collect the results the session already produced.
Read-only mode
nimbus mcp --read-only is the safe default for a shared machine or a CI job. What it refuses is narrower than it sounds, and deliberately so:
execute_anonymousandrun_methodwithcommit: true— the same call withoutcommitstill runs, and you still get the result, the debug output and the exception. Only the writes are discarded.scaffold_metadataunlessdry_run: true, which returns the exact files it would have written.
Tests still run. Every test already executes in its own transaction and rolls back, so a test run leaves nothing behind — refusing them would make read-only mode useless on a test runner while adding no safety. What the mode gates on is whether a change can outlive the call, not whether the call touches the database.
The flag and the property compose one way only: either turns the mode on, and neither turns it off. A safety switch a config file can silently disarm is not a safety switch.
Both the caps and the mode are startup-bound and reported in nimbus_capabilities under state.budget, with used, limit and remaining, so an agent can pace itself rather than discover a ceiling by hitting it.
Execution traces
A replayable trace is not a cheap trace. Stepping through an execution needs per-line spans, which only exist at verbose tracing, and verbose costs roughly 65% more interpreter time and tens of megabytes per run — measured, not estimated. Recording every run to keep the few that matter is a bad trade.
So Nimbus retraces instead. When a run fails, the failing tests are executed a second time with tracing on. It pays only when something failed, and only for what failed: on a 1,212-test suite with 24 failures, that is about 2% of the work rather than all of it.
run_apex_tests does this automatically and returns the recording's id on every failure it covers. Pass record_trace: false to skip it.
Three things a reader of a trace must not get wrong, all of which the payload states outright:
- A retrace is a second execution, not a replay of the first. A test that passes the second time is order-dependent or flaky. The manifest records
reproduced: falseand the trace'slimitssay the recording shows a passing execution — it is never presented as showing the failure. - A test that declares its failures deliberate is not recorded. A fixture that is supposed to fail is not a failure anyone needs to replay.
- An absent governor number means "not measurable", not zero. Counters reset inside a span —
Test.startTestdoes exactly that — make the delta meaningless, and claiming a number would be a lie.
Span schema v1
get_execution_trace returns nimbus.span/v1, stamped with schemaVersion: 1. It is an API, not an output format: integrations persist and diff these payloads, so the shape is fixed.
- Additive only within version 1. Fields are added; none is removed, renamed, retyped or given a new meaning.
- A breaking change ships as version 2 alongside 1, never in place. A consumer pinned to 1 keeps working.
- Consumers must ignore unknown fields, and must treat an unknown span
kindas opaque rather than an error — new kinds appear within v1 as the engine grows. - An absent number means "not measurable", never zero.
The document:
{
"schema": "nimbus.span/v1",
"schemaVersion": 1,
"run": {
"run_id": "2026-08-26T10-11-12_ab12cd34",
"kind": "failure",
"spans_recorded": 812,
"spans_returned": 200,
"truncated": true,
"steppable": true,
"failure": { "tests": [ { "class": "...", "method": "...", "reproduced": true } ] }
},
"filters": { "max_spans": 200, "include_events": true, "include_attributes": false },
"spans": [
{
"span_id": "a1b2c3d4e5f60718",
"parent_span_id": "0918273645abcdef",
"kind": "dml",
"name": "apex.dml.insert",
"start_ms": 12.5,
"duration_ms": 43.0,
"depth": 3,
"status": "ok",
"test": "AccountServiceTest.testInsert",
"line": 42,
"governor": { "dml": 1, "dml_rows": 200 },
"dml": { "operation": "insert", "sobject": "Account", "records": 200 },
"events": [ { "name": "apex.field.write", "at": "...", "attributes": { } } ]
}
],
"limits": [ "spans are what the recorded run captured at its trace level, ..." ]
}Span kinds. session, test, setup, method, soql, dml, trigger, flow, validation, rollup, async, assign, branch, loop, eval, db, http, parse, lex, ast, interpreter, other. The raw span name is always carried verbatim in name; kind is a grouping over it.
Read the details. status: "unset" is OpenTelemetry's default and means no status was reported — not that the span succeeded. depth and test are computed by walking parent links, so a span whose parent is missing from a truncated file reports depth 0. Filtering by kinds can leave parent_span_id pointing at a span that is not in the document.
The full reference, including every kind-specific detail object and every event name, is committed alongside the code as docs/mcp-span-schema-v1.md, with a machine-readable JSON Schema at internal/mcp/schema/span-v1.schema.json. The emitted payload is validated against that schema in CI, so the document and the code cannot drift apart.
Field history Pro
query_field_history answers the question that costs Salesforce developers the most time and that no static tool can answer at all: why is this value what it is?
Every SObject field write a recorded run performed, in order, with what it replaced, which layer wrote it — apex, trigger, flow, rollup, formula, default, platform — and the exact frame: Class.Method:line, TriggerName.Operation:line, a flow's name, a roll-up definition, or the save-pipeline phase.
{
"schema": "nimbus.field-history/v1",
"run": { "run_id": "2026-08-26T10-11-12_ab12cd34", "ledger": "derived" },
"count": 3,
"total": 3,
"writes": [
{ "test": "AccountServiceTest.testInsert", "seq": 1,
"sobject": "Account", "field": "Rating",
"identity": "rec-1", "record_id": "",
"old": "", "new": "Hot",
"writer": "trigger", "location": "AccountTrigger.beforeInsert:7" }
],
"limits": [ "writes are only those the recorded run performed — ..." ]
}Filter by sobject, field (either "Rating" or "Account.Rating"), record (an Id or a ledger identity), or test.
Identity, not Id. A record's writes before the save pipeline assigned it an Id carry an empty record_id. Follow identity to see the whole chain across that boundary.
The queryable index is derived from the recording on demand when it is missing, so a run recorded before Pro was active is still readable. The raw writes live in the trace either way — only the queryable surface is Pro.
Agent skills
MCP exposes the primitives. Skills are short playbooks that tell an agent when and how to reach for them — which tool answers which question, and in what order. Install them with nimbus skills install; the binary fetches from nimbus-skills and writes to the right path for your agent.
fix-failing-apex-test— the inner loop above, written out.bootstrap-nimbus— set Nimbus up on a fresh SFDX project, CI snippet included.apex-coverage-uplift— raise coverage by targeting the uncovered branchesget_coveragereports, not by adding call-only tests.
Discovery. Nimbus publishes an ARD catalog so compatible registries and agents can find this server from task intent. ARD handles discovery only: after selecting Nimbus, the client still installs the local binary and runs nimbus mcp over stdio.
Commands
nimbus sf
Run any Salesforce CLI core command or installed sf plugin through Nimbus. Arguments, the working directory, terminal input/output, interactive prompts, and the Salesforce process exit code pass through unchanged. Nimbus notices are suppressed so JSON output remains machine-readable.
nimbus sf org login web
nimbus sf data query --query "SELECT Id FROM Account" --target-org dev
nimbus sf project deploy start --source-dir force-app --target-org dev
nimbus sf package version create --package core --code-coverageFlags after nimbus sf belong to Salesforce CLI; Nimbus does not parse them. The Salesforce CLI plugin intentionally does not expose ansf nimbus sf command because the user is already insidesf. Recursive nimbus sf nimbus ... invocations are rejected.
nimbus toolchain sf
Report which Salesforce CLI Nimbus resolves for remote work, and whether that version is supported. Installation uses Salesforce's official npm package; updates delegate to the resolved Salesforce CLI. Ordinary Nimbus commands never install or replace Salesforce CLI implicitly.
nimbus toolchain sf status
nimbus toolchain sf status --verbose # resolved path, reported version line, policy
nimbus toolchain sf status --json # stable shape for CI and editors
nimbus toolchain sf install # official @salesforce/cli stable package
nimbus toolchain sf install --version 2.142.7
nimbus toolchain sf update --check # read-only available-version check
nimbus toolchain sf update # delegate to sf update stableResolution order
| Flag | Default | Description |
|---|---|---|
NIMBUS_SF_PATH | - | Explicit override. Pointing it at a missing or non-executable file is an error rather than a silent fallback to PATH |
sf on PATH | - | Used when no override is set |
Flags
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the status as JSON on stdout |
--verbose | false | Include the resolved path, the CLI's own version line, and the supported/tested versions |
Pinning a version
Set nimbus.sf.version in nimbus.properties to assert an exact Salesforce CLI version. Status reports a mismatch so CI can prove it reproduces the configured toolchain.
# nimbus.properties
nimbus.sf.version=2.142.7Install that exact version through Nimbus's assisted official-package flow:
nimbus toolchain sf install --version 2.142.7Install and update flags
| Flag | Default | Description |
|---|---|---|
install --version VERSION | - | Install an exact semantic version from the official @salesforce/cli npm package |
install --channel stable | stable | Install the current stable official package |
update --check | false | Ask Salesforce CLI to list available versions without installing |
update --version VERSION | - | Delegate installation of an exact update to the resolved Salesforce CLI |
update --channel CHANNEL | stable | Delegate an update for the selected Salesforce CLI channel |
Exit codes
The command exits non-zero when the toolchain is not usable as configured, so a CI bootstrap step can gate on it: Salesforce CLI missing, a legacy sfdxgeneration, or a violated nimbus.sf.version pin. A version that merely trails the tested one is reported but exits zero.
Nimbus's local loop — test, validate, exec,lsp, daemon — does not need Salesforce CLI at all, andnimbus doctor reports a missing CLI as a warning rather than a failure for that reason.
nimbus deploy
The safe one-shot Salesforce deployment. Nimbus snapshots the selected payload, runs local semantic validation and Apex tests against that snapshot, validates the same bundle in Salesforce, and only then deploys it. This gated path is available on Free; a raw bypass remains explicit as nimbus sf project deploy start .... The whole cycle runs through the Salesforce CLI's Metadata API deploy, which is what stages a bundle, validates it and produces a job id to quick-deploy later.
For the inner loop — the file you just edited, into a sandbox, now — reach for nimbus metadata deploy instead. It pushes what you name without staging, validating or writing a receipt, which is exactly what keeps the two commands from being confused for one another: this one is the gate, that one is the shortcut past it.
nimbus deploy --target-org staging --source-dir force-app
nimbus deploy --target-org production --manifest manifest/package.xml --confirm-production
nimbus deploy --target-org qa --metadata ApexClass:AccountService --metadata ApexClass:AccountServiceTestProduction targets require an interactive typed confirmation or the explicit --confirm-production flag. Nimbus records the org as a one-way fingerprint and never puts the alias, username, access token, or instance URL in the receipt. Generated receipts and bundles live under .nimbus/releases/.
Selection and gate flags
| Flag | Default | Description |
|---|---|---|
--target-org, --org, -o | - | Salesforce org alias or username. The global org flag — all three spellings are equivalent here and on every other command. |
--release-profile | - | Named nimbus.release profile to source the target org and gate settings from (Pro) |
--source-dir | - | Source directory; repeatable and mutually exclusive with the other selector kinds |
--manifest | - | Package manifest path |
--metadata | - | Metadata component selector; repeatable |
--metadata-dir | - | Metadata API directory or zip |
--nimbus-tests | * | Nimbus local test pattern |
--min-coverage | 0 | Minimum local line coverage percentage |
--test-level | org-dependent | Salesforce deployment test level. Unset resolves to RunLocalTests for production and NoTestRun elsewhere |
--tests | - | Salesforce tests for RunSpecifiedTests; repeatable |
--api-version | project version | Salesforce API version |
--pre-destructive-changes | - | Pre-deploy destructive manifest |
--post-destructive-changes | - | Post-deploy destructive manifest |
--allow-dirty | false | Allow and record an uncommitted Git worktree |
--confirm-production | false | Explicit production approval for CI/non-interactive use |
--report-mismatch | false | Opt in, for this run, to submit a sanitized report if Salesforce rejects a payload your local gates passed |
--no-report-mismatch | false | Never submit a mismatch report for this run; overrides configuration and --report-mismatch |
--json | false | Emit one stable JSON envelope |
nimbus release Pro
Feature deep-dive: release management.
Split validation from approval and deployment with a portable, immutable receipt. The exact uncompressed bundle is content-addressed; fallback deployment verifies and deploys that stored artifact, while production validation retains Salesforce's org-side job for quick deploy. Missing, changed, expired, wrong-org, or incompatible receipts are rejected with no override flag.
nimbus release plan --target-org staging # read-only: what would reconciling the org to source change?
nimbus release validate --target-org staging --changed # bundle and validate only source − org
nimbus release validate --release-profile production
nimbus release deploy --receipt .nimbus/releases/rel_....json --release-profile production --confirm-production
nimbus release run --release-profile staging
nimbus release status # org deploy queue + stuck-Pending diagnosis
nimbus release watch 0Af... | rel_... # live org-side progress until terminal
nimbus release promote rel_... --target-org prod # same bundle, validated against the next org
nimbus release rollback rel_... --target-org prod --confirm-production # restore what that deploy overwrote
nimbus release requeue rel_... # cancel a stuck Pending job, resubmit the validated bundle
nimbus release keygen # create a signing key + trust it in this project
nimbus release verify rel_... # offline: re-hash the bundle, check the signature
nimbus release list
nimbus release get rel_.... --jsonWith no selector, validate and run default their source to the packageDirectories in sfdx-project.json, the same way sf project deploy start does — so a bare nimbus release validate --target-org <org> just works. An explicit --source-dir, --manifest, or --metadata always wins. After a successful validate, the printed Next: line is copy-paste runnable: it carries the --target-org (or --release-profile) the deploy needs, since the receipt stores only a one-way org fingerprint, never the alias.
Exit codes: 0 success, 20 configuration, 21local gate, 22 Salesforce validation, 23 deploy,24 production confirmation, 25 release integrity violation (tampered / mismatched / missing bundle), 26 deploy submitted but outcome unconfirmed (run nimbus release watch <job>),27 drift — baseline components changed in the org after validation (revalidate, or deploy again with --override-drift),28 plan --fail-on-diff found source↔org differences (for scheduled reconciliation jobs), 29 counter-signature required but missing, or a production deploy by the same identity that validated (segregation).
Receipt and bundle files may be uploaded between CI jobs. When a downloaded receipt is passed by path, Nimbus locks, reads, and updates it beside its sibling bundle instead of silently creating a different local copy.
The deploy job still needs the project skeleton on disk. A receipt and its bundle are not enough on their own: sf deploys in project context, so the checkout that runs nimbus release deploy must containsfdx-project.json and every path its packageDirectoriesdeclares (an empty force-app/ is fine, but the directory must exist). Job-status polling (report / resume / cancel) is decoupled from local project state — Nimbus runs those org-side queries from a neutral throwaway project — so a broken checkout no longer makes a poll die while the org deployment succeeds. But the deploy command itself is still a project-context operation, so check the project skeleton out in the deploy job.
Named profile
# nimbus.properties
nimbus.release.profile=staging
nimbus.release.staging.targetOrg=staging
nimbus.release.staging.source.dirs=force-app
nimbus.release.staging.nimbusTests=*
nimbus.release.staging.minCoverage=75
nimbus.release.staging.minMutationScore=80
nimbus.release.staging.codeAnalyzerThreshold=2
nimbus.release.staging.requireCodeAnalyzer=true
nimbus.release.staging.validateLocal=true
nimbus.release.staging.validateSalesforce=true
nimbus.release.staging.requireCleanGit=true
nimbus.release.staging.mismatchReporting=ask
nimbus.release.staging.permissionSeamPolicy=allow
nimbus.release.staging.sfVersionPolicy=compatible
nimbus.release.staging.requireSigned=false
nimbus.release.production.requireCountersigned=truecodeAnalyzerThreshold (1-5) is optional. When set, the local gate runs Salesforce Code Analyzer (sf code-analyzer) against the staged bundle and fails the release if any violation is at that severity or more severe (1=Critical … 5=Info; e.g. 2 blocks Critical and High). The full violation counts by severity are recorded on the signed receipt whether or not the gate blocks — so the receipt carries static-analysis (form) evidence next to the behavioral (tests, coverage, mutation) evidence, both tamper-evident. It reuses the code-analyzer already in your Salesforce CLI; no separate scanner to install. The receipt also records the ruleSelectorand a policyDigest — a fingerprint over the rule selection, engine versions, and any committed analyzer config — in the signed core, so whichrules produced the counts is itself answerable and tamper-evident.
requireCodeAnalyzer=true makes the gate fail closed — the complement to requireSigned/requireCountersigned. Without it the gate is opt-in and its absence is silent: a receipt with no analysis looks identical to a clean scan. With it, a profile refuses to validate or deploy a receipt that carries no static-analysis result at all (it requires codeAnalyzerThreshold to be set). The setting for a regulated profile that must never ship un-scanned code.
minMutationScore (0-100) is optional. When set, the local gate runs mutation testing against the staged bundle — using nimbusTests as the test selection — after the tests pass, and fails the release if the measured mutation score is below the threshold. It is skipped entirely when unset, since mutation testing is expensive; set it only on profiles where you want tests held to a proven quality bar, not just a coverage percentage.
maxReceiptAge (a duration such as 240h) caps how old a receipt may be at deploy time. It applies to every receipt, not only quick-deployable ones — a validated bundle that has aged past the window deploys against a drifted org, so nimbus release deploy refuses it and asks you to revalidate. It defaults to the 10-day Salesforce quick-deploy window. Independently,nimbus.release.keepReceipts (default 30) caps how many receipts a successful validate retains under .nimbus/releases: the oldest terminal receipts (failed / succeeded / cancelled) beyond the cap are pruned along with their bundles, while still-deployable (org_validated /deploying) receipts and the one just created are always kept.
nimbus release deploy consumes an already-validated receipt, so it honors only --receipt (or the positional receipt), --target-org,--release-profile, --confirm-production, and--json. It reruns no gates and restages no source, so the validate-time selectors and thresholds are not offered there. Passing a receipt both positionally and via --receipt is an error rather than a silent pick.
permissionSeamPolicy is allow (default),block-custom, or block-all. Nimbus runs tests as a sysadmin-equivalent mock user. The two seam classes resolve differently, and conflating them is a common misread:
- FLS / object permissions default to
true(allow) outsideSystem.runAs()— a field or object check passes in Nimbus even when the org's running user would be denied. This is the permissive seam. - Custom permissions (
$Permission.X/FeatureManagement.checkPermission) do not default to allow. They resolve against seeded permission-set state and default to deny unless the permission is granted to the mock user. So an ungranted custom permission already readsfalse, exactly as it would in the org — no seam, nothing to block. Ablock-customseam fires when agranted custom permission is read through the default path outsiderunAs: the check passed because the seed granted it, not because the real running user holds it. The gate is deliberately narrow — it is not “unfireable”, it fires precisely on the granted-and-unguarded case.
block-custom fails the release on custom-permission seams (the highest-risk case, because those reads look like business logic, not security) while still allowing the FLS / object-perm seams. block-all blocks on any seam.allow never blocks, but the seam counts are always recorded on the receipt, so the divergence stays visible even under the permissive default.
Deploy queue visibility
Salesforce runs one deployment at a time per org and hides the queue, so a deploy waiting its turn is indistinguishable from a hung one — and a Pending job the scheduler silently dropped is indistinguishable from one about to start.nimbus release status reads the org's deployment queue, shows each job's position, live progress counters, and recent history correlated with your local receipts, and calls out a job that is genuinely stuck — Pending beyond a threshold, never started, with nothing running that explains the wait — together with the exact remedy. Jobs Nimbus did not create are diagnosed but never touched. The threshold is configurable via nimbus.release.stuckPendingAfter. Status is read-only and not Pro-gated: knowing your deploy is stuck is safety.
nimbus release status --json emits a stable, versioned document (schemaVersion, lowerCamelCase fields, whole-second durations) built for scripted gates. Active and in-flight jobs are always shown in full; the terminal recent-history list is capped — default 20, overridable with --limit — so a busy org's full day of deploys never floods a consumer.
Self-healing releases
Release validation and deployment submit asynchronously and own the wait: Nimbus polls the org, streams component and test counters as progress, and — when its own job sits Pending beyond a threshold with nothing running that explains the wait — cancels and resubmits the identical validated bundle automatically, with bounded retries. Nothing has executed org-side for a job that never started, and the payload cannot drift because it is the stored bundle, so no re-confirmation is asked; every recovery attempt is recorded on the receipt. Deployments Nimbus did not create are never touched. nimbus release watch follows any job or receipt read-only and mirrors the outcome in its exit code.
When a job stuck while nothing was attending it, nimbus release requeueapplies the same remedy by hand: it takes a receipt — never a bare job ID — verifies the org matches the receipt's fingerprint and the job is genuinely Pending and never started, then cancels it and resubmits the stored, digest-verified bundle. Foreign jobs are diagnosed by nimbus release status but never touched, so requeue can only ever act on a deployment Nimbus itself validated.
| Flag | Default | Description |
|---|---|---|
nimbus.release.pollInterval | 5s | How often a running job is polled |
nimbus.release.autoRequeueAfter | 5m | Pending age before an attended job is cancelled and resubmitted |
nimbus.release.maxRequeues | 2 | Self-recovery attempts before the stall is surfaced as an error |
nimbus.release.stuckPendingAfter | 3m | Pending age before release status flags a job as likely stuck |
Plan — read-only source ↔ org reconcile
nimbus release plan answers “what would deploying my source actually change?” before any gate runs: it retrieves the org's current copy of the metadata types present in your selection into a throwaway directory, compares normalized content, and reports each component as + new in source,~ changed, unchanged, or - org-only (what a destructive change would remove — plan only ever reports it). Nothing deploys, no receipt is written, and the project's own source is never touched. Line-ending and trailing whitespace differences are not changes.
Human output caps each category at 20 rows so a shared org's thousands of foreign components cannot drown your deployable changes; --json always carries the complete component list plus counts and the compared metadata types (component states use the same camelCase as the count keys: orgOnly,notCompared). Source components whose type plan cannot retrieve for diffing (profiles, for example) are reported as not compared rather than guessed at. Exit is 0 whether or not differences exist — unless--fail-on-diff is set, which exits 28 on any divergence so a scheduled CI job can alert when the org has drifted from source (the compensating control for deploys that bypass Nimbus entirely).
Promote — the same bundle through your environments
nimbus release promote <receipt> --target-org prod carries a validated (or already deployed) release's exact bundle to another org and validates it there, writing a new receipt chained to its origin via promotedFrom. Promote never re-collects from the worktree — the worktree may have moved on; the stored bundle has not — so the bundle digest and source fingerprint are byte-identical across the whole chain. That is the provable claim “what you validated in staging is what reached production”, and it sits under the receipt signature.
What re-runs and what doesn't follows one rule: environment-specific work re-runs, byte-determined work is inherited. Nimbus's local execution is hermetic, so local gate results are a pure function of the bundle bytes — identical bytes carry their proof, recorded as an inheritance note in the transition trail. The Salesforce validation always re-runs against the destination, because every org drifts on its own schedule; a changed-set baseline is likewise recomputed against the destination so the deploy-time drift check guards the org actually being changed. Promote validates only — deploy the receipt it prints, with --confirm-production where required.
Declare your promotion chain once and --next walks it:
# nimbus.properties
nimbus.release.environments = dev, uat, prod
nimbus release promote rel_... --next # from the receipt's org to the next stageThe receipt stores only a one-way org fingerprint, so --next derives the receipt's position by fingerprinting each configured alias. Promoting past a configured stage (dev straight to prod) is allowed but never silent: the receipt records the number of skipped stages inside the signed core, release get and release list show it, and hiding the jump would break the signature.
Changed-set releases and drift
--changed (on validate, run, anddeploy's free sibling) reduces the payload to source − org: only components that are new in source or differ from the target org are bundled and deployed, plus their -meta.xml companions and the project scaffolding a DX deploy requires. The local gates still stage and run the full suite — a smaller payload never means less proof. On a large org this turns an hours-long full validation into minutes: validate 3 changed classes, not 3,000 unchanged ones. Config equivalent: nimbus.release.<profile>.changedOnly=true. If source already matches the org, validate refuses with “nothing to release” rather than writing an empty receipt.
A changed-set receipt records a baseline: the org-side content digest of every component it will overwrite, captured at validation and covered by the receipt signature. At deploy time Nimbus re-retrieves exactly those components (member-precise, not whole types) and compares. If the org moved underneath the receipt — someone hotfixed a class directly in production after you validated — the deploy is refused with exit 27 and the drifted components are named. There is no silent overwrite: --override-drift lets source win for this one deploy, and the override is recorded on the receipt (outside the signed core, so the validation-time signature stays intact) and in the transition trail. Overriding is deliberately flag-only — it cannot be set in configuration.
Rollback — restore what a deploy overwrote
Just before every deploy, Nimbus captures the org's current content of the payload's components as a snapshot bundle beside the receipt (a changed-set deploy reuses the drift check's retrieve, so it costs no extra org round-trip; capture is best-effort and never blocks the deploy — a skipped capture is recorded in the transition trail). nimbus release rollback <receipt> sends that snapshot back through the assured pipeline — check-only Salesforce validation of the snapshot bytes, then deployment — as a new receipt chained via rollbackOf, in one step, with --confirm-production required on production.
Two limits are stated rather than papered over. Components the rolled-back deploy added are not deleted — the snapshot holds prior content, and a component that did not exist has none; removal is a destructive change and stays a separate, explicit step. And local gates do not run — the payload is the org's own previous state, not developer source, so the org-side validation is its oracle, exactly as when that state first shipped. Both appear in the receipt's transition trail.
Rollback carries its own drift guard: before restoring, Nimbus compares the org's current content of the snapshot's components against what the rolled-back deploy shipped. If the org moved after that deploy — a post-incident hotfix, say — restoring the snapshot would erase that change too, so the rollback refuses (exit 27, components named) unless --override-drift is given; the override is recorded on the receipt with the actor who granted it.
Signed receipts
A receipt states what was validated; a signed receipt also proves who validated it. nimbus release keygen creates an Ed25519 key: the private half lives in ~/.nimbus/release-signing.json (user-only permissions, never inside the project), and the public half is appended to .nimbus/release-signers.json in the project root. Commit that roster — adding or removing a release signer then goes through code review like any other change. If you gitignore .nimbus/ (for the db and receipts), commit the roster with git add -f .nimbus/release-signers.json; once tracked, git follows it normally. An uncommitted roster silently breaks verification for everyone else, so keygen prints the right command for your repo.
Once a key exists, every release validate signs the receipt's immutable core (digests, gates, validation result, org fingerprint, toolchain) automatically, and release deploy verifies signed receipts against the committed roster before anything is sent: a receipt whose signed core was modified after signing, or whose signer is not on the roster, is refused as an integrity violation (exit 25). Verification is fully offline — the receipt embeds the public key, the roster decides trust.
Signing is opt-in per project. Unsigned receipts keep working until a profile sets requireSigned=true, which refuses to deploy any unsigned receipt — the natural setting for a production profile once the team has keys.
Who did what: the signature answers who validated; deploy-time events carry their own actor. When a signing identity exists (a key, or NIMBUS_SIGNING_IDENTITY in CI), the transition trail records who started the deploy, who granted a drift override (driftOverride.by), and who ran a rollback. An optional --change-ref JIRA-1234 on validate/run/rollback links the receipt to the process that authorized it — inside the signed core, so stripping the ticket reference breaks the signature; promote inherits it with the bundle. Receipts still never carry org aliases, but the machine that ran a release keeps a local, non-portable sidecar (.nimbus/org-aliases.json) so release get can say staging instead of a fingerprint — labeled as a local alias, and absent on any other machine.
Key lifecycle: nimbus release keygen --revoke <keyId>marks a key untrusted (lost laptop, leaked CI seed, departed teammate). Revocation is conservative — verification refuses every receipt signed with a revoked key, whenever it was signed, because an unsigned timestamp cannot prove a signature predates the compromise. The entry stays in the roster: the revocation is itself part of the record. Rotate the CI secret, keygen the replacement, revalidate what must stay deployable.
Trust is visible where humans read the evidence, not only at deploy. nimbus release verify <receipt> re-hashes the bundle against the recorded digest and checks the signature against the roster, fully offline — a tampered bundle, a forged field, or an untrusted signer fails with exit25; an unsigned receipt passes with an explicit warning. release get runs the same checks and printsIntegrity: and Signature: lines on every receipt — an unsigned receipt is labeled UNSIGNED — this view is unverified evidence, so an evidence reviewer is never fooled by numbers the file merely claims. Every receipt also records expiresAt (10 days by default,maxReceiptAge to change), so an archived artifact answers “validate Friday, deploy Monday?” on its own.
The displayed signer comes from the roster, not the receipt's own signature.signer string — that string is unsigned, so verification resolves the name from the committed keyId→name binding and flags any mismatch. For gates that must fail closed, release verify --require-signed exits25 on an unsigned receipt instead of passing with a warning.
Two attestations: validation and deploy. The validation signature covers the facts fixed at validation time — gates, digests, change ref, target org, toolchain, baseline. A separate deploy counter-signature covers the terminal deploy outcome — who deployed, the final status, any drift override — chained to the receipt so it can't be lifted onto another release. release get and verify show both:Deploy: counter-signed by <name> when attested, ornot counter-signed (status and actor are recorded, not attested) otherwise. This closes the gap where a signed receipt's deploy status and actor could be appended without breaking the signature.
Counter-signing is per-profile: nimbus.release.production.requireCountersigned=truedemands it. When required, the deploy must carry a signing identity (a dev's key or CI's NIMBUS_SIGNING_KEY), and on a production target that identity must differ from the validator — segregation of duties, so authorization is distinct from execution. A production deploy by the same identity that validated is refused with exit 29; a second person must deploy.
nimbus release keygen # key + roster entry for git config user.email
nimbus release keygen --signer you@team.dev # explicit identity
nimbus release keygen --ci # also print the seed once, for a CI secret
# headless CI signs via environment
NIMBUS_SIGNING_KEY=<seed> NIMBUS_SIGNING_IDENTITY=release-bot nimbus release validate ...
# refuse unsigned receipts on production
nimbus.release.production.requireSigned=trueSalesforce test level
Leave salesforceTestLevel unset and Nimbus resolves it from the target org: RunLocalTests for production, which Salesforce requires, andNoTestRun everywhere else. Nimbus has already run the whole suite locally in seconds by that point, so having a sandbox re-run every test in the org pays twice for the same answer and is the most expensive thing a release does. The org stage on a sandbox is there to prove deployability against real org metadata, which needs no tests.
Set it explicitly to override. On Winter '26 and later,RunRelevantTests runs only the tests relevant to the deployed metadata — Salesforce measures it at roughly 93% faster thanRunLocalTests — and is the better production choice where the org supports it.
nimbus.release.production.salesforceTestLevel=RunRelevantTestsA validation that runs no tests cannot be quick-deployed, so Nimbus records no quick-deploy window for it and a later deploy re-sends the verified bundle.
Select a named profile with --release-profile. This is deliberately distinct from the global --profile, which activates a Quarkus-style%profile. configuration overlay.
Mismatch reporting
When Salesforce rejects the exact payload your local gates passed, and Nimbus classifies the rejection conservatively as a likely Nimbus compatibility gap — every reported problem looks like a compiler diagnostic Nimbus claims to cover, and no org- or project-dependency pattern matched — Nimbus can send a small, sanitized report so that gap becomes a permanent regression case. Org and project mismatches (missing dependency, managed package, coverage policy, org-only field) are never treated as Nimbus defects, and the deploy stays failed regardless of reporting.
A report contains only: the Nimbus and Salesforce CLI versions, the API version, the org kind (production/sandbox/…), the Salesforce test level, the classification, a dedupe fingerprint, the local-gate summary (validate/tests passed, tests run, coverage), and the rejection's error lines after they pass through the anonymizer. It never contains source code, org IDs, aliases or usernames, file paths, instance URLs, or record data — custom identifiers and paths are renamed while line numbers and standard platform vocabulary (Method does not exist, ApexClass) survive so the report stays diagnosable.
Reporting is opt-in. The mode is nimbus.release.<profile>.mismatchReporting=never|ask|always; ask is the default and only prompts in an interactive terminal — in CI, and inside a --json contract, ask behaves as never. --report-mismatch opts in for a single run and --no-report-mismatch always wins. When prompted, Nimbus prints the full sanitized record and the count of renamed identifiers before asking, defaulting to No. Every eligible record is always saved locally under .nimbus/releases/<releaseId>.mismatch.json for review, whether or not it is sent, and reporting never changes the release command's outcome or exit code. Set NIMBUS_REPORT_URL=disabled to switch submission off entirely regardless of configuration, or point it at your own endpoint for staging.
In the VS Code and IntelliJ release surfaces, the preview dialogis the prompt. When a validate or deploy fails this way, the editor offers to review an anonymized report; choosing to review shows exactly the sanitized record that would be sent, plus the count of renamed identifiers, and asks Send report / Save locally only / Cancel, defaulting to not sending. The daemon builds and sanitizes the record — the editor never sanitizes client-side and never sees raw problem text in the consent path — so what the dialog displays is byte-for-byte what submission would post. Both editors show the same wording and the same three choices.
Exit codes
| Flag | Default | Description |
|---|---|---|
20 | - | Configuration, toolchain, receipt, or preflight failure |
21 | - | Nimbus local validation, test, coverage, or mutation gate failure |
22 | - | Salesforce validation failure |
23 | - | Salesforce deployment failure |
24 | - | Production confirmation required or cancelled |
25 | - | Release integrity violation — the bundle is tampered, digest-mismatched, or missing (distinct from a config typo so CI can alert on tamper) |
26 | - | Deploy submitted to the org but its outcome could not be confirmed (lost contact) — the job runs on; resolve it with nimbus release watch <job>. NOT a deploy failure |
nimbus assurance Pro
Feature overview: the Assurance console.
The Assurance console: a self-hosted web view of every release's signed evidence, for the whole team — including the people who read releases but never open a terminal. Your code and receipts never leave your infrastructure: Nimbus hosts nothing, the console reads local receipts and is read-only, and it never contacts a Salesforce org and never deploys. The only outbound connection is Nimbus's own licence check on startup — no code, no receipts, no org data — the same as any Nimbus command.
nimbus assurance # open the console for this project
nimbus assurance --addr 0.0.0.0:8090 # share it with the team on your networkEach release shows, in plain language: who validated it, who deployed it, whether the checks passed (tests, coverage, static analysis), whether it drifted, its change reference, and — verified live in the browser — whether the bundle and signatures are intact. A dashboard summarises the set: production deploys, signed vs unsigned, counter-signed, and anything that failed verification. The verification is the same path release verify runs, so a tampered bundle or a forged signer name reads identically in the console and on the command line.
Access. On localhost the console is open — it's your machine. The moment you bind beyond loopback it requires an access token, so an evidence server reachable on the network is never wide-open: pass --tokenor set NIMBUS_ASSURANCE_TOKEN for a stable token, or let a network bind generate a per-session one and print a ready-to-open URL. The console (the page) is always served; only the evidence behind it is gated.
Evidence export and offline re-verification
nimbus release export writes the verified receipt set plus the signer roster into one self-verifying JSON document — the file you archive to your own immutable / WORM store for retention. It carries no source, no org aliases, and no credentials: it is evidence, not payload. The console offers the same as an export evidence download.
nimbus release export --org-kind production --since 2026-07-01 --output evidence-2026Q3.json
nimbus release verify-export evidence-2026Q3.json # re-check every signature, offline
nimbus release verify-export --require-signed evidence-2026Q3.json # CI gate: fail unless fully signed
nimbus release verify-export --roster .nimbus/release-signers.json ev.json # prove authenticity vs a trusted rosternimbus release verify-export re-checks every signature and counter-signature against the roster embedded in the export — fully offline, on any machine, years later. It needs no Nimbus licence: an auditor who holds the file can re-verify it with no vendor relationship (authoring an export stays a Pro workflow; re-checking one is free). It exits 25 if any signature fails. Bundle integrity isn't re-checkable from an export (it carries no bundles), but each receipt's signed core — what was validated, and by which key — re-verifies forever.
A bare run reports what it finds and only fails on a signature that verifies incorrectly, so an empty or all-unsigned file still exits 0. Add --require-signed to turn it into a gate: it fails (25) unless the set is non-empty and every receipt carries a valid signature — the form to wire into a retention-integrity check.
Internal consistency vs. authenticity
By default verify-export checks each signature against the roster embedded in the same file — it answers "are these signatures internally consistent?", which is necessary but is not proof of provenance: a file could carry its own roster. To prove authenticity, pin an external trust anchor you already trust — the project's committed .nimbus/release-signers.jsonvia --roster, or a specific --expect-signer <keyId>. Any signature by a key outside the anchor is reported and fails (25). The anchor, not the file, becomes the root of trust.
What a signature attests — validation, not the deploy
The validation signature covers the immutable core (digests, gates, target, toolchain) fixed at validate time — not the status, actor, or transition trail, which change afterward at deploy time. So a succeededstatus is only proven when a deploy counter-signature backs it; otherwise it is self-reported. verify-export and the console label a deploy that is not counter-signed as self-reported, so a green "signature verified" is never mistaken for proof of who deployed, or when. Turn on requireCountersigned to make the deploy attestable (and, on production, enforce a second identity — exit 29).
Reading a receipt as change-management evidence
A signed receipt maps directly onto the assertions a change-management review asks for — so an auditor can read one as evidence:
| Control assertion | Receipt evidence |
|---|---|
| Authorization | the change-ref (in the signed core) links the release to its ticket; the validation signature names who authorized it |
| Testing / quality gates | gates: tests, coverage, mutation score, static-analysis findings by severity — all in the signed core |
| Segregation of duties | the deploy counter-signature, by a different identity than the validator on production (exit 29 enforces it) |
| Integrity | the sha256 bundle digest and the Ed25519 signatures — verify-export re-checks them offline |
| Completeness | failed and overridden releases keep their own receipts; the transition trail is timestamped and, once signed, tamper-evident (any edit fails verify) |
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.
An explicit pattern that matches no tests is an error: the run reports No tests ran and exits non-zero, so a typo’d pattern can’t produce a green CI run. Running with no pattern on a project that has no test classes prints a warning and exits 0.
Intentional failures
A test class whose leading doc comment contains EXPECTED TO FAIL (case-insensitive) declares its failures deliberate — a demo fixture, a documented reproduction, an org-comparison probe. Those failures are tagged [expected], listed after the real ones, counted separately in the summary line (✗ 2 test(s) failed · 5 intentional demo failures), reported as expected_failures under --json, and excluded from the exit code. Anything else that fails still exits non-zero.
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. Under --json the selection comes back in the tests array with status "skipped" and summary.status "skipped", so a caller can enumerate a shard without scraping the human listing. |
--timeout | 60 | Per-test timeout in seconds (catches infinite loops). Covers class loading too, so a static initializer that never terminates fails the class — reporting every method — instead of hanging the run. |
--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. |
--evidence | false | Record what each line actually did - executions, SOQL queries, DML statements and database time - to .nimbus/evidence/last-run.json. Read it back with nimbus coverage lines. Collects line data without printing a coverage report. |
--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 |
--record | false | Forward managed-package static calls to the org and record what they return, into .nimbus/recordings/ (one file per test method). Requires an explicit -o and runs single-threaded. Re-recording a method replaces its file wholesale. The calls really execute in the org, so prefer a scratch or developer org. Recording intercepts calls to stub classes only — on a project without stubs/, use nimbus record, which pulls the stubs first. |
--no-replay | false | Ignore recordings in .nimbus/recordings/ and run the stub bodies instead. Replay itself needs no flag: recordings present means they are used. |
--org-default | - | Seed a custom setting org default before tests: ObjectName__c.Field=value (repeatable) |
-e, --exclude | - | Exclude paths matching a substring or glob — ** spans directories, e.g. **/dist/** (repeatable; persist via nimbus.test.exclude). Case-insensitive, like the positional pattern. Excluded paths are not compiled, so this removes code rather than only deselecting tests — to split a suite across jobs use --shard instead. |
-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). stdout carries the JSON document and nothing else — progress and warnings go to stderr, so the stream always parses. |
--trace | false | Enable execution tracing |
--trace-level | normal | Trace verbosity: minimal, normal, verbose, debug, system |
--trace-output | .nimbus/traces | Directory for trace output files |
--no-trace-on-failure | false | Do not record a replayable trace for failed tests. Persist via nimbus.test.trace-on-failure=false. |
Replay on failure
When a run has failures, Nimbus re-runs just the failed tests with tracing on and prints the command that reopens each one:
Failed Tests:
AccountServiceTest.testCreate
Assertion failed: expected 99, got 4
Class.AccountServiceTest.testCreate: line 12, column 1
replay: nimbus trace AccountServiceTest.testCreate --run 2026-08-26T02-08-59_5ba1f24eThis is on by default and costs nothing on a green run — nothing is recorded and nothing is written. Traces are recorded at verbose, the level the replay debugger needs to step line by line.
Tests whose class declares its failures deliberate are skipped, and a test that passes when re-run is reported as such rather than presented as a recording of the failure. With --json, each failed test carries trace_path, trace_run_id and replay, so an editor can offer "replay this failure" without guessing where the trace landed.
Retention is bounded: the last nimbus.test.trace-on-failure.keep automatic traces are kept and older ones pruned. Traces you recorded yourself with --trace are never pruned.
| Flag | Default | Description |
|---|---|---|
nimbus.test.trace-on-failure | true | Record a replayable trace for failed tests |
nimbus.test.trace-on-failure.keep | 3 | How many automatic failure traces to retain; oldest pruned |
nimbus.test.trace-on-failure.max-tests | 25 | Cap on how many failed tests get retraced in one run |
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.
validate also runs semantic analysis across your whole project’s type universe — resolving every type reference, variable, method call, and field against your classes, the Salesforce standard library, and your SObject schema. It flags things the parser accepts but the platform rejects at deploy: unknown types, undefined variables (including through the inheritance chain), unknown, wrong-arity, or wrong-argument-type methods and constructors (s.frobnicate(),add(1, 'two') against add(Integer, Integer),new Widget(1, 2) with no matching constructor — on the standard library and on your own classes), calling another class's private method, instance members from static context (and statics through an instance), duplicate fields, methods, and variables (including block-scope and parameter shadowing), constructing an abstract class, extending a non-virtual class, overriding a non-virtual method or omitting the override keyword, unimplemented abstract methods, reassigning a final field, missing returns and unreachable statements, break outside a loop, illegal assignments, returns, and ternaries (Integer x = 'hello';,List<Integer> xs = new List<String>();, assigning avoid call), non-Boolean conditions, catching or throwing non-exceptions, DML on non-records, iterating non-collections (and loop variables that can't hold the element type), impossible casts and always-false instanceof, incompatible comparisons, ! and ++ on non-numeric types, interface conformance (implementing every interface method — and notimplements-ing a class, or the same interface twice),switch rules (non-switchable expression types, duplicate and type-mismatched when values), SOQL bind variables and unknown columns, source-absent unnamespaced custom SObjects (checked-in SFDX and stubs metadata is authoritative), typed SObject field assignments, integer-only SOQL LIMIT/OFFSET binds, and unknown SOSL return fields,Datetime-to-Date narrowing, this()/super()constructor chaining, private field access across classes, protected members and@isTest methods in the wrong kind of class, abstract methods with bodies, access modifiers on interface methods, @future and@InvocableMethod shape rules, test-setup and web-exposure annotation contracts (@AuraEnabled, @RemoteAction, REST, andwebservice), illegal class, constructor, field, and method modifiers, annotation placement and context rules for @testSetup,@ReadOnly, and HTTP methods, REST URL mappings and per-verb method cardinality, property, nested-class, interface-method, and enum modifiers, circular self-inheritance, constructor-chain placement, generic type arity, inner-class nesting and static rules, abstract/static modifier combinations, catch ordering, property accessor visibility, global members in non-global classes, duplicate and unknown enum values, duplicate declaration scopes, protected/static field combinations, nested override visibility and return types, class-qualified instance calls, generic array shapes,@InvocableVariable field contracts, annotation property names,@SuppressWarnings arguments, duplicate and illegally modified method parameters, interface return and visibility contracts, classes extending interfaces, instance-initializer returns, construction of the built-in Exception, conflicting sharing modes, @AuraEnabled enums, private@RemoteAction methods, @TestVisible parameters, and unknown fields on your classes and SObjects. Method and conversion rules run against a catalog generated from the platform compiler's own symbol table and are verified against real check-only deploys — quirks likeDatabase.insert(records, false), ''.isBlank(), variables that shadow type names, methods named like their class, conversion-covariant collections, and keyword-less abstract-method implementations all resolve exactly as the org compiler would.
The current differential corpus catches 233 of 235 org-rejected error classes, each verified against a real check-only deploy, with zero intrinsic false rejects in the deployable-source gate. The two deferred cases both require managed-package context to decide whether an identifier may use @Deprecated.
Resolution is conservative by design: anything it can’t disprove — managed-package namespaces, inherited members, and unsynced field types — resolves rather than flags, so it never reports a deployable file. Every rule is gated on a corpus of thousands of real, deployable open-source classes before release. Findings are deploy blockers by default — like the platform, validate fails on code a check-only deploy would reject. Use --sema=warn to downgrade findings to warnings (e.g. while a stale schema sync is being refreshed), or --sema=off to disable.
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 --sema=warn # Downgrade semantic findings to warnings
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 matching a substring or glob — ** spans directories, e.g. **/dist/** (repeatable) |
-i, --interactive | false | Select package scope interactively |
--sema | error | Semantic analysis level: error (default, deploy-blocker), warn, or off. |
--json | false | Output results as JSON, including apiVersion and semantic 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 lint
The governor-safety and best-practice checks every Salesforce developer is trained to look for: SOQL, DML, async enqueues and callouts inside loops, hardcoded record Ids, swallowed exceptions, and seeAllData tests. Where nimbus validate asks whether the platform would accept the file, lint asks whether you want what it does.
Every other Apex linter is purely static, so every one of them says the same thing about a query inside a for: that it might run many times. Nimbus ran the code. Point it at a recorded run and the finding stops being a warning about the future and becomes a report about the past.
nimbus lint # every class and trigger
nimbus lint AccountService # files matching "AccountService"
nimbus lint *Service* # files matching "*Service*"
nimbus lint force-app/main/default/classes/ # everything under a path
nimbus lint --rules # what each rule protects against
nimbus lint --json # schema nimbus.lint/v1Without a run, it speaks in the conditional
AccountService.cls
5:35 warning soql-in-loop
SOQL query inside the for-each loop at line 4. Every iteration that
reaches it spends one of the transaction's 100 queries.
1 finding: 1 soql-in-loop in 2 files.
No recorded run: every message above says what the code MIGHT do.
Record one to see what it did: nimbus test "*" --evidenceWith one, it reports what happened
nimbus test "*" --evidence
nimbus lint
AccountService.cls
5:35 warning soql-in-loop
SOQL query inside the for-each loop at line 4. Run
2026-08-27T17-35-45_0031534d issued it 47 times, seen in
AccountServiceTest.testBulkInsert. A transaction may issue 100.A message never claims anything ran unless a recording covers that exact line. When a run covered the file and missed the line, the finding says so instead of guessing — untested code carrying a governor-safety defect is a sharper finding than the defect alone, and it is one no static linter can produce.
The rules
| Flag | Default | Description |
|---|---|---|
soql-in-loop | warning | A SOQL or SOSL literal, or Database.query / Search.query, inside a loop. A transaction may issue 100 queries. |
dml-in-loop | warning | insert / update / upsert / delete / undelete / merge, or the Database.* form, inside a loop. A transaction may issue 150 DML statements. |
async-in-loop | warning | System.enqueueJob, Database.executeBatch, System.schedule, or an @future method, inside a loop. Each names its own ceiling: 50 Queueable jobs or @future calls per transaction, 5 batch jobs at once, 100 scheduled jobs at once. |
callout-in-loop | warning | Http.send or Messaging.sendEmail inside a loop. A transaction may make 100 callouts and 10 email sends. |
hardcoded-id | warning | A genuine Salesforce record Id written as a literal in production code. Ids differ between orgs. |
empty-catch | warning | A catch block with no rethrow and no log. A comment does not reach the log. |
see-all-data | warning | @isTest(seeAllData=true). The result depends on whatever the org happens to hold. |
What the rules refuse to flag
A rule that fires on correct code is worse than a rule that does not exist, so each one is measured against ~23,000 Apex files from about forty real open-source Salesforce repositories before it ships. The whole corpus produces 188 findings, and the exclusions are as deliberate as the rules:
for (Account a : [SELECT ...])evaluates its query once, on entry. It is the bulk-safe idiom, not the defect, and it is the first false positive a naive implementation produces.for (Account[] batch : [SELECT ...])is handed 200 records at a time precisely so the body can do one DML per batch, sodml-in-loopstays quiet inside it.- A statement followed by an unconditional
breakorreturnruns at most once, so it is not one per iteration. hardcoded-idverifies the real 18-character checksum, or a key prefix the bundled describes actually list.AccountNumber12is the right length and the right character set and is not an Id.empty-catchandhardcoded-iddo not judge test code: an empty catch afterAssert.fail()is the success path, and a fabricated Id is how a unit test avoids a DML. The loop rules do judge tests, because a test transaction has the same 100 queries as any other.
| Flag | Default | Description |
|---|---|---|
[pattern] | * | Class-name glob, path prefix, or a single .cls / .trigger file. Omitted, it auto-scopes to the package subdirectory you are in. |
--severity | warning | Report and gate on findings at this level or above: error, warning, info. |
--disable | - | Turn one rule off by id, e.g. --disable empty-catch (repeatable). An unknown id is an error, not a no-op. |
--rules | false | List every rule with its severity and what it protects against, then exit. |
-e, --exclude | - | Exclude paths matching a substring or glob — ** spans directories (repeatable). |
--json | false | Emit the report on schema nimbus.lint/v1. The run block is present only when a recording was read, so a consumer can tell an evidenced report from a conditional one. |
Exit status is 1 when any finding survives the severity threshold, so nimbus lint can gate a build; 2means the command could not run. Files that do not parse are skipped rather than reported — judging a half-built tree is how a linter invents defects — and the count of skipped files appears in the summary.
The same rules appear in the editor as the nimbus/lint diagnostic family, with the rule id as the diagnostic code so you can mute one rule without muting Nimbus. They read the same recording, so the squiggle carries the same counts. Turn them off withnimbus.diagnostics.lint: false. They ship no quick fixes on purpose: flipping seeAllData to false hands back a broken suite, “log the exception” cannot know which logger the codebase uses, and the loop rules need a refactor rather than a text edit.
lint reads every class in the project, so it resolves an@future method called through another class. The editor, which cannot promise a current project index, deliberately under-reports that one case.
nimbus 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);"
# Ends in a statement, so it runs as written - nothing is wrapped
nimbus exec -c "Account a = new Account(Name = 'T'); insert a;"-c wraps the code in System.debug() only when it ends in a bare expression. Anything ending in a semicolon or a closing brace is already executable and runs verbatim.
| Flag | Default | Description |
|---|---|---|
-c, --code | - | Inline Apex code to execute |
nimbus run-method
Call one method of one class and report what it returned. Where nimbus exec runs a block of Apex you wrote, run-method takes a method that already exists and generates the invocation for you — including constructing the receiver, so an instance method needs no new of your own.
# Static method, no arguments
nimbus run-method AccountService.createDefault
# Arguments are Apex expressions, one --param each
nimbus run-method Calculator.add --param a=2 --param b=3
# Note the inner quotes: the value is Apex source, not a literal
nimbus run-method AccountService.byName --param name="'Acme'"
# Structured result, including per-line variable snapshots
nimbus run-method AccountService.createDefault --jsonWhether the method is static and what it returns are read from your project's source, so the argument is just Class.method — the last dot splits it, which keeps an inner class's qualified name intact (Outer.Inner.method).
Because a --param value is Apex source rather than a string literal, it can be anything the parameter accepts: a list, a new Account(Name = 'Acme'), or a call to a factory. Only the first = separates, so a value that is itself an assignment or a map literal survives intact.
Flags
| Flag | Default | Description |
|---|---|---|
--param | - | Argument as name=<apex expression>. Repeatable; pass them in declaration order. |
--json | false | Emit the structured result instead of the human summary |
--static | read from source | Override static detection: true or false |
--return-type | read from source | Override the return type read from source. Use void to discard the result. |
The two overrides exist for a method nimbus cannot resolve — one that lives in a managed-package stub, say. Resolution failure is an error on its own; pass both --static and --return-type and the call goes ahead anyway.
Output
The default output is the return value with its Apex type, any System.debug lines with the file and line that produced them, a count of the lines that executed and the files they were in, and the duration. A method that threw exits non-zero and prints the failure.
--json adds what the terminal cannot usefully show: variables carries a per-line snapshot of every local in scope (file, line, and each variable's name, value and type), and executedLines maps each file to the lines that ran. This is the same document the VS Code and JetBrains plugins render as inline annotations — the editors' "Run method" code lens and this command are one implementation.
The method runs against the local database with the same configuration nimbus exec uses — seeded rows, org defaults, and managed-package stub namespaces all present — but inside the same isolating transaction a test gets, and that transaction is rolled back when the command exits. Its DML does not survive the run, which is the difference between it and nimbus exec: you can call a method that inserts records without leaving them behind.
nimbus compare
Run the same thing locally and on a connected org, then diff the two results structurally. Three inputs: a snippet of anonymous Apex (-c), a SOQL query (-q), or a whole test selection (--tests). For the first two the verdict leads: identical, different, or which side errored.
# Compare a computation
nimbus compare -c "System.debug(Decimal.valueOf('1.5').round());"
# Compare a query against a specific org
nimbus compare -q "SELECT Name FROM Profile WHERE Name = 'System Administrator'" -o my-sandbox
# Compare a whole test class, then the whole suite
nimbus compare --tests AccountServiceTest -o my-sandbox
nimbus compare --tests "*" -o my-sandbox
# Machine-readable result
nimbus compare -c "System.debug(1+1);" --jsonWhat can never match is normalized out and every normalization is reported: record Ids (org and local Ids never agree, so Id values are excluded from comparison), audit fields (CreatedDate and friends), row order when the query has no ORDER BY (with one, order is part of the contract and is compared), and number/datetime formatting. Differences are typed — row counts, cells, columns, output lines, exceptions — and classified with a confidence: data, schema, engine, nondeterminism, or unknown.
The local side always rolls back — a compare never writes to the local database. The org side is a real org call, so anonymous Apex really executes in the org. A sanitized repro is written under .nimbus/differential/<timestamp>/.
Comparing a test run
--tests runs the selection on both sides and reports a verdict per test method:
match— both sides agree. If both failed, the normalized messages agree too.divergence— the two sides reached different outcomes. This is the one to look at.message-divergence— both failed, with different messages. Softer on purpose: Nimbus and Salesforce word the same assertion failure differently, so this bucket is expected to be noisy and is counted apart.org-only-error— the org produced no trustworthy outcome for that test (a class it could not compile, a run that never finished).missing-in-org/missing-in-local— one side never ran it.
An org test run can fail for reasons that say nothing about your code: an expired session, an exhausted API budget, a run still queued when Nimbus stops waiting. None of those is ever reported as a divergence. They are surfaced as environmental failures, in their own section, above the counts.
The org side is a real org test run: it takes as long as one normally does, and any data those tests create is created in the org. Failure messages are normalized before comparison — record Ids, durations, timestamps, whitespace — and every normalization is reported.
The selection has to be one the org can be asked for: *, a class name, a Class.method, or a comma-separated list of those. A wildcard inside a name (Account*Test) is refused rather than guessed at — running two different sets of tests and calling the difference a divergence would be worse than not answering.
The repro report
When tests disagree, a sanitized repro is written under .nimbus/differential/testrun_<timestamp>/ — the same pattern the snippet modes follow. It records what ran, both sides for the divergent methods only, the normalizations applied, the environmental block verbatim, and the org test run id so the run can be found again in the org. The org identity is masked to its alias; a full username is replaced outright, because a masked local part with an intact domain still names the org. A run where the two sides agreed writes nothing — there is nothing to reproduce. --no-report turns it off, and the path is printed when one is written.
Comparing coverage
--coverage adds a second table: each class's covered percentage on both sides and the gap between them, biggest gap first. The local side is the same collection nimbus test --coverage uses; the org side is sf apex ... --code-coverage. Percentages are recomputed from line counts on both sides, because the org's own figure is an already-rounded integer.
Coverage is a table beside the verdicts and never one of them. The two engines instrument different lines, so a gap is a fact to look at — not a disagreement about behaviour, and never a divergence. A side that could not report coverage says so and is never shown as 0%: this class has no coverage
and nobody told us about this class
are different findings. Classes only one side reported are listed after the table without a delta, and every exclusion the comparison applied is named.
--tests "*" asks the org for RunLocalTests, which is every unmanaged test in the org — a superset of this project's suite whenever the org carries classes you don't. Those are reported as missing-in-local and counted separately, not as divergences. Naming the classes is the precise way to ask.
| Flag | Default | Description |
|---|---|---|
-c, --code | - | Anonymous Apex to run on both sides |
-q, --query | - | SOQL SELECT to run on both sides |
--tests | - | Test selection to run on both sides ("*", "AccountTest", "AccountTest.testInsert", or a comma-separated list) |
--org-timeout | 30m0s | How long to wait for the org test run before giving up (--tests only) |
--coverage | false | Also compare per-class code coverage on both sides (--tests only) |
--no-report | false | Don’t write a repro report when tests disagree (--tests only) |
--json | false | Emit the full comparison result as JSON |
nimbus serve Pro
Start a local Salesforce-compatible API server. See the Local Server guide 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
Feature overview: local app hosting.
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 App preview target — React bundles, LWC components,
# Visualforce pages
nimbus app list
# The same listing as JSON, with each LWC package's component inventory
nimbus app list --json
# Generate a GraphQL SDL from your local SObject metadata (replaces
# "npm run graphql:schema" which requires a real org)
nimbus app schema > schema.graphql
# Or fetch the same SDL from the live server
curl http://localhost:4545/__nimbus/app/schema > schema.graphql
# Run the bundle's production build (npm run build, no deploy)
nimbus app build reactRecipes
# Run the bundle's own test suite (npm test)
nimbus app test reactRecipes
# Serve the built artifact instead of the HMR dev server (npm run preview)
nimbus app preview reactRecipes
# Score what the local LWC preview actually renders, per component
nimbus app scorecard
# The same run as a committable Markdown artifact
nimbus app scorecard --markdown > lwc-scorecard.mdAPI 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, 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 | 66.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 | --json | Print every App preview target nimbus can see — React UI bundles, LWC components and Visualforce pages. --json emits the same listing as a structured document, adding each LWC target’s component inventory (name, preview route, bundle files) and project-relative paths |
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). The bundler’s own preview server, unmodified — no SFDC_ENV injection, no Nimbus API proxy |
scorecard | --json --markdown --browser --serve --provenance-file --timeout | Score every LWC component the preview can see, tier by tier. --json emits the structured report; --markdown emits the committable artifact; --browser off skips the mount tier; --serve boots a local runtime so wire adapters and Apex calls reach real data |
build, test and preview are deliberately thin: Nimbus does not reimplement Vite or Vitest. Each discovers the bundle and runs that bundle's own package.json script (npm run build, npm test, npm run preview) in its directory, with stdio wired through to your terminal and its exit code passed back. What they add is the discovery and script resolution — a CI job runs nimbus app test and needs no bundle-aware logic of its own. A bundle with no package.json is an error rather than a silent no-op.
Their bundle selection is simpler than the dev server's: a named argument picks that bundle, a project with exactly one bundle needs no argument, and anything ambiguous is an error listing the candidates — there is no interactive picker on these three, TTY or not.
Note that preview starts the bundle's own preview server and nothing else. Unlike nimbus app, it does not stand up the Nimbus proxy, so the previewed build gets no SFDC_ENV injection and its /services/data/* calls do not reach the local runtime. It verifies that the optimised bundle builds and serves; verifying it against Nimbus's API surface still means running nimbus app.
scorecard measures the local LWC preview instead of describing it. For every component it can see, it records four tiers: whether the preview's own toolchain compiles the component, whether the real preview page mounts it in a headless browser, which platform data adapters (lightning/ui*Api, lightning/messageService, @salesforce/apex/*) it needs, and which other platform modules (@salesforce/*, lightning/*) it needs. The first two tiers are measured by running the real pipeline; the last two are classified statically against the renderer's own module tables, and the report says which is which rather than blending them. A component blocked at an unsupported platform import is reported as a declared boundary, not a defect — the renderer fails that import on purpose instead of returning invented platform data. Anything the machine cannot measure (no dependency cache, no headless browser) is marked unmeasured with the reason; it never becomes a pass, and Nimbus downloads nothing to produce a number.
--serve boots a local Nimbus runtime on an ephemeral port behind the measured page, so a mounted component's wire adapters and Apex calls reach the real interpreter and the embedded database instead of nothing. Without it a component that needs platform data still mounts — proving its modules loaded, not that its data path works — and the report says which of the two it measured. The run records what each mounted component actually rendered, which is the difference between a green row and evidence. It starts an embedded Postgres, so it is opt-in.
What the org-free renderer supplies. @salesforce/label/<ns>.<Name> and @salesforce/schema/<Object>[.<Field>] resolve at build time, the way the platform compiler resolves them: a label becomes the value your project's .labels-meta.xml declares, and a schema import becomes the same { objectApiName, fieldApiName } descriptor the platform emits. Neither reaches a server, in an org or here. A label your project does not declare fails by name rather than resolving to an empty string, and scoring a component set that has no Salesforce project behind it refuses labels outright — the report's environment block says which of the two applied.
lightning/uiRecordApi and lightning/uiObjectInfoApi are supplied too, wired to the UI API endpoints Nimbus already serves: getRecord, getRecords, getRecordCreateDefaults, getObjectInfo, getObjectInfos, getPicklistValues, getPicklistValuesByRecordType, the imperative createRecord/updateRecord/deleteRecord, and the pure helpers (getFieldValue, getFieldDisplayValue, the generateRecordInput* family). The records are real — rows in the embedded Postgres, written through the same handlers a React bundle uses, firing the project's own triggers. What is not there is the Lightning Data Service: no normalised cache, so two components wiring the same record issue two requests and a write does not re-emit to a sibling's wire. getRecordNotifyChange and notifyRecordUpdateAvailable exist only to invalidate that cache, so they are visible no-ops that announce themselves in the preview header rather than pretending to do something. The scorecard reports both modules as stubs for exactly this reason — the data is right, the behaviour is local.
@salesforce/apex/<Class>.<method> resolves to the same callable-adapter value the platform's compiler generates — one export that returns a Promise when called imperatively and provisions a property when used with @wire. The call reaches the local Apex interpreter and runs the project's own method against the embedded Postgres, returning what JSON.serialize would. Only methods annotated @AuraEnabled are reachable: anything else is refused by name, because a bridge that called any method on any class would be an arbitrary RPC into your source rather than a preview of Salesforce. A thrown Apex exception arrives as error.body.message / error.body.exceptionType, the shape an org sends for a controller call.
Two differences are reported rather than imitated. @AuraEnabled(cacheable=true) runs the method on every provision here — there is no Lightning Data Service cache to populate — and the response says so on X-Nimbus-Apex-Cacheable, which the preview surfaces as a header badge. If a cacheable method performs DML, something the platform refuses outright, Nimbus reports the statement count on X-Nimbus-Apex-Cacheable-Dml and lets the call stand instead of raising an exception whose exact org wording is not yet verified. refreshApex re-runs the request behind the value you hand it; with no cache there is no cross-component invalidation, and a value that did not come from an Apex wire is announced rather than silently treated as refreshed.
lightning/messageService and lightning/empApi remain blocked at compile time, as do the lightning/ui*Api families with no local endpoint behind them.
TypeScript components. A bundle whose class lives in <name>.ts is a component on the same terms as a .js one — discovered, previewed, hot-reloaded, and scored, with the scorecard's Lang column saying which language each row was written in. A .js beside a .ts resolves the way the platform compiler resolves it: .js wins. The bundle metadata stays <name>.js-meta.xml in both cases.
The renderer strips the type annotations ahead of the LWC compiler and pins two settings while doing it — experimentalDecorators: false and useDefineForClassFields: true — overriding whatever your tsconfig.json says. Both decide whether @api, @wire and @track are still decorators by the time the compiler sees them; with either the other way they are rewritten first and the component compiles, mounts, and silently has no public properties. Worth checking in your own build too: the failure has no error message.
The Pro gate is on nimbus app itself — the dev server and its Salesforce API surface. list, schema, build, test, preview and scorecard run on the free tier.
nimbus mcp
Start a Model Context Protocol server over stdio. AI coding agents (Claude Code, Cursor, and any other MCP-compatible client) call Nimbus's local test runner directly through structured tool calls instead of parsing CLI output.
The runner is initialised once at server start (database, project sources) and reused across tool calls. The agent disconnects when stdin closes; the server tears the runner down on exit.
The tool catalog, the setup files for each client, and the frozen span schema live in AI agents. This section covers the command itself. An agent can also get the catalog at runtime by calling nimbus_capabilities, which is always in step with the binary it is talking to.
Tools exposed: run_apex_tests, execute_anonymous, run_method, run_mutation_tests (Pro), get_test_failure, explain_failure, triage_failures, get_execution_trace, query_field_history (Pro), get_governor_usage, query, describe_schema, get_coverage, list_test_classes, query_graph, scaffold_metadata, nimbus_capabilities.
Operator visibility. The server logs each tool call to stderr in structured form (mcp.run_apex_tests.start, ...complete, ...timeout, ...failed) so you can tail the agent's MCP transport and see exactly what's happening. --verbose raises the log level to debug.
Does the verification loop actually help? That is an empirical question, so it gets an experiment rather than a claim. The Nimbus repository ships verifybench, a harness that measures an agent's fix rate on real failing Apex tests with and without this toolset, over a frozen, hashed task set. The harness and its methodology are available now; no registered run has been executed, so there is no result to report yet. We will publish the task set, the raw per-attempt records, and the report together, or not at all.
Resources exposed: nimbus://capabilities, nimbus://project, nimbus://parity, nimbus://coverage (and nimbus://coverage/{class}), nimbus://schema (and nimbus://schema/{object}). All read-only; they execute nothing and are never refused by a cap or by read-only mode.
Operator visibility. The server logs each tool call to stderr in structured form (mcp.run_apex_tests.start, ...complete, ...timeout, ...failed) so you can tail the agent's MCP transport and see exactly what's happening. A refused call logs too — mcp.refused.read_only or mcp.refused.budget. --verbose raises the log level to debug.
Register with Claude Code:
claude mcp add nimbus -- nimbus mcpFor a shared machine or CI, start it read-only — tests and Apex still run, but nothing they write survives the call and no files are written:
nimbus mcp --read-onlyPer-session ceilings (nimbus.mcp.max-test-runs-per-session, nimbus.mcp.max-apex-executions, nimbus.mcp.max-wall-clock) bound an unattended agent and are off until a project sets them. Full details under Safety caps.
Agentic Resource Discovery. Nimbus publishes an ARD catalog so compatible registries and coding agents can discover this MCP server from task intent. ARD only handles discovery: after selecting Nimbus, the client still installs the local binary and invokes nimbus mcp over stdio.
Stdio safety. Stdout is reserved for JSON-RPC framing. The runner writes nothing to stdout, and the process-global os.Stdout is rerouted to stderr for the server's lifetime so any stray prints elsewhere in the call tree can't corrupt the protocol stream. Use stderr for any out-of-band logging; --verbose is safe.
| 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. |
--read-only | false | Refuse any tool call that could leave a durable change behind — a committed write, or a file written into the project. Tests, anonymous Apex and method calls still run; they roll back. |
Recommended skills
MCP exposes the primitives; skills are short playbooks that tell an agent when and how to use them. Install with nimbus skills install — the binary fetches from nimbus-skills and writes to the right path for your agent. Three skills to start:
fix-failing-apex-test— the inner loop. Read failure → narrow → edit → re-run, until green.bootstrap-nimbus— set Nimbus up on a fresh SFDX project, including a CI snippet.apex-coverage-uplift— raise coverage by writing targeted tests for uncovered branches.
nimbus skills
Install agent skills from the nimbus-skills repo. Skills are short, opinionated playbooks (one per workflow) that pair with MCP to give your agent both the primitives and the recipes to use them. The binary embeds no skill content — every install fetches from GitHub at runtime, so the skills repo evolves independently of nimbus releases.
Subcommands:
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
nimbus sync -s WorkOrder -o myorg # Pull a standard object Nimbus doesn't ship a describe for-s also reaches objects Nimbus has never heard of. Standard-object describes ship with Nimbus, and there is no .object-meta.xml a project could write for a standard object — so when one is missing, naming it explicitly alongside -o describes it from your org and caches it. It is remembered, so later syncs keep it, and a release that adds the describe quietly takes over.
| Flag | Default | Description |
|---|---|---|
-s, --sobjects | - | Comma-separated list of objects to sync (others are left untouched). An object Nimbus has no describe for is fetched from the org named by -o. |
--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 Salesforce source — Apex, Lightning components, objects, fields, labels, custom metadata, and the admin surface from page layouts to approval processes — into your project's default package, following the standardforce-app/main/default/… layout. Every type writes its -meta.xml companion, and the API version comes from sourceApiVersion in sfdx-project.json.
nimbus new apex-class AccountService --test
nimbus new apex-trigger AccountTrigger --object Account --events before-insert,after-update
nimbus new lwc orderPanel --expose --targets lightning__RecordPage
nimbus new lwc typedPanel --ts # a .ts class module; automatic when lwc/ has a tsconfig.json
nimbus new custom-object Delivery_Area__c --label "Delivery Area"
nimbus new custom-field Zone__c --object Delivery_Area__c --type Text --length 80
nimbus new layout "Delivery Area Layout" --object Delivery_Area__c --field Name --field Zone__c
nimbus new lightning-page Delivery_Area_Record_Page --type RecordPage --object Delivery_Area__cTypes
| Type | Creates | Type-specific flags |
|---|---|---|
apex-class | .cls + -meta.xml | --template, --test |
apex-trigger | .trigger + -meta.xml | --object, --events |
lwc | .js or .ts, .html, .css, .js-meta.xml | --expose, --targets, --ts, --label |
visualforce-page | .page + -meta.xml | --controller, --scaffold-controller |
aura | .cmp, -meta.xml, controller, helper | — |
custom-object | .object-meta.xml | --label, --plural, --name-field-type, --display-format, --sharing-model |
custom-field | .field-meta.xml | --object, --type, plus the flags that type needs |
custom-label | a <labels> entry in CustomLabels.labels-meta.xml | --value, --categories, --language, --protected |
custom-metadata-type | __mdt .object-meta.xml | --label, --plural |
custom-metadata-record | .md-meta.xml | --type, --set, --protected |
permission-set | .permissionset-meta.xml | --label, --description |
field-set | .fieldSet-meta.xml | --object, --label, --field |
record-type | .recordType-meta.xml | --object, --label, --inactive |
validation-rule | .validationRule-meta.xml | --object, --formula, --error-message, --error-display-field |
list-view | .listView-meta.xml | --object, --label, --filter-scope, --column |
layout | .layout-meta.xml | --object, --field, --related-list, --highlights-panel |
lightning-page | .flexipage-meta.xml | --type, --object, --label, --template |
compact-layout | .compactLayout-meta.xml | --object, --label, --field |
quick-action | .quickAction-meta.xml | --type, --object, --target-object, --lwc, --page, --height |
tab | .tab-meta.xml | one of --object, --lwc, --aura, --page; --motif |
app | .app-meta.xml | --tab, --nav-type, --utility-bar |
path | .pathAssistant-meta.xml | --object, --picklist-field, --record-type, --inactive |
profile | .profile-meta.xml | --user-license, --description |
global-value-set | .globalValueSet-meta.xml | --picklist-value, --sorted, --label |
queue | .queue-meta.xml | --supports, --email, --label |
duplicate-rule | .duplicateRule-meta.xml | --object, --label, --inactive |
approval-process | .approvalProcess-meta.xml | --object, --label, --description |
email-template | .email-meta.xml + the .email body | --type, --subject, --folder |
sharing-rules | <Object>.sharingRules-meta.xml | named after the object |
assignment-rules | <Object>.assignmentRules-meta.xml | named after the object |
escalation-rules | <Object>.escalationRules-meta.xml | named after the object |
matching-rules | <Object>.matchingRule-meta.xml | named after the object |
The older Apex spellings still work: class, test, trigger, triggerhandler, batch, queueable and schedulable route to the same generator, and --sobject remains an alias for --object.
New metadata is queryable immediately
Objects, fields, labels and custom metadata are picked up by the next nimbus test without a sync. Nimbus rescans the project's own metadata on every run, so a field you created a second ago is already a column. nimbus sync is for pulling schema from an org, which is a different job.
Field types use the metadata vocabulary
--type takes the name the Metadata API uses in <type>, which is not always the name a describe call answers with. Rich text is Html, not RichTextArea; a multi-select picklist is MultiselectPicklist, not MultiPicklist. Pass a describe spelling and Nimbus tells you the metadata one rather than writing a field that silently behaves as text.
$ nimbus new custom-field Tags__c --object Account --type MultiPicklist
Error: "MultiPicklist" is a describe type name, not a metadata FieldType — use
"MultiselectPicklist" (metadata XML and describe results spell several types differently)Accepted: AutoNumber, Checkbox, Currency, Date, DateTime, Email, Lookup, MasterDetail, Number, Percent, Phone, Picklist, MultiselectPicklist, Text, TextArea, LongTextArea, Html, Time, Url.
Roll-up summaries (Summary), geolocations (Location), encrypted text (EncryptedText), hierarchical relationships (Hierarchy) and metadata relationships (MetadataRelationship) are creatable too — each takes the extra flags listed below. External and indirect lookups are not, and neither are formula fields: an external lookup's target exists only through an external data source, and a local project has none to offer or check against. Naming one tells you that, rather than treating it as a typo.
Flags
| Flag | Default | Description |
|---|---|---|
--dir | | Target directory, bypassing the standard layout. |
--api-version | sourceApiVersion | API version for -meta.xml companions (falls back to 62.0). |
--force | false | Overwrite existing files. Without it, a collision names the file and nothing at all is written. On a custom label it replaces that one entry and leaves the rest of CustomLabels.labels-meta.xml alone. |
--label | | Label. Defaults to a readable form of the API name. |
--description | | Description, where the type supports one. |
--template | class | Apex class shape: class, test, batch, queueable, schedulable, triggerhandler. |
--test | false | Shorthand for --template test. |
--object | | A trigger's SObject, or the object a field belongs to. |
--events | all seven | Trigger events, e.g. before-insert,after-update. |
--expose | false | Set isExposed on an LWC bundle. |
--targets | | LWC targets, e.g. lightning__RecordPage. Implies --expose. |
--ts | auto | Write the LWC class module as <name>.ts. Left off, a tsconfig.json in the destination lwc/ directory decides; --ts=false opts one component out. The bundle metadata is <name>.js-meta.xml either way. |
--controller | | Apex controller for a Visualforce page. |
--scaffold-controller | false | Also generate that controller class. |
--plural | | Plural label for an object. |
--name-field-type | Text | Record name type: Text or AutoNumber. |
--name-field-label | "<Label> Name" | Record name label. |
--sharing-model | ReadWrite | ReadWrite, Read, Private or ControlledByParent. |
--display-format | | AutoNumber sequence, e.g. ORD-{0000}. |
--type | | Field data type; for custom-metadata-record, the __mdt type. |
--length | per type | Length for Text, Text Area (Long) and Rich Text. |
--precision | 18 | Total digits for Number, Currency and Percent. |
--scale | per type | Decimal places for Number, Currency and Percent. |
--required | false | Mark the field required. |
--unique | false | Mark the field unique. |
--external-id | false | Mark the field an external ID. |
--picklist-value | | A picklist value. Repeatable. |
--picklist-restricted | false | Restrict the picklist to its value set. |
--reference-to | | Related object for a Lookup or Master-Detail. |
--relationship-name | | Child relationship name for a Lookup or Master-Detail. |
--delete-constraint | SetNull | Lookup delete behaviour: SetNull, Restrict or Cascade. |
--checkbox-default | false | Default a Checkbox to checked. |
--help-text | | Field help text. |
--summarized-object | | Roll-up: the child object being aggregated. |
--summary-foreign-key | | Roll-up: the master-detail field on the child pointing back at this object. |
--summarized-field | | Roll-up: the child field to aggregate. Not used by count. |
--summary-operation | | Roll-up: count, sum, min or max. |
--summary-filter | | Roll-up filter as "Field__c operation value". Repeatable. |
--location-notation | decimal | Geolocation: decimal or degrees. |
--mask-char | asterisk | Encrypted text: asterisk or X. |
--mask-type | all | Encrypted text: all, creditCard, ssn, sin, nino or lastFour. |
--controlling-field | | Metadata relationship: the EntityDefinition field scoping a FieldDefinition target. |
--field | | Field set member, in display order. Repeatable. |
--inactive | false | Create a record type or validation rule inactive. |
--formula | | Validation rule errorConditionFormula — true rejects the save. |
--error-message | | Validation rule message shown when it fires. |
--error-display-field | the record | Field to show a validation error on. |
--filter-scope | Everything | List view scope: Everything, Mine, Queue, Delegated, MyTerritory, MyTeamTerritory or Team. |
--column | | List view column, in display order. Repeatable. |
--related-list | | Layout related list, e.g. Contact or Delivery__c.Account__c. Repeatable. |
--highlights-panel | false | Show the highlights panel on a layout. |
--target-object | the owning object | Object a quick action creates or updates. |
--lwc | | Lightning web component a tab or quick action shows. |
--aura | | Aura component a tab shows. |
--page | | Visualforce page a tab or quick action shows. |
--motif | Custom53: Bell | Tab icon. |
--height | | Quick action height in pixels. |
--tab | | App navigation item, in navigation order. Repeatable. |
--nav-type | Standard | App navigation: Standard or Console. |
--utility-bar | | Lightning page of type UtilityBar. |
--picklist-field | | Picklist whose values are a path's steps. |
--record-type | __MASTER__ | Record type a path applies to. |
--sorted | false | Sort a global value set's values alphabetically. |
--supports | | Object a queue can own records of. Repeatable. |
--email | | Queue notification address. |
--user-license | Salesforce | Profile user license. |
--folder | unfiled$public | Email template folder. |
--subject | | Email template subject line. |
--value | | Label text (required for custom-label). |
--categories | | Label categories. |
--language | en_US | Label language. |
--protected | false | Mark a label or custom metadata record protected. |
--set | | Custom metadata record value as Field__c=value. Repeatable. |
--list | false | List every type and the inputs it takes, then exit. |
--json | false | Emit the result as JSON (with --list, the capability data). |
Data types are the Metadata API vocabulary, not the one a describe call answers with — rich text is Html, a multi-select picklist is MultiselectPicklist, encrypted text is EncryptedText. A describe spelling is refused with the metadata name given, rather than written into a file that would silently behave as text.
Summary, Location, EncryptedText, Hierarchy and MetadataRelationship are generated. ExternalLookup and IndirectLookup are not, and the refusal names the reason: both need a referenceTo pointing at an external object, which exists only through an external data source — a local project has none, so there is nothing to offer and nothing to validate a typed name against. Write those two by hand.
The four object children — field-set, record-type, validation-rule and list-view — require an object that already exists, in the project or on the platform; a name matching neither is refused rather than written one directory deep. A child lands beside its own object, so an object in a second package directory keeps its children there.
Nimbus evaluates validation rules during local DML, and skips any rule whose formula it cannot read. Creating a rule therefore parses its errorConditionFormula first and warns when the formula engine cannot — the rule still deploys and still fires in the org, but local runs let the save through, so a green suite would not mean what it looks like.
--list --json is what the IntelliJ plugin reads to build its New menu, so its dialogs always describe the CLI you actually have installed.
nimbus fixture
Feature overview: test data.
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. 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 packageRequired lookups are generated, not left as TODOs
When an object has a required lookup, Nimbus follows it: the parent object is added to the factory automatically (even if you did not name it), methods are emitted parents-first so they can actually be inserted in order, and a create<SObject>WithParents() method inserts the parents and wires the child to them.
$ nimbus fixture LoyaltyTransaction__c
static Account createAccount() { ... }
static LoyaltyTransaction__c createLoyaltyTransaction__c() {
return new LoyaltyTransaction__c(
Points__c = 1,
Reason__c = 'Purchase'
);
}
// Inserts the required parents, then returns an un-inserted record wired to them.
static LoyaltyTransaction__c createLoyaltyTransaction__cWithParents() {
Account parent0 = createAccount();
insert parent0;
LoyaltyTransaction__c record = createLoyaltyTransaction__c();
record.Account__c = parent0.Id;
return record;
}A required lookup to the object's own type has no root to generate from, so it becomes an explicit TODO rather than infinite recursion. Lookup cycles between two objects are reported as a warning instead of being silently broken. A required parent with no local schema also becomes a named TODO (plus a warning to run nimbus sync) — never a call to a create method that was not generated, which would produce a factory that does not compile.
Values the schema vouches for
Picklist fields use a real value from the field's value set, not a placeholder — a restricted picklist rejects anything else, so a fabricated value produces a factory that always fails on insert. When the local schema has no value set, the field is marked unresolved rather than guessed. The generated class is deterministic: the same project always produces the same file, so it diffs cleanly.
| 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 coverage lines
What a line cost, not whether it ran. A coverage report says line 42 was executed. This says line 42 ran 47 times, issued 47 SOQL queries — 47 of the 100 a transaction gets — and spent 1.2 seconds in the database, and that AccountServiceTest.testBulk is the test that did it.
The counts are the governor's own: a query is attributed to a line from the same call that charges it against Limits.getQueries(), so the per-line numbers and the transaction's usage cannot disagree. Record with --evidence, read with this.
nimbus test "*" --evidence
nimbus coverage lines AccountService
nimbus coverage lines force-app/main/default/classes/AccountService.cls --all
nimbus coverage lines AccountService --jsonBy default only lines that issued a query or wrote a row are listed — those are the lines a limit is spent on. Database time is attributed to the outermost line that opened the operation, so an insert accounts; reports the time its triggers spent too; counts are not rolled up that way, so a query inside a trigger is counted on the trigger's own line.
| Flag | Default | Description |
|---|---|---|
--all | false | List every line the run reached, not only the ones that queried or wrote. |
--json | false | Emit the per-line evidence as JSON (schema: nimbus.lineevidence/v1). |
nimbus budget Pro
Per-test ceilings on the governor counters, enforced as a CI regression gate — a performance budget for Apex. The platform's own limits are a cliff at 100 SOQL and 150 DML: a test sits at 94 queries for a year, someone adds a loop, and the failure arrives in production. A budget is the line you draw well below that cliff and defend on every pull request.
Five counters are budgetable: soql, soql_rows, dml, dml_rows and cpu_ms. All five are machine-independent. Four are exact counts; cpu_ms is Nimbus's virtual CPU clock, which counts operations rather than wall time — the same test produces the same number on a laptop and on a CI runner, which is what makes a CPU budget checkable at all.
nimbus budget init # measure the suite, write nimbus.budgets.json
nimbus budget check # re-measure, exit 1 if any test is over budget
nimbus budget check --json # machine-readable result for CIThe budget file
nimbus.budgets.json at the project root. nimbus budget init writes it; it is meant to be committed and read in code review.
{
"version": 1,
"defaults": {
"soql": 50,
"dml": 30
},
"tests": {
"OrderServiceTest.*": { "soql": 20, "dml_rows": 60 },
"OrderServiceTest.testBulkOrder": { "soql": 5 }
}
}Keys under tests use the same wildcard syntax as nimbus test <pattern>, matched against ClassName.methodName. An unknown counter name is a hard error, not a silently ignored key.
Which budget applies
Resolution is per counter and layered, most specific first:
- Every
testspattern matching the test, ordered by specificity: an exact test name (no*) beats any wildcard; otherwise the pattern with more literal characters wins; ties break on the pattern string ascending. - The
defaultsblock.
The first level that names the counter wins, and counters it does not name keep falling through. So OrderServiceTest.testBulkOrder above takes soql: 5 from its exact entry and dml_rows: 60 from the class wildcard. A counter no level names is unbudgeted for that test and is never reported. Every violation line names the key its budget came from, so you know which line of the file to edit.
Adopting budgets
nimbus budget init runs the suite and writes measured usage plus headroom.defaults gets the suite's worst observed value per counter — the ceiling a newly written test must stay under — and every measured test gets an entry at its own usage plus headroom. The per-test entries are what actually catch a regression: a single test doubling its queries would still sit far below the suite-wide default. Derived budgets are raised to a small per-counter floor, so a test that issues one query is not broken by its second, and clamped to the platform limit, so no budget is written that could never fire.
What is measured
A test's usage is what Limits reports when the test method ends. Test.startTest() resets the governor counters and Test.stopTest() restores them — platform behaviour, which Nimbus reproduces exactly — so work inside that block is not counted, and a budget on such a test covers only what it does outside the block. Both commands say so out loud when they see it, rather than letting a suite look green while budgeting nothing.
Tests that fail or are skipped are reported but not judged: a test that threw on its third line stopped consuming counters there, so its usage says nothing about what it costs when it works.
Exit codes
| Flag | Default | Description |
|---|---|---|
0 | — | Every measured test is within budget. |
1 | — | At least one budget was exceeded. This is the CI gate. |
2 | — | Configuration problem: no budget file, an invalid one, or a run in which nothing could be measured. |
nimbus budget check flags
| Flag | Default | Description |
|---|---|---|
--pattern | * | Tests to measure (same syntax as nimbus test). |
--file | nimbus.budgets.json | Budget file path. |
--json | false | Emit the result as JSON instead of the human report. |
-p, --parallel | nimbus.test.parallel | Test workers. The counters are deterministic, so this affects speed only. |
nimbus budget init flags
| Flag | Default | Description |
|---|---|---|
--pattern | * | Tests to measure (same syntax as nimbus test). |
--file | nimbus.budgets.json | Budget file path to write. |
--headroom | 20 | Percent of slack to leave above measured usage. |
--defaults-only | false | Write only the suite-wide defaults block, no per-test entries. |
--force | false | Overwrite an existing budget file. |
-p, --parallel | nimbus.test.parallel | Test workers. |
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 tier
nimbus whoami --json # Structured status for editor and tooling integrationsLicenses are validated online and cached locally; a 7-day offline grace period is built in for outages and travel.
Re-running nimbus login is always safe: if nothing changed it says so, and if your account's license changed since this machine last signed in, the machine is moved to the current license automatically.
nimbus whoami --json returns plan, activation state, a shortened machine identifier, capability lists, and account URLs. It never returns a license key or full machine identifier.
Headless / CI: set NIMBUS_LICENSE_KEY in the environment instead of running nimbus login. Team plans include a dedicated CI runner license with its own machine pool.
nimbus 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 which database this project will use, what decides that database's connection, and which org the org-facing commands will reach. For a health check with fixes attached, use nimbus doctor instead.
nimbus status # Human-readable summary
nimbus status --json # The same facts, machine-readable--json
Three objects with stable lowercase keys. project carries found and path; database carries provider, url_source (flag, environment or embedded) and data_dir for the embedded provider; org carries target and source (flag, config or none).
The connection URL is never included, only what decides it — a URL can carry credentials and this output is meant to be safe to paste into an issue. When no org is configured, target is empty rather than the placeholder the human-readable output prints.
{
"project": { "found": true, "path": "/work/my-project" },
"database": { "provider": "embedded", "url_source": "embedded", "data_dir": ".nimbus/db" },
"org": { "target": "my-sandbox", "source": "config" }
}nimbus doctor
Run diagnostic checks on the current Salesforce project. Reports setup problems - missing configuration, no synced schemas, broken database, unknown config keys - with a concrete fix for each issue. Think brew doctor or gh auth status.
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 |
Permission set coverage | Permission sets and groups your tests resolve by name are defined in source or synced from the org | 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 |
--suite | false | Also run the test suite and report failure clusters, largest cause first |
--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 org doctor
Report which Salesforce login Nimbus found, and which org operations it performs natively versus through the Salesforce CLI.
The native org engine
The org operations Nimbus makes most often go straight to the Salesforce APIs, using the login sf org login web already wrote to your sfdx auth store. No Node process starts, and Nimbus works on a machine whose Salesforce CLI is missing, broken, or mid-upgrade.
| Operation | How |
|---|---|
Anonymous Apex (nimbus exec -o, nimbus compare -c, org probes) | Apex SOAP executeAnonymous, debug log included |
SOQL against an org (nimbus soql -o, nimbus compare -q) | REST query, every page followed |
Org test runs (nimbus compare --tests) | Tooling runTestsAsynchronous + ApexTestResult |
Apex source (nimbus metadata retrieve, nimbus test --fetch-missing) | Tooling query on ApexClass / ApexTrigger |
Deploying a class or trigger (nimbus metadata deploy) | Tooling MetadataContainer + ContainerAsyncRequest |
Retrieving metadata (nimbus metadata retrieve with a wider manifest) | Metadata API retrieve + checkRetrieveStatus, unzipped to source format |
Deploying metadata (nimbus metadata deploy of a directory or manifest) | Metadata API deploy + checkDeployStatus, zipped from source format |
| Listing what an org holds (the editors' org browser) | Metadata API describeMetadata and listMetadata |
The Metadata rows carry a boundary, and that boundary decides which of your commands is fast. Source format keeps most types in a single file, and those Nimbus converts and moves itself. It does not keep an object in one file — a Salesforce DX project splits an object across per-field, per-record-type and per-list-view files, and reassembling those is the Salesforce CLI's job. Workflows, sharing rules, Lightning and Aura bundles, and anything held in a folder are the same story. nimbus org doctor names the count and the exclusions on those rows rather than reporting a bare "native":
Capabilities
execute anonymous Apex native
SOQL query native
run org tests native
read org test results native
retrieve Apex source native (Apex classes and triggers by name, over the Tooling API)
deploy a class or trigger native (single Apex files, over the Tooling API's container flow)
retrieve metadata native (29 metadata types; objects, workflows, bundles and folder contents retrieve via Salesforce CLI)
deploy metadata native (29 metadata types; objects, workflows, bundles and folder contents deploy via Salesforce CLI)
list org metadata native (describeMetadata and listMetadata, every type the org exposes)The split is per call, not per component: a manifest naming even one excluded type runs through the Salesforce CLI whole. Two retrieves are two snapshots of an org that changed in between, and two deploys are two transactions, which makes rollback-on-error a promise a pair of them cannot keep.
Everything else — the nimbus deploy assurance cycle, org creation, package operations, nimbus sf passthrough — still runs through the Salesforce CLI, which stays a supported dependency.
Fallback, never replacement
Every native call falls back to the exact Salesforce CLI invocation it replaced if anything goes wrong — no login, an expired refresh token, an org that will not answer. You get one line saying so, once per capability:
native org call failed (SOQL query: ...), using sf CLINimbus never runs an OAuth flow of its own. If there is no sfdx login it says which command creates one, rather than authenticating an org that sf would not be able to see.
The Salesforce CLI encrypts the tokens in its auth store, so Nimbus reads the same key from your OS credential store (Keychain on macOS, libsecret on Linux, or the CLI's key.json where neither exists). If that key is unavailable — a locked keychain, a restricted CI runner — nimbus org doctor reports the tokens as encrypted (no key available) and every call falls back to the Salesforce CLI, which reaches the credential store through its own bindings. Nimbus will not send a secret it could not decode.
Reading the report
nimbus org doctor # the default target org
nimbus org doctor -o my-sandbox # a specific org
nimbus org doctor --probe # also make one live call to prove reachability
nimbus org doctor --json # the same report, structuredEverything except --probe is answered from local files, so this works offline and on an org whose session has expired. Access and refresh tokens are never printed — the fields shown are the ones sf org list already displays. The command exits non-zero when there is no usable login, or when --probe was passed and the org did not answer.
| Flag | Default | Description |
|---|---|---|
--probe | false | Make one live call to confirm the org answers |
--json | false | Emit the report as JSON |
-o, --org | default target org | Which authenticated org to diagnose |
Turning it off
Set org.native=false in nimbus.properties. Every org call then goes through the Salesforce CLI exactly as it did before — the fallback path is the old behaviour, so nothing else changes.
# nimbus.properties
org.native=falseTracing
NIMBUS_ORG_NATIVE_TRACE=1 logs a redacted summary of every native call to stderr: method, endpoint, status, timing and body size. Never a request body, never a query string's contents, and never a token.
NIMBUS_ORG_NATIVE_TRACE=1 nimbus soql -o my-org "SELECT Id FROM Account LIMIT 1"nimbus org status
Report whether the org's copy of your files changed since you took them. This is the read-only half of nimbus metadata deploy's conflict detection: same registry, same query, nothing written and nothing deployed.
nimbus org status # everything tracked for the target org
nimbus org status AccountService # one class
nimbus org status --json # the same rows, structuredSync base — my-sandbox (00D5f000004XXXXEAA)
.nimbus/org-base.json — this checkout only, not for git
ApexClass AccountService current synced 2026-08-26 09:15
ApexClass ContactService local edit synced 2026-08-25 14:00, edited since
ApexClass OrderService ORG NEWER changed 2026-08-26 11:02 by Dana Ruiz
3 entities tracked, 1 newer in the org — `nimbus metadata deploy` will refuse those.| Flag | Default | Description |
|---|---|---|
current | — | The org is where you left it, and so is your file. |
local edit | — | The org is where you left it; your file has changed since. A deploy will land. |
ORG NEWER | — | Someone saved it in the org after you took it. A deploy is refused without --force. |
gone | — | The org no longer has it. A deploy would recreate it. |
A project with no rows has simply never retrieved or deployed through Nimbus — that is the brownfield starting state, and a deploy in it warns once and proceeds. The command exits non-zero when any row is ORG NEWER, which makes it usable directly as a pre-push check.
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the rows as JSON |
--org, -o | required | Org to ask |
Distinct from nimbus org doctor on purpose: doctor answers "can Nimbus talk to this org natively" from local files and works offline, while this asks the org about data and needs a live connection.
nimbus agent (experimental)
Test an Agentforce agent's action surface locally — the actions, never the planner. nimbus agent list reads the agent's declared actions from your source (both GenAi metadata generations and Agent Script) and shows how each resolves against this project; nimbus agent test executes them through the local runtime with per-action results, governor cost, and rollback by default. An action whose declared inputs don't match its Apex request wrapper is refused by name — a correspondence nothing org-side checks.
nimbus agent list --coverage answers the other question: which actions has no local test reached? Each action is marked observed — a coverage run recorded a test executing its target's file — or static, meaning no run recorded anything and a test merely reaches the target through source references. The two are never merged into one "covered" column: one is a measurement and the other is a reading, and only the first is evidence. An action nothing reaches at all is marked NONE; one with no local target is marked n/a rather than uncovered, because nothing here could ever cover it. Reaching a target is also not the same as testing the action — the input contract is nimbus agent list's job and executing it is nimbus agent test's.
nimbus agent list # every action, and how each one resolves
nimbus agent list --coverage # ...and what no local test has reached
# One action, with the inputs the planner would have supplied at runtime.
nimbus agent test Support_Agent --action Submit_Case --template
nimbus agent test Support_Agent --params inputs.jsonExperimental: the shape of these files and flags may still change.
What nimbus will not test
This list is the shape of the product, not a backlog. Read it before the rest of the section, because everything below is only worth as much as this is honest.
| Not tested | Why not |
|---|---|
| The planner | No LLM call, no prompt evaluation, no topic selection, no conversation. Which action fires for an utterance belongs to Salesforce's stochastic runtime, and a local verdict on it would be unfalsifiable — you could not tell whether nimbus was wrong or the model rolled differently. Nimbus tests the actions, never the planner. |
| Standard and platform actions | Record CRUD, knowledge lookups, draft email. Your repository does not contain them. An action a topic references with no GenAiFunction file in the project is listed and refused, never skipped quietly. |
| External Services and API actions | The spec is local, the endpoint is not. Refused by name — unsupported-target-type: nimbus executes apex:// and flow:// only. |
| Prompt templates | An LLM call by definition, and nimbus calls no LLM. |
| Custom Lightning Types | A c__caseInput parameter's shape lives in a Lightning Type Bundle nimbus does not read. --template emits an empty object there and says so, rather than inventing fields. |
| Example inputs | Agent metadata carries no example values, anywhere. That is a platform fact rather than a gap, and it is why fixtures exist — see below. |
Conversation fixtures
Agentforce metadata declares an action's input shape and never an example value — the planner fills inputs from the conversation at runtime, so there is nothing on disk to replay. A fixture file is where those inputs live, which is what turns a one-shot CLI invocation into something a pull request can be red about.
# agent-tests/support-agent.agenttest.yaml
schema: nimbus.agenttest/v1
agent: Support_Agent
actions:
Submit_Case:
cases:
- name: high priority escalates and opens a case
inputs:
subject: Printer is on fire
priority: High
expect:
result:
- path: $[0].escalated
equals: true
debug:
- contains: "subject=Printer is on fire"
db:
- soql: SELECT Id FROM Case WHERE Subject = 'Printer is on fire'
count: 1
governor:
maxSoql: 2
maxDml: 1
# A refusal is not a failure. Pinning the code makes it a regression test.
Mismatched_Action:
cases:
- name: an input the request wrapper does not carry
expect:
outcome: refused
refusal: apex-input-not-on-request-wrappernimbus agent test --fixtures # every **/*.agenttest.yaml
nimbus agent test --fixtures ./agent-tests # one directory
nimbus agent test --fixtures --case "Submit_Case/high priority escalates"
nimbus agent test --fixtures --results-xml results.xml --coverage-report cov.jsonEvery case runs in its own transaction and is rolled back — nothing a fixture does is ever committed. Failures print first, each naming every assertion that did not hold and the exact command that reruns that one case.
What a case can assert
| Key | Asserts |
|---|---|
result | The return payload, by path. One of equals, contains, matches, exists, type per entry. |
debug | System.debug output. Passes when any line matches. |
db | Your own SOQL, run inside the action's transaction before the rollback. One of count, min, max, exists. |
governor | Cost ceilings: maxSoql, maxSoqlRows, maxDml, maxDmlRows, maxCpu. |
outcome | pass (default), fail (the action throws), refused (the resolver declines it), resolves (checked against local source, not run). |
result paths are a deliberately small JSONPath subset — $, $.field, $.a.b, $[0], $[0].field. Wildcards, recursive descent, filters and slices are refused by name: each matches a setrather than a value, and an assertion whose subject is "some element, possibly none" passes for reasons you did not write down.
Governor limits are ceilings, not equalities. An assertion that a method issues exactly three queries breaks on every unrelated refactor; one that it issues no more than three catches the loop that turned into an N+1.
Database assertions have to run inside the action's own transaction, because the writes are never committed — a query issued anywhere else would see an empty database and every one of them would pass by reporting zero. Your SOQL is run verbatim, never rewritten: a rewritten query is a different assertion from the one in the file.
Importing a Salesforce test-spec
If you already maintain a sf agent generate test-spec YAML, a fixture can read it. Nimbus maps the one thing in it that can be checked locally — the actions the planner is expected to reach — and refuses the rest by name, printed on every run.
schema: nimbus.agenttest/v1
import: ./support-agent-test-spec.yamlEach named action becomes a case that checks it against local source without running it: the target still exists, and its declared inputs are still fields on the request wrapper. That second check is the one no other tool in the ecosystem makes — the org validates that invocationTarget names something in the org; it cannot tell you whether the action's inputs still match the Apex in this working copy. The spec carries no input values, so nimbus does not invent any: a fabricated input set produces a green case that tested a value nobody chose.
| Spec expectation | Not mapped, because |
|---|---|
utterance | The planner's input. Nimbus does not select actions from natural language. |
expectedTopic, topic_sequence_match | Topic selection is the planner's, and stochastic. A local verdict would be unfalsifiable. |
expectedOutcome, bot_response_rating | Natural language scored by an LLM judge. Nimbus calls no LLM. |
coherence, conciseness, completeness, factuality, instruction_following | LLM-judged quality metrics. |
output_safety, output_pii_leakage | LLM-judged safety metrics. |
output_latency_milliseconds | A timing of Salesforce's hosted runtime. A local action's duration is a different measurement. |
contextVariables | Conversation context from the running session. Give the action its values with inputs: instead. |
subjectVersion | Pins an activated agent version in the org. A local run tests the source in this working copy. |
Those rows are the boundary above, applied key by key: every planner-side expectation is refused by name and printed on the run, never approximated into a local verdict.
CI
A fixture run emits the same JUnit XML and coverage reports a test run does, through the same writers — so anything that already consumes a nimbus report consumes this unchanged. The GitHub Action picks fixtures up automatically and adds one section to the same PR comment, failures first. A repository with no fixtures pays nothing and the comment says nothing about agents.
- uses: nimbus-solution/nimbus/.github/actions/nimbus-test@v1
with:
license-key: ${{ secrets.NIMBUS_LICENSE_KEY }}
agent-fixtures: auto # or a path, or falseFlags
| Flag | Default | Description |
|---|---|---|
--fixtures | — | Run conversation fixtures. Bare, it searches the project for **/*.agenttest.yaml; give a file or directory to narrow it. |
--case | — | Run only cases whose "Action/Case name" contains this text. This is what the reproduce line in a failure uses. |
--action | — | Single-action mode: run just this action with --params. |
--params | — | JSON file holding one action's inputs, or an object keyed by action name. |
--template | false | Write a fill-in-the-blanks input template from the declared schema instead of running. |
--results-xml | — | Path to save JUnit XML for the fixture run (Pro). |
--coverage | false | Collect code coverage across the fixture run. |
--coverage-report | — | Path to save the coverage report; .json, .xml (Cobertura) and .html are detected from the extension. Implies --coverage. XML is Pro. |
--json | false | Emit the run as JSON (schema nimbus.agenttest-run/v1). |
--trace | false | Record an execution trace for each action. |
--trace-output | .nimbus/traces | Directory for trace output files. |
nimbus org diff
Show where this project and the org disagree — what exists only in the org, only here, and what both have with different bodies.
nimbus org diff # the default target org
nimbus org diff -o my-sandbox # a specific org
nimbus org diff --type ApexClass,ApexTrigger # narrow it
nimbus org diff --all # list the matching components too
nimbus org diff --json # the whole tree, structuredFive states, not three
Every component comes back as one of five states. The fifth is what makes the other four worth trusting.
| State | Means |
|---|---|
← org only | The org has it and this project does not |
→ local only | This project has it and the org does not |
≠ differs | Both have it and the bodies are not the same |
= in sync | Both have it and the bodies match |
? undetermined | Something is in the way of a verdict, and the row says what |
A row is undetermined when the org will not hand over a body in the same call that listed the name — a static resource's binary, an Aura or LWC bundle's files, source a managed package hides from a subscriber — or when this project defines the same API name in two package directories, in which case there is no single local copy to compare. Nimbus says so rather than guessing. A wrong "differs" costs you a diff; a wrong "in sync" is how a stale class ships.
What is compared, and how
Bodies are compared after trailing whitespace is removed from each line and from the end of the file, and after nothing else — the two differences a round trip creates on its own. No other normalization is applied, deliberately: every additional rule makes an "in sync" verdict easier to produce and less true.
| Type | Compared |
|---|---|
ApexClass, ApexTrigger | Presence and body |
ApexPage, ApexComponent | Presence and markup |
StaticResource | Presence only |
AuraDefinitionBundle, LightningComponentBundle | Presence only |
CustomLabel | Presence only |
Managed-package members are excluded. A subscriber org cannot hold their source, so every one of them would report as org-only forever.
What it does not cover
Objects, fields, layouts, flows, permission sets, profiles, record types, validation rules and the rest of the long tail are not looked at. Those cannot be listed over the Tooling API at all — they need the Metadata API's listMetadata call, which this build does not make. The command prints them by name on every run, and the JSON carries them in pendingTypes, so a clean report is never mistaken for a complete one.
A type the org refuses to list is reported undetermined with the org's own error, never as empty. "The org has none of these" and "the org would not tell me" are different answers, and the second one must not silently turn this project's components into local-only rows.
Bounded output
An org with a few packages installed answers a listing with four figures, so the tree lists at most 200 components per type and counts the rest. The cut is stated on the row that replaces them. Components are ordered drift first, so the cap can only ever hide rows nobody needed to see; the listing behind it is never truncated, because a component dropped before the comparison would come back as a local-only verdict rather than as a missing row.
Matching components are counted but not listed unless you pass --all. The answer to "is my project current" is the rows that are not.
| Flag | Default | Description |
|---|---|---|
--type | every listed type | Only these metadata types, comma-separated |
--limit | 200 | How many components to list per type before counting the rest; -1 for all |
--all | false | List the components that match too, not only the ones that drifted |
--json | false | Emit the drift tree as JSON |
-o, --org | default target org | Which org to compare against |
JSON
The payload is pinned by a schema field and carries its own caveats: supportedTypes is every type this build can list, pendingTypes is every type it cannot, and limits states what the run could not establish. Every array is present even when empty.
nimbus org diff --json | jq '.types[].entities[] | select(.status == "bothDiffer") | .name'{
"schema": "nimbus.orgdiff/v1",
"org": "my-sandbox",
"summary": { "orgOnly": 1, "localOnly": 0, "bothSame": 142, "bothDiffer": 2, "undetermined": 4 },
"types": [
{
"type": "ApexClass",
"status": "listed",
"counts": { "orgOnly": 1, "localOnly": 0, "bothSame": 142, "bothDiffer": 2, "undetermined": 0 },
"total": 145, "shown": 3, "hidden": 142, "hiddenInSync": 142, "hiddenCapped": 0,
"entities": [
{
"name": "AccountService",
"status": "bothDiffer",
"localPath": "/projects/acme/force-app/main/default/classes/AccountService.cls",
"orgId": "01p000000000001AAA",
"localHash": "27ff45bbc204",
"orgHash": "154c010ef21f",
"actions": ["retrieve", "deploy", "open"]
}
]
}
],
"pendingTypes": [
{ "type": "CustomObject", "reason": "typeNeedsListMetadata", "detail": "..." }
],
"limits": ["Managed-package members are excluded. ..."]
}actions names only the operations Nimbus judged unambiguous for that row — retrieve when the org has it, deploy and open when exactly one local file backs it. A name defined in two package directories gets neither, because "which file" has no answer.
In the IDE
The IntelliJ plugin renders the same tree on the Drift tab beside the Org browser, with Retrieve and Deploy on the rows that support them. Both hand off to the same paths the editor's right-click menu uses, so the git overwrite guard and the local Apex check before a deploy behave exactly as they do elsewhere.
nimbus schema
Open a visual schema explorer showing synced SObjects, their fields, relationships, and field types. Useful for verifying your local database matches your org metadata.
nimbus schemaMachine-readable schema
--json skips the TUI and writes the full schema to stdout: every table with its columns (name, dataType, isNullable, isPrimary, isReference), its inferred foreign keys in both directions (references and referencedBy), and its rowCount. Tables are sorted by name so two dumps of the same database diff cleanly.
# The whole schema
nimbus schema --json
# Which tables point at Account?
nimbus schema --json | jq -r '.tables[] | select(.references[]?.toTable == "account") | .name'
# Row counts, largest first
nimbus schema --json | jq -r '.tables[] | "\(.rowCount)\t\(.name)"' | sort -rnThe document is built through a runner rather than read straight off the connection, so the tables your project's metadata implies exist before they are described — the same document the editor plugins read from the daemon, so a script and an IDE see one schema.
Piping without --json is unchanged: it still prints the plain table list, because nimbus schema | grep Account is a table-name search and answering it with JSON would break every script that does it.
Flags
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the full schema (tables, columns, relationships, row counts) as JSON instead of opening the TUI |
nimbus soql
Run a SOQL query and print the rows. By default it queries this project's local database — the same one your tests run against, seeded the same way — so you can check what a test will actually see before you write the assertion.
# Query the local database
nimbus soql "SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%'"
# Show the PostgreSQL it translates to, without running it
nimbus soql --preview "SELECT Id, Account.Name FROM Contact WHERE CreatedDate = LAST_N_DAYS:7"
# Ask a real org the same question
nimbus soql -o my-org "SELECT Id, Name FROM Account LIMIT 5"
# Structured output for scripts and editors
nimbus soql --json "SELECT Id, Name FROM Account" | jq '.rows[].Name'The local mode runs through the same runner configuration nimbus exec builds — seeded rows, org defaults and managed-package stub namespaces included. A console that answered differently from an @isTest method would be worse than no console at all.
Three modes, one query
- Local (default) — executes against the embedded Postgres and prints an aligned grid with a row count and timing. No box drawing and no colour: the output is meant to survive a pipe into
greporawk. --preview— translates the query to PostgreSQL and prints the SQL without running it. The fastest way to understand why a query returned what it did: you see the joins a relationship field produced and the window a date literal resolved to. It describes the local translation, so it cannot be combined with an org.-o <alias>— forwards the query to that org instead, so the same command answers "and what does the org say?". It goes straight to the org's REST API (seenimbus org doctor) and is bounded by a two-minute timeout, so an expired session fails with an error rather than blocking forever on a login prompt.
Flags
| Flag | Default | Description |
|---|---|---|
--preview | false | Print the translated PostgreSQL instead of running the query (local only) |
--json | false | Emit the structured result as JSON |
--org, -o | unset | Run the query on this org instead of locally |
--json carries columns, rows, count, the generated sql, the resolved object and the elapsed time. A failed query emits the document with an error field and exits non-zero — one thing to parse, and the verdict on the exit code rather than on whether output appeared.
nimbus cache
Manage the parsed AST cache. Nimbus caches parsed Apex classes to speed up subsequent test runs.
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 resetThis is the destructive one. If you only want the rows gone — schema, daemon and synced metadata intact — use nimbus data reset instead and skip the nimbus sync that this makes necessary.
nimbus data
Operate on the rows in the local database, as opposed to the database itself (nimbus db) or the whole project state (nimbus reset).
# Empty every table; keep the schema, the daemon and the database
nimbus data reset
# No prompt (required when stdin is not a terminal)
nimbus data reset --yesThe prompt needs somebody to answer it, so a script, a cron job or a CI step has to pass --yes. Without it the command refuses and says so — including under < /dev/null, which looks like a terminal to the usual check and used to fail as though someone had declined.
nimbus data reset vs nimbus reset
The distinction is the point of the command, and reaching for the wrong one costs a nimbus sync.
| Flag | Default | Description |
|---|---|---|
nimbus data reset | non-destructive | Truncates every table. The schema stays, the daemon keeps running, the postgres process keeps running, and .nimbus/ is untouched. Only the rows go. |
nimbus reset | destructive | Stops the daemon, kills the database process, and deletes the entire .nimbus/ directory — including the synced schema and the AST cache. |
Seed rows (profiles, users, ApexClass records) are cleared along with everything else, but every run re-creates them before it executes anything — so the next nimbus test starts from a clean standard org rather than a broken one. Use it when scratch data has accumulated into a state you no longer trust.
Flags
| Flag | Default | Description |
|---|---|---|
--yes, -y | false | Skip the confirmation prompt. Required when stdin is not a terminal (CI, a script). |
Rows are deleted, so it asks first — the same rule the release commands apply to a production deploy: a non-interactive caller must have said so on the command line, because there is nobody there to ask.
nimbus data branch
Keep more than one state of the local org at once. A data branch is a copy of this project's local database, made with a PostgreSQL CREATE DATABASE … TEMPLATE inside the cluster Nimbus already runs for the project. At the size a local org reaches that is a file copy — tens to a few hundred milliseconds — and switching between branches changes one word in a connection string.
# Copy the current data into a new branch and switch to it
nimbus data branch experiment
# Copy a specific branch rather than the current one
nimbus data branch --from default hotfix
# What exists, what it costs, which one is in use
nimbus data branches
# Move between them
nimbus data switch default
nimbus data switch experiment
# Remove one (refuses the branch in use)
nimbus data branch --delete experimentWhat comes along
Everything in the database: tables, rows, the seed records a run creates, and any per-worker schemas the test runner built. There is no re-seed, no re-sync and no org round trip — the copy is of the database as it stands.
What this is not
This is local dev data, not a migration tool and not a backup. Branches live inside .nimbus/db with everything else, so nimbus reset deletes them along with the rest of the local database, and so does a daemon shutdown that tears the database down. Nothing here versions your schema, moves data between orgs, or survives a machine. It exists so you can run the destructive test and still have the state you had before it.
Branch names
Lowercase letters, digits, - and _, starting with a letter or digit, up to 40 characters. default is the project's original database: it is where nimbus data switch default goes back to, and it is not a branch you create or delete.
The daemon
PostgreSQL will not copy or drop a database while a session is connected to it, and the daemon holds one for its whole life. So a branch or a switch asks the daemon to move: it closes its pool, performs the operation and reopens on the result, in one round trip.
If a separate Nimbus process is mid-run against the branch being copied or deleted, the command refuses rather than severing it. If the daemon cannot be moved for some other reason, the switch still takes effect for everything started afterwards and Nimbus says so — a test run detects the mismatch and connects to the right branch itself rather than silently reading the previous one.
Flags
| Flag | Default | Description |
|---|---|---|
--from <branch> | current branch | Copy this branch instead of the one in use. |
--delete <branch> | — | Delete a branch. Refuses the branch currently in use and refuses default. |
Following the git branch
Off by default. Turn it on and Nimbus creates and switches to a data branch matching the checked-out git branch whenever it changes, printing one line to say what it did:
# nimbus.properties
nimbus.data.branch.follow-git=true| Flag | Default | Description |
|---|---|---|
nimbus.data.branch.follow-git | false | On project open, create and switch to a data branch matching the git branch when it has changed. |
The git branch name is lowercased and anything outside the allowed characters becomes a hyphen, so feature/NIM-42 becomes feature-nim-42. It is evaluated when a test run starts and when the daemon starts — the two moments that genuinely are opening the project. A detached HEAD names a commit rather than a branch and is left alone, and deleting a git branch never deletes its data.
It is off by default deliberately: a checkout silently moving your local org is a surprise nobody asked for, and every branch you touch costs another copy of the database on disk. nimbus data branches shows what that adds up to.
nimbus 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 metadata
Move metadata between the org and this project: pull what a run could not resolve, push files, a directory or a manifest back.
# Retrieve everything the last run reported missing
nimbus metadata retrieve -o my-org
# Machine-readable summary
nimbus metadata retrieve -o my-org --json
# Push a class, a directory, or a manifest
nimbus metadata deploy AccountService -o my-sandbox
nimbus metadata deploy force-app/main/default/permissionsets -o my-sandbox
nimbus metadata deploy --manifest manifest/package.xml -o my-sandboxHow it travels
Both directions go through Nimbus's own Salesforce APIs where they can, and hand the identical sf invocation to the Salesforce CLI where they cannot. What decides which is one question: does source format keep the type in a single file?
| Selection | Transport |
|---|---|
| Apex classes and triggers by name | Tooling API — one round trip, no CLI process |
| Permission sets, permission set groups, profiles, layouts, Lightning pages, flows, labels, tabs, apps, quick actions, custom metadata records, custom permissions, static resources, Visualforce pages and components, named credentials, remote site settings, queues, groups, value sets, path assistants, duplicate rules, approval processes, message channels, notification types | Metadata API — retrieved and deployed natively, converted to and from source format |
| Objects and their fields, record types, validation rules, workflows, sharing and assignment rules, Lightning and Aura bundles, reports, dashboards, documents, email templates, single custom labels | Salesforce CLI — reassembling these from source format is its job |
The split is per call, not per component: a manifest naming even one type from the third row goes to the Salesforce CLI whole. Two retrieves are two snapshots of an org that changed in between, and two deploys are two transactions — which makes rollback-on-error a promise a pair of them cannot keep.
nimbus org doctor prints the exact boundary for your org, and org.native=false in nimbus.properties sends everything through the Salesforce CLI as before.
nimbus metadata retrieve
Every run that hits a class, object, field, custom metadata type or label Nimbus has no source for reports it and writes the whole list to manifest/nimbus-missing.xml. This command reads that manifest and retrieves the items — and then stops. Not re-running the tests is the difference between it and nimbus test --fetch-missing.
Transport follows the table above. An Apex-only manifest comes back over the Tooling API and lands in the same files at the same paths, each with its -meta.xml companion. A manifest naming the single-file types comes back over the Metadata API as a package and is converted to source format on the way to disk. A manifest naming an object, a field or a single label goes through a single sf project retrieve start --manifest call instead.
Object and field schemas are separate again: they come back as REST describes written to .nimbus/schemas/, because that is what the runtime actually reads.
An org is required and must be explicit — -o <alias> or a default target-org. Run the tests again afterwards: retrieving one class often reveals the next missing reference behind it, so a project with deep unresolved chains takes a few rounds.
Flags
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the retrieval summary (retrieved, failed, errors) as JSON |
--org, -o | required | Org to retrieve the missing metadata from |
Progress goes to stderr, so --json owns stdout and a plain run still shows what is happening during a retrieve that can take a minute. Any failed item exits non-zero.
nimbus metadata deploy
Push files, directories or a manifest into an org. An argument is a file path, a directory, or a bare Apex name resolved against the project's package directories.
nimbus metadata deploy AccountService -o my-sandbox
nimbus metadata deploy force-app/main/default/classes/AccountService.cls -o my-sandbox
nimbus metadata deploy AccountService AccountServiceTest -o my-sandbox
nimbus metadata deploy force-app/main/default/permissionsets -o my-sandbox
nimbus metadata deploy --manifest manifest/package.xml -o my-sandbox
nimbus metadata deploy AccountService -o my-sandbox --check-onlyA handful of .cls or .trigger files keeps the Tooling API's metadata-container flow — the same one the Salesforce IDE plugins use to save a file — because that is the path that reports the Apex compiler's own errors with line and column:
Deploy failed: 1 component error.
AccountService (line 7, column 13): Variable does not exist: bogusAnything wider goes over the Metadata API as a package, and anything the third row of the table above names goes to the Salesforce CLI with the identical invocation. Only a bare name is Apex-specific: other types' member names are not unique across types — an "Account" is a layout, a tab and a profile — so pass a path for those.
A deploy rolls back as a unit, so one component's error means none of them landed. Any component error exits non-zero.
This command is the inner loop, not the release: it is for the twenty seconds between changing a file and wanting it in a sandbox. For the full assurance cycle — staged release, validation, tests, receipt — use nimbus deploy instead.
Conflict detection
A deploy replaces the org's copy, and the org does not ask whether yours was derived from theirs. So before anything is pushed, Nimbus asks the org one question: has this class changed since this working copy last took it?
It can answer that because every time Nimbus writes a file from an org — a retrieve — or pushes one to it, it records the org's LastModifiedDate, who saved it, and a hash of the bytes involved. That record is the sync base, and it lives in .nimbus/org-base.json, keyed by org id. It describes this working copy, so it belongs outside version control — committing it would hand a teammate a claim about their files that was never true of them.
There are four outcomes, and only one of them stops anything:
| Flag | Default | Description |
|---|---|---|
Base matches the org | silent | Nobody has touched it. The deploy runs and says nothing about it. |
Org changed after your base | refused | Names who changed it and when, and exits non-zero. --force overrides. |
No base recorded | one warning | Nimbus has never retrieved or deployed it, so it can't tell. The deploy runs. |
Your file already matches | skipped | The bytes are already in the org. Nothing is deployed, and it exits zero. |
Refusing to deploy: the org's copy is newer than yours.
AccountService changed in the org on 2026-08-26 11:02 by Dana Ruiz, after your last retrieve.
Deploy anyway with --force, or take the org's copy with:
nimbus sf project retrieve start -m ApexClass:AccountService -o my-sandboxBrownfield projects start with no bases at all, which is why a missing one warns rather than refuses:
No sync base for AccountService — nimbus can't tell whether the org copy changed. Deploying anyway.That warning is self-limiting: a successful deploy records a base, so from the first one onward the file is checked. Warnings and notices go to stderr, so --json still owns stdout — under --json a refusal is a single document with "status": "Refused" and the same sentence in message.
Three deliberate limits. --check-only is never refused — it saves nothing, so it reports the conflict and compiles anyway. A check that cannot run (unreachable org, no login, org.native=false) prints one line and lets the deploy through; conflict detection is never a reason a deploy fails. And a class the org does not have yet is a create, so it is silent. Use nimbus org status to read the bases without deploying anything.
Flags
| Flag | Default | Description |
|---|---|---|
--check-only | false | Validate without saving, to see the errors without changing the org |
--dry-run | false | Alias for --check-only, matching the Salesforce CLI's spelling |
--force | false | Deploy even when the org's copy changed after your last retrieve |
--manifest, -x | | Deploy the components a package.xml selects, instead of named paths |
--json | false | Emit the deploy result as JSON, in the Salesforce CLI's deploy shape |
--org, -o | required | Org to deploy into |
nimbus rename
Rename a custom field or a custom object and update every place this project refers to it — Apex, SOQL, metadata XML, LWC, Aura and Visualforce — with a preview you approve before anything is written.
# Preview. Nothing is written.
nimbus rename field Account.Legacy_Code__c Account_Code__c
# Write the changes
nimbus rename field Invoice__c.Amt__c Amount__c --apply
# Rename an object: its references, its directory, and every file named after it
nimbus rename object Invoice__c Bill__c --apply
# The plan as JSON, for an editor to drive
nimbus rename field Account.Legacy_Code__c Account_Code__c --jsonThe preview is the product. It lists every file, every line, old above new, and the reason that line was believed to mean your field. Nothing is written without--apply, and an apply is all-or-nothing: every file is re-hashed against the plan first, and one file that changed underneath aborts the whole thing before a byte is written.
What gets updated, and why each one is safe
| Attribution | What it means |
|---|---|
qualified | The source spells the owner out: Account.Old__c, Schema.Account.Old__c, @salesforce/schema/Account.Old__c, <field>Account.Old__c</field>, or a new Account(Old__c = …) initialiser. |
path-scoped | The file is that object’s own metadata: objects/Account/…, layouts/Account-…, workflows/Account.…. |
soql-from | The occurrence sits in a query clause whose FROM names the object — including the field read off a query’s result, [SELECT F__c FROM Obj].F__c. |
relationship | It sits behind a Lookup__r whose lookup points at the object. |
sole-owner | Exactly one object in this project declares a field by that name, so a bare mention has one possible meaning. |
definition | The component’s own metadata: the field-meta.xml, the object-meta.xml, and the file or directory carrying the API name. |
type-position | Object renames only: a slot only a type can occupy — new Foo__c(), List<Foo__c>, FROM Foo__c, (Foo__c) x, Foo__c.SObjectType. |
What it will not decide for you
A field and the object it looks up routinely share an API name —ServiceDelivery__c.ProgramEngagement__c pointing atProgramEngagement__c. So a field rename never touches a slot only a type can occupy, and an object rename never touches a slot only a field can occupy. Everything else that mentions the name and cannot be attributed is listed as an unresolved candidate for you to check by hand — never silently skipped, and never silently edited. A query assembled by string concatenation is the common case:'SELECT ' + fields + ' FROM Account' names no field this reading can attribute, so the fragment is reported.
Every plan also carries what it cannot see: Apex comments are ignored entirely, receiver types are resolved from declarations in the same file only,stubs/ is skipped because those API names belong to an installed package, and the whole thing covers your source — the org-side rename that moves the data and preserves history is still Setup → Object Manager.
nimbus rename field
The field is named with its object, because a bare field name is not a field: two objects can both declare Status__c, and which one you mean decides which mentions in your source are yours. The new name may be given bare or with the same object prefix.
nimbus rename object
Renames a custom object, custom setting or custom metadata type, along with the directory its metadata lives in and every file whose name leads with the API name — its layouts, its tab, its quick actions, its workflow, its object translations.
| Flag | Default | Description |
|---|---|---|
--apply | false | Write the changes. Without it nothing is written. |
--yes | false | Skip the confirmation prompt. Required with --apply when there is no terminal. |
--json | false | Emit the plan as JSON (schema: nimbus.rename/v1) - the same payload an editor drives a rename from: target, per-file changes with byte offsets and a SHA-256 digest, unresolved candidates, skipped mentions, and the limits. |
nimbus record
Set up managed-package record/replay in one command: pull the packages' stubs from an org, then run the matching tests with recording on. After it finishes, plain nimbus test replays the recorded answers offline — no org, no flag.
nimbus record -o my-org # pull stubs, then record the whole suite
nimbus record -o my-org MyTestClass # pull stubs, then record one class
nimbus test # from now on: replays offlineIt is exactly nimbus stub pull followed by nimbus test --record, which both remain available individually — stub pull when you want the package's shape without running anything, test --record to re-record once stubs exist. The pull is additive (project metadata and existing stub files are left alone), so re-running after a package upgrade is safe; recording re-records each matched test method wholesale.
| Flag | Default | Description |
|---|---|---|
--org, -o, --target-org | required | Org to pull stubs from and forward recorded calls to. Must be explicit — the calls really execute there, so prefer a scratch or developer org. |
--namespaces | every namespace found | Comma-separated managed-package namespaces to pull. |
--data | false | Also record org data during the pull: custom-setting rows into nimbus.properties, custom-metadata records into stubs/customMetadata/. |
--force | false | Overwrite stub files that already exist (project metadata is never overwritten). |
Recordings land in .nimbus/recordings/, one JSON file per test method, and are meant to be committed — the team and CI then run against the same answers with no org access. Details of the record/replay mechanics are in the stub documentation.
nimbus stub
Scaffold and inspect stubs/ — the directory Nimbus loads before the main source tree so tests can reference managed-package classes, custom objects and fields, and custom labels that aren't in the project. See also the User Stubs / Managed Packages section.
List existing stubs
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.
Scaffold a custom label
nimbus stub label writes a label into stubs/labels/ so Apex that reads it resolves to a real value instead of an empty string. Unnamespaced labels land in CustomLabels.labels-meta.xml; a managed-package label lands in <ns>.labels-meta.xml, where the file name is what declares the namespace — the metadata format has nowhere to record another package's namespace, and Label.<ns>.<Name> is the only legal Apex spelling for one.
# Unnamespaced → stubs/labels/CustomLabels.labels-meta.xml
nimbus stub label Welcome_Message --value "Hello there"
# Either form is accepted for a managed-package label
nimbus stub label npe03.RecurringDonationStageName --value Pledged
nimbus stub label RecurringDonationStageName --namespace npe03 --value Pledged
# Several at once (value defaults to the label name)
nimbus stub label Alpha Beta Gamma --namespace npe01| Flag | Default | Description |
|---|---|---|
--namespace | - | Managed-package namespace; selects stubs/labels/<ns>.labels-meta.xml. A ns.Name argument sets it too. |
--value | the label name | Label value. Applies to a single label, so pass one name at a time when using it. |
Re-running is safe: a label that already exists in the file has its value updated in place instead of being duplicated. Apex then reads Label.npe03.RecurringDonationStageName or System.Label.Welcome_Message as usual.
Pull labels, fields, and classes from an org
nimbus stub pull reads an org that has the managed packages installed and writes the parts your project is missing into stubs/. Labels come from the Tooling API's ExternalString entity — the only place another package's labels are readable — fields come from SObject describes, and classes come from ApexClass.SymbolTable, which exposes the exact global surface of an installed package: every method overload, constructor, property, inner class, and enum. It needs the Salesforce CLI (sf) on PATH and an authenticated org.
# Everything the org has that the project doesn't
nimbus stub pull --org dev
# Just the packages you care about
nimbus stub pull --org dev --namespaces npe01,npe03,npo02
# See what it would do first (prints every sf command it runs)
nimbus stub pull --org dev --namespaces npe03 --dry-run
# Class stubs for one package, without running anonymous Apex
nimbus stub pull --org dev --namespaces Nebula --classes-only --no-exec
# Narrow the describes to specific objects
nimbus stub pull --org dev --objects Contact,Opportunity --fields-only| Flag | Default | Description |
|---|---|---|
--org, -o, --target-org | default target-org | Org alias to read. Fails with the Salesforce CLI's own message if the alias is unknown. |
--namespaces | every namespace found | Comma-separated managed-package namespaces to pull. |
--objects | discovered from the project | Describe only these SObjects instead of deriving the list from the project. |
--dry-run | false | Query the org and print what would be written without touching disk. |
--force | false | Overwrite stub files that already exist. Project metadata is never overwritten. |
--labels-only | false | Pull custom labels only — no describes, so it finishes in one round trip. |
--fields-only | false | Pull fields and objects only. |
--classes-only | false | Pull managed-package classes only. |
--no-exec | false | Skip the anonymous Apex that records global constant values; constants stay null. |
--data | false | Also record org data: custom-setting rows as seed lines in nimbus.properties, custom-metadata records into stubs/customMetadata/. |
--data-only | false | Record org data only (implies --data). |
What gets written. stubs/labels/<ns>.labels-meta.xml for labels, stubs/objects/<Obj>/fields/<ns__Field__c>.field-meta.xml for namespaced fields on objects your project references, stubs/objects/<ns__Obj__c>/<ns__Obj__c>.object-meta.xml plus its fields for namespaced objects the project uses but does not define, and stubs/<ns>/<Class>.cls for every global class of the pulled namespaces.
Class stubs record the org's behavior, not a guess. Signatures are the org's own, so the overloads your tests call resolve the way the package declared them. Bodies are stubbed with usable defaults: builder methods hand back a constructed object instead of null, collection returns come back empty instead of null, and enums keep their declaration order so ordinals survive. Global constants have no value in a SymbolTable, so their values are read by running one short anonymous Apex script per class against the org — pass --no-exec to skip that. A recorded constant keeps its final modifier; an unrecorded one loses it so your tests can assign it. Two fidelity limits are inherent to the source: the SymbolTable erases generics (a List<Contact> parameter arrives as List, rendered as List<Object>, and overloads that collapse under erasure are deduplicated and reported), and a @Deprecated class is invisible to anonymous Apex, so its constants stay null.
--data records the package's data, opt-in. Custom-metadata records become ordinary stubs/customMetadata/<Type>.<Record>.md-meta.xml files, so SOQL against the type and getInstance(name) return what the org returns. Custom-setting rows become seed lines in a fenced, regeneratable block of nimbus.properties: the org-wide default of a hierarchy setting as nimbus.seed.org-default.<Object>, list-setting rows as nimbus.seed.record.<Object>.<Name>. Nothing outside the fence is ever touched, and a value the property format cannot carry (commas, equals signs, line breaks) is skipped by name rather than written wrong. After a pull that adds schema, run nimbus sync so the next test run sees it — the pull reminds you.
Which objects get described. The objects your project ships objects/ metadata for, the standard objects packages usually extend (Account, Campaign, Contact, Lead, Opportunity), any namespaced object name your Apex mentions, and the lookup targets of the fields it pulls.
What it never touches. Anything the project already defines, and anything already under stubs/ unless --force is passed — so re-running after a package upgrade adds what's new and leaves your hand edits alone. Every file written and every skip is printed, and the summary line counts both.
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.
Recording per-call behavior
A stub carries a package class's shape; its method bodies return type-defaults. When a test depends on what the real package actually returns, record it once against an org and replay it offline from then on:
nimbus record -o my-org MyTest # one command: pulls stubs, then records against the org
nimbus test MyTest # replays from .nimbus/recordings/ - no org connection
nimbus test MyTest --no-replay # ignores the recordings, runs the stub bodies
nimbus test MyTest --record -o my-org # re-records without re-pulling stubsRecordings are plain JSON, one file per test method, written as each method finishes so an interrupted run keeps what it captured. They are meant to be committed: the rest of the team and CI then run against the same answers with no org access. Re-recording a method replaces its file wholesale; other methods are untouched.
Fallback is always the stub body. A call that was never recorded, an argument with no JSON form, an org that refuses the forwarded call because the installed package version drifted — each prints one line and runs the stub body. Adding recordings to a suite cannot break it, and --record is never harder to complete than an ordinary run.
Scope. Static methods only — a local stub instance has no org counterpart to forward to, so instance methods keep their stub bodies. Callouts are deliberately out: the platform refuses a callout in a test without Test.setMock, so a deployable test already ships its own mocks. Forwarding is capped at 200 org round trips per run.
See the User Stubs page for the full workflow including dual-layout details.
nimbus upgrade
Check for a newer version of nimbus and upgrade the binary in-place.
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, IntelliJ, Neovim DAP — can launch this command to step through Apex tests with real breakpoints, a call stack view, and expandable local-variable inspection. This is the debug transport both first-party editors use automatically: the VSCode extension launches it as a DebugAdapterExecutable, and the IntelliJ plugin drives it through LSP4IJ's DAP integration. You don't need to configure either editor to get it — it's the default.
nimbus dap # Editors launch this; don't run in a shellSupported DAP requests: initialize, launch, setBreakpoints, setExceptionBreakpoints, exceptionInfo, configurationDone, threads, stackTrace, scopes, variables, evaluate, setVariable, continue, next, stepIn, stepOut, stepBack, reverseContinue, pause, disconnect, terminate. Emits initialized, stopped, thread, output, and terminated events.
Beyond stepping and inspection, the server supports: conditional, hit-count, and log-point breakpoints (evaluated in the paused frame); caught/uncaught exception breakpoints, each optionally filtered to a comma-separated list of Apex exception types; expression evaluation for the debug console, watch expressions, and hovers; and editing a variable's value in place from the Variables pane (live sessions only — a replay session has nothing to write to).
Launch arguments (set in .vscode/launch.json, an IntelliJ run configuration, or equivalent) select one of four session kinds via mode:
| Flag | Default | Description |
|---|---|---|
mode | launch | "launch" runs a test live; "method" calls one method directly; "replay" steps a recorded trace; "flow" (Pro) debugs one record-triggered flow |
program | - | mode "launch": test pattern, e.g. "CalculatorTest.addsPositive" or "*Test" |
method | - | mode "method": target in "ClassName.methodName" form |
isStatic | false | mode "method": call the method statically instead of constructing the class first |
returnType | void | mode "method": declared return type; a non-void type reports the result |
parameters | [] | mode "method": argument list, each {name, type, value} with value as an Apex expression |
traceFile | - | mode "replay": path to a trace.jsonl (or its run dir); implies replay mode. Omit to use the newest trace |
flow | - | mode "flow": the flow’s API name (the .flow-meta.xml basename) |
record | - | mode "flow": field map for $Record, e.g. {"Name": "Acme"} |
triggerType | beforeInsert | mode "flow": "beforeInsert", "afterInsert", "beforeUpdate", or "afterUpdate" |
projectPath | CWD | Project root containing sfdx-project.json |
orgAlias | default | Salesforce org alias |
stopOnEntry | false | Pause at the first statement of the program |
Calling one method directly, without a wrapping test:
{
"type": "nimbus-apex",
"request": "launch",
"name": "Debug Method",
"mode": "method",
"method": "Calculator.add",
"isStatic": true,
"returnType": "Integer",
"parameters": [
{ "name": "a", "type": "Integer", "value": "2" },
{ "name": "b", "type": "Integer", "value": "3" }
]
}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.
Flow debugging Pro
Breakpoints work in .flow-meta.xml files: a flow element is defined at a concrete place in its XML, and a breakpoint set on — or anywhere inside — an element's block stops the session before that element executes. While paused, the stack shows a flow frame (flow name · current element), and its scopes expose $Record (plus $Record__Prior on updates), the flow's variables, and the current element's metadata. next advances to the next element — a decision's taken branch is the element you land on — stepIn on an Apex-action element enters the method, and stepOut runs the flow to completion.
This works in the default launch mode — a test whose DML fires a record-triggered flow stops at flow breakpoints mid-DML — and in the dedicated flow mode, which runs one record-triggered flow against a single in-memory record in a rolled-back database context, no Apex test required:
{
"type": "nimbus-apex",
"request": "launch",
"name": "Debug Flow",
"mode": "flow",
"flow": "Account_Set_Rating",
"record": { "Name": "Acme", "Industry": "Tech" },
"triggerType": "beforeInsert"
}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 80
# Emit a machine-readable JSON report for CI (exit code still set by --min-score)
nimbus mutate --json --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. |
--json | false | Emit a single machine-readable JSON report to stdout (mutationScore, total, killed, survived, timedOut, errors, survivors[]) and suppress the human summary. Combine with --min-score to get both the report and the exit code. This is the format the release mutation gate reads. |
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 fuzz Pro
Property-based testing for trigger paths. Nimbus generates records that are valid per your project's schema - required fields populated, picklist values from the real value set, string lengths within bounds, required lookups satisfied by a generated parent record - and runs each one through the DML pipeline with your triggers firing. Alongside typical values it generates the adversarial ones your triggers actually meet in production: null in every nillable field, empty strings, boundary numbers and dates, unicode.
This is the QuickCheck/Hypothesis technique, applied to triggers. It has never been practical for Apex because an org grants 150 DML statements per transaction and each round-trip costs seconds. Locally, every run executes in its own rolled-back transaction with fresh static state, in milliseconds - hundreds of generated records take seconds.
A validation-rule rejection or an addError is not a failure - that is the object doing its job, and it is tallied separately. A failure is the pipeline surfacing a defect: an unhandled exception inside a trigger, a non-DML exception escaping the DML, or an engine-level error. On the first failure the input is shrunk - fields toward null, strings shorter, numbers toward zero, re-running the pipeline at every step - until only what triggers the bug remains, and reported as a paste-ready Apex snippet for nimbus exec.
# 200 generated inserts through the Account trigger pipeline
nimbus fuzz Account
# More runs - same milliseconds each
nimbus fuzz Account --runs 1000
# Also update each record after a clean insert (before/after update paths)
nimbus fuzz Account --update
# Replay a reported run exactly
nimbus fuzz Account --seed 741253
# Machine-readable report
nimbus fuzz Case --jsonA found failure reports the minimal reproducing input:
FAILURE on run 8 of 200 (seed 741253), insert phase
UNKNOWN_EXCEPTION: Insert failed. First exception on row 0; first error:
UNKNOWN_EXCEPTION, Attempt to de-reference a null object: []
Minimal failing record:
Name = 'a'
Rating = 'Hot'
Repro (paste into `nimbus exec`):
Account rec = new Account(Name = 'a', Rating = 'Hot');
insert rec;
Shrunk in 26 steps (32 executions). Replay this run: --seed 741253Flags
| Flag | Default | Description |
|---|---|---|
--runs | 200 | Number of generated records to run |
--seed | random | Seed for deterministic replay. Every report prints the seed it used; passing it back replays the identical sequence, including the failure and its shrinking. |
--update | false | After each clean insert, also update the record with newly generated values, exercising the before/after update trigger paths |
--json | false | Emit the full report as JSON: pass/rejection tallies by DML status code, average per-run cost, and the shrunk failure with both the minimal and the original repro |
Exit code is non-zero when a failure is found, so nimbus fuzz can gate CI. Deliberate rejections are tallied by DML status code in the report - a large REQUIRED_FIELD_MISSING count means generated records are missing a constraint, not that your triggers are healthy.
nimbus explain
Feature overview: failure intelligence.
Explain one test's outcome in full: the exception, the source location, the assertion's expected and actual values, and the SOQL and DML executed just before it failed.nimbus test output stays terse - this is the explicit expansion, so nothing about a normal run changes.
# Explain a specific failure
nimbus explain AccountServiceTest.testInsertAccount
# Structured output for scripts and agents
nimbus explain AccountServiceTest.testInsertAccount --json
# Mask runtime values before pasting into an issue or chat
nimbus explain AccountServiceTest.testInsertAccount --redact
# What does this command keep, and where?
nimbus explain --retention-policyWhat it prints
ZZDiagFailTest.assertionFailsWithOperationTail — FAILED (8ms)
System.AssertException
Assertion failed: expected 999, got 1. diagnostics fixture: expected 999 accounts
at force-app/main/default/classes/ZZDiagFailTest.cls:23
Assertion
expected 999
actual 1
Last operations before the failure (most recent last)
dml insert Account (1 row) ZZDiagFailTest.cls:18
soql SELECT Id, Name FROM Account WHERE Name = 'Diagnostics Fixture Co' ZZDiagFailTest.cls:20
Not established by this explanation
- Explanation is derived from local execution only; the org remains the final authority.
- Record-level mutation provenance is not recorded.
- No comparison against a previous passing run is available.Last passed
When history holds a run in which the test passed, the report says when — and whether the Apex source has changed since. An unchanged fingerprint means the code is not what differs, which separates a regression from non-determinism. It does not close the question: test data, ordering, time, configuration and org metadata all sit outside the fingerprint, and the report says so.
It states what it does not know
Every explanation ends with what it could not establish. Nimbus runs Apex locally at high fidelity, but it is not the Salesforce platform: an absent record in the evidence means "not observed", not "did not happen". The report says so rather than letting you assume otherwise - and it names the boundaries of the feature itself, so an empty operation tail is never mistaken for a test that ran no queries.
Flags
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the versioned JSON explanation (schema: nimbus.explain/v1) |
--redact | false | Mask assertion operands, SOQL string literals, and their occurrences in the exception message |
--retention-policy | false | Print what this command keeps, and where |
Retention and redaction
Explanations are computed in-process and are not written to disk, cached, or uploaded. Operands and SOQL text are shown verbatim by default, because the local developer already has that data in front of them. Use --redact for output that leaves the machine; it deliberately over-masks, keeping structure (object, fields, exception type, location) so a redacted report stays diagnosable.
Same contract for agents
The MCP tool explain_failure returns the same versioned payload, built from the same code path, so a human and an agent never read divergent accounts of one failure. It answers from the previous run when possible and otherwise runs just that test.
nimbus triage
Run tests and group the failures by the cause the engine recorded when each one happened. Built for the first run on an unfamiliar codebase, where the output is a wall of failures and the question is "how many distinct problems is this, really?"
# Triage the whole suite
nimbus triage
# Triage one class
nimbus triage "AccountTest.*"
# Structured output (schema: nimbus.triage/v1)
nimbus triage --json
# Show at most 3 tests per group
nimbus triage --limit 3What it prints
Triage — 133 failed of 2357 tests (*)
Grouped by engine-assigned cause
assertion.failed 93 test(s)
An assertion in the test did not hold
AccountServiceTest.testCreate
replay: nimbus trace AccountServiceTest.testCreate --run 2026-08-26T02-08-59_5ba1f24e
loaddata.resource-not-found 2 test(s)
Test.loadData referenced a static resource the project does not contain
fix: Retrieve the static resource, or check the resource name passed to Test.loadData.
No cause recorded (40) — listed individually, not grouped
ACCT_IndividualAccounts_TEST.testOwnerLastNameUpdate — Invalid field Owner for Account
Run-level evidence (2) — not attributed to any failure
Apex Classes ABadClassEach failure carries the command that reopens its recorded trace, the same as nimbus test — see replay on failure. Pass --no-trace-on-failure to skip recording.
It is not a classifier
Every group corresponds to a cause some part of the engine assigned at the moment of failure — a Test.loadData resource that was absent, a @testSetup that threw, a field the schema provider looked up and did not find. Nothing inspects message text to decide where a failure belongs.
Failures the engine could not classify are listed individually, never folded into whichever group looks plausible. A null-pointer exception is a symptom whose cause could be a missing stub, an unpopulated field, or a genuine bug — no pattern match can tell those apart, and a wrong grouping sends you to fix the wrong thing while hiding the failure inside a count.
Run-level evidence
Missing metadata is reported separately and deliberately not attributed to individual failures. Nimbus degrades an absent definition to null or zero rows instead of throwing, so the failure surfaces later, somewhere that no longer knows a schema gap caused it. The gap is real and worth fixing; which test it broke is not something the engine can honestly claim.
Flags
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the versioned JSON triage report (schema: nimbus.triage/v1) |
--limit | 5 | Max tests listed per group (0 = all) |
Suite health in doctor
nimbus doctor --suite runs the same triage and appends a ranked "fix in this order" list to doctor's setup checks - largest cause first, so a red suite becomes a short ordered worklist instead of a wall of failures. It is opt-in because it runs the tests; plain nimbus doctor stays fast enough for a pre-flight check. With --json the report appears undersuite_health.
nimbus history
Browse past test runs in an interactive TUI. Every nimbus test run is recorded under .nimbus/history/; this command reads it back — recent runs, their pass/fail breakdowns, and a flaky-test report. Free.
# Browse recent test runs
nimbus history
# Flaky report: same test, same conditions, different outcome
nimbus history --flaky
# Show the last 50 runs
nimbus history --limit 50Piped output is JSON
When stdout is not a terminal — a pipe, a CI runner — nimbus history skips the TUI and emits the run list as JSON, so nimbus history --limit 50 | jq works without a TTY.
Flags
| Flag | Default | Description |
|---|---|---|
--flaky | false | Show the flaky test report instead of the run list. Applies to the TUI; a piped run always emits the run list. |
--limit | 20 | Number of recent runs to display |
nimbus history get <run-id>
The list carries per-run totals only. Every per-test result — its status, duration and failure message — lives in the stored record, and history get prints it. show is an accepted alias.
# Everything recorded for one run
nimbus history get 1756060500123456789
# The timestamp matches too, and is easier to type
nimbus history get 20260824T1015
# Just the failures
nimbus history get 20260824T1015 --json | jq '.results[] | select(.passed == false)'The run ID is the id field from nimbus history — a nanosecond timestamp. The store matches it as a substring of the record's file name, <timestamp>_<id>.json, so a leading chunk of the ID works and so does the human-readable timestamp. The output leads with the run's pattern, timing, totals and the environment it ran under (Nimbus version, worker count, isolation mode, whether it was a full run), then one line per test with its failure message.
JSON is the default when stdout is not a terminal, matching the run list — so piping into jq needs no flag. --json forces it when it is.
| Flag | Default | Description |
|---|---|---|
--json | false (true when piped) | Emit the full run record as JSON |
History also powers nimbus explain's "last passed" comparison and the trend views in traces & analytics.
nimbus graph
Feature overview: dependency graph.
Show which classes reference a class — directly and transitively — and which tests relate to it. A navigation aid for "what does changing this touch?"
nimbus graph # project overview: size, cycles, most-depended-upon
nimbus graph AccountService # what reaches one class
nimbus graph Label.Order_Error # which classes read one custom label
nimbus graph Handler_Config__mdt # who queries one custom metadata type
nimbus graph SObject.Account # who reads it, who writes it, what fires
nimbus graph Permission.Can_Approve_Orders # what is gated on one custom permission
nimbus graph Resource.Test_Accounts # what loads one static resource
nimbus graph Flow.Order_Automation # what fires one flow, and what it calls
nimbus graph AccountService --jsonBare nimbus graph prints the orientation view: how many classes, triggers and edges, the cycle count, the custom-label count with how many of them nothing in Apex reads, the number of custom metadata types, how many SObjects the code touches, the custom permissions with how many nothing in Apex checks, the static resources with how many nothing in Apex loads, the flows with how many of them are inactive but still wired to something, and the most-depended-upon classes — the step before you know which class to ask about. With --json it emits the full nimbus.graph/v1 payload instead.
It is not a test-selection decision
This reads syntactic references. We measured that reading against real coverage across five projects and found it unsafe for choosing which tests to skip: a trigger dispatching through custom metadata reaches its handler with no reference to follow, so on one real project it found 1 of the 46 test classes that genuinely exercise a handler. The command says so, and refuses to present itself as a selection tool.
Measured alongside, not merged
When a coverage map exists, the tests that actually executed the class are shown as a separate answer, along with any that no syntactic path reaches — a direct measurement of the gap:
ACCT_IndividualAccounts_TDTM — reachability
Tests reached syntactically (1)
ACCT_IndividualAccounts_TEST
Tests a coverage run actually observed executing it (46)
ACCT_AdministrativeNameRefresh_TEST
...
Observed but NOT reachable syntactically (45) — the blind spot, measured
Not established by this view
- Syntactic references only. A trigger dispatching through custom metadata
reaches handlers with no reference to follow.
- This is reachability, not a test-selection decision.The two are deliberately not combined into one number. Where they disagree, the disagreement is the useful part.
Custom labels
Custom labels are nodes in the graph, and Label.<Name> is a root selector: nimbus graph Label.Order_Error reports the label's value and the classes that read it. Reads spelled out in source — Label.Order_Error, or System.Label.get with a constant name — are found by reading the code. Reads through a computed name are not visible to any syntactic reading; those come from the last coverage run, where the interpreter resolved the name as the read happened, and are listed separately as observed.
Label edges cover Apex only. References from Flows, LWC, Aura and formulas are not read, so unreferenced in the overview line means unreferenced from Apex — it is not a delete list. Dynamic reads carry the freshness caveat that applies to every observed edge: a read on a path the coverage run did not execute is not there, and one recorded before the code changed may no longer happen.
Custom metadata types
Custom metadata types are nodes too, and the API name is the root selector — no prefix to learn, since __mdt is reserved:
$ nimbus graph Handler_Config__mdt
Handler_Config__mdt — custom metadata type
4 record(s)
Queried by
1 directly, 12 transitively
TDTM_TriggerHandler
Dispatches to (4) — classes named by record field values
ACCT_IndividualAccounts_TDTM
...
Tests reached syntactically (1)
TDTM_TriggerHandler_TEST
Classes a coverage run observed being dispatched to (4)
ACCT_IndividualAccounts_TDTM
...Three readings, kept apart. Queried by is read from source: SOQL against the type, getAll, getInstance. Dispatches to is a heuristic — a record's field values are matched against the class registry, and a value that names a class becomes an edge — so it can over-claim (a text field that happens to read like a class name) and under-claim (a handler named by a value the records do not carry). The third is measurement: after a coverage run, Type.forName(...).newInstance() resolved an exact class name while the code ran, and that dispatch is recorded as observed.
This closes the gap the limits above only apologised for. A trigger dispatching through custom metadata used to reach its handler with no reference to follow; AccountTrigger → TDTM_TriggerHandler → Handler_Config__mdt → ACCT_IndividualAccounts_TDTM is now a chain you can walk. The caveat travels with it — the handler side is a guess until a coverage run confirms it.
SObjects
Objects the code queries or writes are nodes as well, and SObject.<Name> is the root selector — the prefix exists because a class may legitimately be called Account:
$ nimbus graph SObject.Account
Account — SObject
Queried by (19)
AccountService
...
Written by (19) — these fire its triggers
AccountService
...
Triggers on this object (1)
AccountTrigger
Record-triggered flows on this object (1)
Probe_Account_Flow
Tests reached syntactically (20)
AccountServiceTest
...
Classes a coverage run observed querying it (1)
DynamicSelectorTestRead and write are separated because they are different questions. Written by is the one that carries consequences: a write reaches every trigger the project declares on that object, so the object-to-trigger edges are certain rather than inferred — this is where a trigger finally gets inbound edges, and the caveat that they "come from DML, which this reading cannot derive" no longer applies.
Objects named in source — a SOQL FROM clause, a typed variable under insert, X__c.getInstance() — are found by reading the code. Dynamic access is not visible to any syntactic reading: Database.query with a computed string, DML on a generic SObject or a List<SObject>. Those come from the last coverage run, where the SOQL translator and the DML executor knew the concrete object, and are listed separately as observed — which is how an fflib-style selector layer becomes visible at all. The usual freshness caveat applies: access on a path the run did not execute is not there. Readers and writers cover Apex only; Flows, LWC, Aura and integrations touch objects without appearing here.
A bare argument is still resolved as a class first, since that is what it has always meant. When no class answers, __c and __e names resolve to the object silently — the platform reserves those suffixes for schema — and any other bare name gets an error that suggests the SObject. form when an object of that name exists. Nodes exist only for objects this code touches or a trigger runs on; this is not the org's schema.
Custom permissions and static resources
Both are nodes too, both are leaves, and both have their own root selector — Permission.<Name> lists the classes gating on a custom permission, Resource.<Name> the classes loading a static resource:
$ nimbus graph Permission.Can_Approve_Orders
Can_Approve_Orders — custom permission
"Approve Orders"
force-app/main/default/customPermissions/Can_Approve_Orders.customPermission-meta.xml
Checked by
1 directly, 2 transitively
ApprovalGate
Tests reached syntactically (1)
ApprovalGateTest
Classes a coverage run observed checking it (1)
ApprovalGate
$ nimbus graph Resource.Test_Accounts
Test_Accounts — static resource
text/csv
force-app/main/default/staticresources/Test_Accounts.resource-meta.xml
Loaded by
1 directly, 1 transitively
ApprovalGateTest
Classes a coverage run observed loading it (2)
ApprovalGateTest
FixtureLoader
Observed but named nowhere in source (1) — loaded through a computed name
FixtureLoaderChecks and loads spelled out in source — FeatureManagement.checkPermission with a constant name, Test.loadData or a PageReference('/resource/…') — are found by reading the code. A name assembled at runtime is not visible to any syntactic reading; those come from the last coverage run, where the permission or resource resolved to an exact name as the check or load happened, and are listed separately — with the observed-only ones called out as named nowhere in source.
Only Apex is read, which matters more here than for any other kind. A permission granted through a permission set or a profile, or enforced in a Flow, a formula or a validation rule, is invisible to this view; so is a resource referenced from LWC, Aura, Visualforce or markup. Unchecked and unread in the overview therefore mean from Apex — neither is a delete list. The usual freshness caveat applies to the observed side: a check on a path the coverage run did not execute is not there.
Flows
Nimbus runs record-triggered flows itself, so they are nodes too, and Flow.<Name> is the root selector:
$ nimbus graph Flow.Order_Automation
Order_Automation — flow
Draft
force-app/main/default/flows/Order_Automation.flow-meta.xml
Fired by DML on ParentObj__c
1 class(es) write that object
OrderIntakeService
Calls Apex (1)
AccountService
Calls subflows (1)
Probe_Account_Flow
Writes objects (1) — these fire their own automation
ChildObj__c
Tests reached syntactically (1)
OrderIntakeServiceTestA flow is not a leaf. It has edges in both directions: inbound from the object it triggers on, outbound to the Apex actions it invokes, the subflows it runs and the objects its record elements read and write. That completes the DML edge — a write to an object now reaches both its triggers and its record-triggered flows, so SObject.Account lists the flows next to the triggers and the blast radius of an insert includes the automation a flow starts. Flows are also the first non-class nodes that can sit inside a cycle, through a subflow loop or a flow calling a class that writes the flow's own trigger object; --cycles reports such a loop as nodes rather than classes.
Status travels with the node and changes what its edges mean. A Draft, Obsolete or invalid flow still owns every edge above, but it does not run on record changes — those edges describe what it would reach if it were activated. The overview counts inactive-but-connected flows separately for that reason, and the detail view says it outright rather than letting a full-looking graph imply the automation is live.
Edges are read from the flow's own metadata: the object it is triggered on, the Apex actions and subflows it names, and the objects its record elements touch. An action invoked through a name assembled at runtime is not read, and neither is anything a flow reaches through a screen component. Nor is the launch path of a screen flow started from a page, a button, a quick action or a process — such a flow appears with no inbound edge. Being fired by DML is derived rather than referenced (nothing in an org names a record-triggered flow), and whether a particular write meets a given flow's entry conditions is not evaluated here.
Agentforce agents
Where a project ships Agentforce metadata, its agents, topics and actions are nodes too, and each action's edge lands on the Apex class or Flow its invocationTarget names. A class or flow reached that way says so — Reached by agent actions (1) · Support_Agent › Submit Case — which is a caller no reading of Apex can find, because nothing in Apex references an agent back. For an autolaunched flow it is the only inbound edge there is. Which action a planner selects for a given utterance is Salesforce's stochastic runtime and is not modelled at all. An action whose target this project does not ship, or whose target type nimbus does not run, carries no edge and is counted separately — nimbus agent list names which. A project with no agent metadata sees none of this and pays nothing for it.
Circular dependencies
nimbus graph --cycles lists every circular dependency, largest first, with a shortest loop through each — so the output says where to cut rather than only that a cycle exists.
$ nimbus graph --cycles
8 classes: Advancement_Adapter, Advancement_Info, TDTM_Config, ...
shortest loop: Advancement_Adapter → Advancement_Info → Advancement_Adapter
cut any one edge on that loop to break itClasses that reference themselves are counted separately rather than listed: in Apex that is nearly always a class qualifying its own statics or an inner class, and printing dozens of them buries the finding that matters. Reported cycles are a lower bound — edges invisible to a syntactic reading may close further loops.
Export and visualise
--format dot|mermaid|json exports the graph. Edges only a coverage run revealed are drawn dashed, triggers get a distinct shape, and the limits travel inside the file as comments — an exported diagram outlives the session that explained it. Past 300 nodes the export refuses and tells you how to narrow, rather than emitting an unreadable hairball. Non-class nodes — custom labels, custom metadata types, SObjects, custom permissions, static resources and flows — stay out of the dot and mermaid exports unless --include-metadata is passed (--include-labels adds the labels alone), so that ceiling keeps measuring the class graph; the JSON always carries them. Pointing the command at a metadata root overrides the default, since stripping the node you asked about would draw its neighbourhood with the subject missing.
nimbus graph --format dot | dot -Tsvg -o graph.svg
nimbus graph AccountService --depth 2 --format mermaid
# Raw Mermaid is not meant to be read in a terminal. Write a .md instead and
# open it — VS Code's preview, GitHub and Obsidian all render the diagram.
nimbus graph AccountService --out graph.mdWith --out, a .md path wraps the diagram in a Markdown document: a fenced Mermaid block that renders natively, the cycles listed underneath, and the limits as prose rather than as comments — a comment is invisible once rendered, which is exactly when someone is most likely to read the picture as complete.
An interactive view is available in the Dev UI, VS Code (Nimbus: Show Dependency Graph) and IntelliJ, all rendering the same nimbus.graph/v1 payload. It reads like a note-graph tool: hovering a class highlights its direct neighbourhood and fades the rest, clicking pins the highlight, and search filters by name. Test classes and unconnected nodes can be hidden, the layout forces are adjustable, and nodes can be dragged. Names appear on hubs first and fade in as you zoom. In VS Code, double-clicking a node opens the file.
The resting view is classes and triggers only. Custom labels are added from the display panel — drawn as squares, with the label's value and its reader count in the tooltip; a second checkbox adds the labels nothing reads. Searching does not wait for either: a query that matches a label reveals it while the query is active, so a label that exists in source never comes back as no match. A label: prefix restricts the search to labels.
show custom metadata adds the custom metadata types the same way, drawn as diamonds; the tooltip gives the record count, how many classes query the type, and how many classes its records dispatch to. Their outbound edges are the heuristic ones, and the legend says so. Search reaches them while the toggle is off too, and an mdt: prefix restricts the search to types.
show sobjects adds the objects, drawn as triangles; the tooltip gives the reader count, the writer count and how many triggers a write fires, and an object reached only through dynamic access is drawn in the same orange as every other runtime-observed node. Search reaches them with the toggle off, and an obj: prefix restricts the search to objects.
show permissions & resources is one toggle for the two smallest kinds: custom permissions are drawn as pentagons, static resources as tall boxes. The permission tooltip gives the permission's label and how many classes check it, the resource tooltip its content type and how many classes load it, and either is drawn in the runtime-observed orange when nothing in source names it. Both have their own search prefix — perm: and res: — and search reaches them while the toggle is off, like every other kind. These two are also the first metadata nodes that open: they carry a file, so double-clicking one opens its -meta.xml, where double-clicking a label, a type or an object frames its neighbourhood instead.
show flows is a toggle of its own — an org runs hundreds of flows and "which flows are there" is a question worth its own switch. They are drawn as an outline hexagon: the trigger's shape in the trigger's amber, unfilled and a size step smaller, because a flow is automation rather than data and belongs next to triggers rather than next to the squares and triangles. A flow that is not Active is drawn with a dashed outline — its edges exist but it does not run — and the tooltip gives the status, what fires it, how many classes and subflows it calls, and how many objects it writes. A flow: prefix restricts the search to flows, search reaches them while the toggle is off, and double-clicking one opens its .flow-meta.xml.
Flags
| Flag | Default | Description |
|---|---|---|
--cycles | false | List circular dependencies, largest first |
--format | | Export the graph: dot, mermaid or json |
--depth | 2 | With a class name, how many hops of neighbourhood to include |
--out | | Write to a file instead of stdout; a .md path wraps the diagram so VS Code, GitHub and Obsidian render it |
--include-labels | false | Draw custom-label nodes in the dot and mermaid exports; labels only (JSON always includes them) |
--include-metadata | false | Draw every non-class node — custom labels, custom metadata types, SObjects, custom permissions and static resources — in the dot and mermaid exports |
--json | false | Emit the report as JSON (schema: nimbus.graph/v1) |
nimbus flow
Inspect the Flows in a project without opening an org. nimbus already parses every .flow-meta.xml to fire record-triggered flows during tests; this command family exposes that same parse as something you can read. Free.
nimbus flow list # every flow: name, type, trigger, status
nimbus flow graph Order_Automation # one flow's elements and connectors
nimbus flow graph Order_Automation --json
nimbus flow graph Order_Automation --mermaid
nimbus flow graph Order_Automation --out order.html # self-contained interactive viewer
nimbus flow graph force-app/main/default/flows/Order_Automation.flow-meta.xmlList
nimbus flow list tables every flow in the package directories — name, label, type, trigger object, status. Draft and Obsolete flows are listed like Active ones: they are still source, and the flow that no longer runs but still names an Apex class is exactly the one worth finding before that class is deleted.
Graph one flow
nimbus flow graph <name> prints the flow's identity — process type, trigger, entry-condition count, status, API version, resource counts — then every element with its source line and every connector. The argument can also be a .flow-meta.xml path, which works outside a Salesforce project too.
$ nimbus flow graph Order_Path
Order_Path — flow
"Order Path"
AutoLaunchedFlow · Active · API 59.0
RecordAfterSave on Order__c (CreateAndUpdate)
Elements (5)
start Start — RecordAfterSave on Order__c (CreateAndUpdate)
20 decision Check Amount — 1 rule
37 recordUpdate Save Order — update Order__c
...
Connectors (6)
Start → Check_Amount
Start → Notify [scheduled: Reminder (+3 Days)]
Check_Amount → Save_Order [rule: Large order]
Save_Order → Notify [fault]Every edge is typed rather than merely drawn: decision rules carry their rule label, the loop's next-value and no-more-values connectors are split, scheduled paths carry their offset, and fault connectors are marked — so a renderer can dash the error path without re-deriving flow semantics. Element line numbers come from the same index the flow debugger keys breakpoints by.
Exports
--json emits the nimbus.flowgraph/v1 document; --mermaid and --dot export diagrams with fault edges dashed and rule edges labeled. With --out, a .html path writes a self-contained interactive viewer — pan, zoom, hover to light a step's neighbourhood, click to pin its facts — with no server and no external assets, and a .md path wraps the Mermaid diagram so VS Code, GitHub and Obsidian render it.
In the IDE
IntelliJ has the same viewer as View Flow: in the editor and Project-view right-click menus of any .flow-meta.xml, and on the flow's editor capsule, which also shows the flow's identity facts (label, type, trigger object, status, API version). Double-clicking a node opens the flow file at that element's exact line. The renderer is the same file every other host uses, so all surfaces draw one definition.
Flags (flow graph)
| Flag | Default | Description |
|---|---|---|
--json | false | Emit the flow graph as JSON (schema: nimbus.flowgraph/v1) |
--mermaid | false | Export the flow as a Mermaid flowchart |
--dot | false | Export the flow for Graphviz |
--out | | Write to a file instead of stdout; a .html path gets the self-contained interactive viewer, a .md path a rendered Mermaid document |
nimbus bisect
Find the commit that broke a test, using git bisect — made practical for Apex because each step is a local, sub-second test run instead of an org deploy. Free.
# AccountTest.testDiscount used to pass at v1.40.0 and fails now
nimbus bisect "AccountTest.testDiscount" --good v1.40.0
# Bad ref other than HEAD
nimbus bisect "AccountTest.testDiscount" --bad abc1234 --good v1.40.0
# Machine-readable result for CI
nimbus bisect "AccountTest.testDiscount" --good v1.40.0 --jsonHow it works
The search runs inside a temporary git worktree of the project's own repository — never your actual checkout. git bisect moves HEAD as it walks history, which would be destructive against a checkout other sessions or tools might be using, so nimbus bisect creates a disposable worktree, drives the whole bisection there, and removes it before exiting — including on Ctrl-C. Each worktree gets its own embedded database automatically (the port and database name are derived from the worktree's path), so a bisect run never collides with the database from your regular checkout.
At each candidate commit, a probe step runs nimbus test "<pattern>" --no-daemon in the worktree and reports one of three verdicts back to git bisect run: good (the test passed), bad (the test actually failed), or skip when the class or method doesn't exist yet at that commit. Skipping a missing test rather than calling it "bad" matters — otherwise bisect would blame whichever commit happens to introduce the test for a failure that is really somewhere later in history. A build, parse, or config error that never got to evaluate the test is also treated as a skip rather than a guess.
Output
Progress streams as the bisection narrows the range. On convergence, a verdict block reports the culprit commit (hash, author, date, subject), how many steps it took and how long, and a copy-pasteable command that reproduces the failure in its own disposable worktree. --json emits a nimbus.bisect/v1 payload on stdout instead — progress moves to stderr so a CI run watching stdout still gets clean JSON, while someone watching the terminal still sees steps happen in real time.
Flags
| Flag | Default | Description |
|---|---|---|
--good | | Ref known to have the test passing (required) |
--bad | HEAD | Ref known to have the test failing |
--json | false | Emit the versioned JSON result (schema: nimbus.bisect/v1) |
nimbus bisect runs against the project's own git history — the Salesforce project under test, found the same way every other nimbus command finds it. If that directory isn't a git repository, or --bad/--good don't resolve to commits, nimbus bisect fails with a clear error before creating a worktree.
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.
Traces & Analytics
A test run with --trace records every span of its execution — methods, SOQL, DML, triggers, and (at --trace-level verbose) every statement — into .nimbus/traces/<run>/trace.jsonl. The trace is what powers replay debugging, the trace viewer, and the field ledger below.
Execution Traces
A trace is the record of what a test actually did: which methods it entered, which SOQL it issued, which DML it ran, and in what order. Nimbus does not collect one unless you ask, because collecting it costs time on every statement.
# Run a test with tracing on
nimbus test AccountServiceTest.testCreate --trace
# Open the trace in the viewer
nimbus trace AccountServiceTest.testCreate
# Most recent trace, whatever test produced it
nimbus trace --lastnimbus test --trace writes traces to .nimbus/traces/. nimbus trace opens the interactive TUI viewer on the newest trace file there and roots the flow at the test you name; with no argument, or with --last, it roots at whatever ran.
If you collected somewhere other than .nimbus/traces/, point the viewer at the same place with --trace-output, or set nimbus.trace.output in nimbus.properties so both sides read it and neither needs the flag.
Verbosity
Two flags share one ladder. --trace-level on the test run decides how much gets recorded; --level on the viewer decides how much of a recording is shown, hiding spans below the level you ask for.
Filtering only ever subtracts. Recording at minimal and then viewing at debug shows you nothing extra, because the detail was never written down — so the level that matters most is the one you collect at.
| Flag | Default | Description |
|---|---|---|
minimal | | Method entry and exit only |
normal | default | Methods, SOQL, DML — the level that answers most questions |
verbose | | Adds statement-level detail |
debug | | Adds variable values as they change |
system | | Also traces Nimbus-internal frames |
Flags
nimbus test:
| Flag | Default | Description |
|---|---|---|
--trace | false | Collect an execution trace for each test that runs |
--trace-level | normal | How much to record: minimal, normal, verbose, debug, system. Anything else is refused rather than quietly treated as normal. |
--trace-output | .nimbus/traces | Directory for trace output files. Pass the same directory to nimbus trace, or set nimbus.trace.output to change both. |
nimbus trace:
| Flag | Default | Description |
|---|---|---|
--last | false | Root the flow at whatever ran, ignoring a test name argument |
--level | normal | Hide spans below this verbosity: minimal, normal, verbose, debug, system. Cannot show detail the recording did not capture. |
--trace-output | .nimbus/traces | Directory to read traces from. Also settable as nimbus.trace.output; a relative path is resolved against the project root. |
--run | - | Open one recorded run instead of the most recent. Accepts a run directory, its full name, or any unambiguous suffix. This is what the replay line under a failed test uses, so it keeps working after later runs record traces of their own. |
The viewer needs an interactive terminal. Piped or run under CI it prints the directory the traces are in and exits successfully, so a CI step that reaches it by accident does not fail the build. The same traces are rendered inline by the VS Code trace viewer.
nimbus trace Pro
nimbus trace opens the interactive trace viewer for a test's most recent recorded run:
nimbus test "AccountServiceTest.*" --trace # record
nimbus trace AccountServiceTest.testCreate # inspect
nimbus trace --last # newest trace, any testnimbus trace timeline — the run on one axis
Where nimbus trace shows one transaction's call tree, nimbus trace timeline puts a whole recorded run on a single time axis: test methods, Apex calls, SOQL, DML, triggers, flows and the rest of the save pipeline as lanes of bars, with every field write as a tick and every span's governor delta as a reading. Statement-level spans are left off by default and the header reports how many; --detail places them.
nimbus trace timeline # newest traced run
nimbus trace timeline 20260824T101530_a1b2c3d4
nimbus trace timeline --json # the nimbus.timeline/v1 document
nimbus trace timeline --out run.html # self-contained interactive viewer
nimbus trace timeline --detail # with the statement-level spans
nimbus trace timeline --diff 20260823T090000_9f8e7d6c| Flag | Default | Description |
|---|---|---|
--json | false | Emit the nimbus.timeline/v1 document instead of the textual timeline |
--out | - | Write to a file; a .html path gets the self-contained interactive viewer, anything else the JSON document |
--width | 60 | Columns the textual timeline strip is drawn across |
--detail | false | Include the statement-level spans (assignments, branches, loop iterations), drawn inside the bar that ran them |
--diff | - | Compare against another traced run, aligned by test method name; prints a per-method duration delta table, slower first |
--detail is off by default because a verbose recording holds orders of magnitude more statement spans than lanes of bars. They are not a lane of their own: each is drawn inside the bar that ran it, once the axis is magnified far enough for a statement to be worth a pixel. The document reports detail_available either way, so you can tell whether asking would show anything.
--diff takes a second run id and aligns the two by test method name — never by a run identifier, since trace run ids and test-history ids are separate id spaces. The two runs are not laid on one axis: they do not share a clock, so what is compared is how long each method took, not where it fell. The viewer draws one row per method with the current run's bar over the baseline's; the terminal prints the same ordering as a delta table.
The lanes, the span bars and the governor readings are free — they are span data, and spans are the trace. The field-write ticks are a ledger surface and need Pro Pro; a timeline built without it says so rather than showing an empty track.
The HTML export and the IDE panel share one renderer, so the page you write and the panel JetBrains opens are the same viewer. In the IDE (Nimbus → View Run Timeline, or the Run Timeline button on the debugger toolbar during a replay session) clicking a bar seeks the replay to that instant — landing on the last recorded step at or before the click. With no replay session open the position is copied to the clipboard and the panel says so.
nimbus trace ledger — the field ledger Pro
Every traced run also records the causal field ledger: each SObject field write, with the old value, the new value, and the frame that performed it — your Apex line, a trigger line, or the engine itself (a formula resolve, a roll-up recompute, a platform default). nimbus trace ledger queries a run's ledger; it answers "why is this field this value?" without re-running anything.
nimbus trace ledger # newest traced run, every write
nimbus trace ledger --field Account.Name # one field's history
nimbus trace ledger --record 001Kj00000AbCdEfGH1 # one record's history
nimbus trace ledger 20260824T101530_a1b2c3d4 --jsonEach row reports the test, sequence number, timestamp, object, record, field, old -> new, the writer kind (apex, trigger, flow, default, rollup, formula, platform), and the writing location — Class.method:line for code, or the pipeline phase / roll-up definition for engine writes, which have no source line to point at.
| Flag | Default | Description |
|---|---|---|
--field | - | Filter to one field, as Object.Field (e.g. Account.Name) |
--record | - | Filter to one record: a platform Id (15 or 18 chars) or a ledger identity (rec-N) |
--json | false | Emit rows as JSON instead of the table |
The run-id argument is a directory name under .nimbus/traces (any unambiguous suffix works); omitted, the newest traced run is used. The raw apex.field.write events are in every trace at any tier — the indexed, queryable ledger is Pro.
The same ledger drives Find Where This Was Set in the IDE: during a replay debug session, right-click a field in the Variables view and Nimbus runs the recording backwards to the write that set it, via standard DAP data breakpoints.
Test Analytics
Every nimbus test run is recorded under .nimbus/history/ whether or not you asked for it. nimbus analytics reads that history back as trends: pass rate and duration over time, rather than the state of the last run.
# Trends over the last 30 days
nimbus analytics
# Narrow the window
nimbus analytics --days 7Flags
| Flag | Default | Description |
|---|---|---|
--days | 30 | Number of days to include in the trend data |
The dashboard is a TUI. Piped or under CI it emits JSON instead — the requested days alongside the raw runs the trends are computed from, so a build step can chart or threshold them itself.
# Pass rate of the most recent run
nimbus analytics | jq '.runs[0] | .passedTests / .totalTests'The JSON dump reads up to 1000 runs regardless of --days; the flag scopes the interactive view. Trends need history to exist — a fresh project shows nothing until it has runs behind it, and nimbus reset deletes them along with the rest of .nimbus/.
Flaky Detection
A flaky test is one that changes its answer while nothing that could legitimately change it moved. nimbus history --flaky reports them out of the same run history the analytics view uses.
nimbus history --flakyWhat counts as a flip
Not every pass-then-fail is flakiness, and treating it as such is how a flaky-test report becomes noise nobody reads. Nimbus records the conditions of every run alongside its results, and compares only runs whose conditions match:
| Flag | Default | Description |
|---|---|---|
sourceFingerprint | | A digest of the project’s Apex sources. Different fingerprint means different code — fixing a bug must not mark the test flaky. |
fullRun | | False when the run was narrowed by a pattern or --impacted. A test absent from a narrowed run did not pass or fail; it did not run. |
workers | | The parallel worker count. Contention is the most common real source of order-dependent failure, so runs at different widths are not compared. |
isolation | | The test isolation mode, for the same reason. |
tier | | The entitlement the worker count was resolved under — without it, a deliberate -p 1, a single-core machine and a lapsed activation are indistinguishable afterwards. |
An outcome flip is evidence of non-determinism only when all of that held still. Records written before this environment data existed carry none of it and are not comparable.
Reading the report
The report is part of the history TUI and needs a terminal: --flaky selects the flaky view over the run list. A piped nimbus history always emits the run list as JSON — the flag does not change that, so scripting a flakiness gate means consuming .results[] across runs yourself.
Flakiness is a property of a history, not of a run, so the report is only as good as the history behind it. A test that has run twice cannot be called flaky; one that flips under -p 8 and never under -p 1 is telling you about contention, which is a real bug and not a measurement artifact.
Configuration
Global Flags
These flags work with every command:
| Flag | Default | Description |
|---|---|---|
-o, --org, --target-org | - | Target Salesforce org alias (passed to SF CLI for sync/fallback). All three spellings are the same flag on every command — --target-org matches the Salesforce CLI, so scripts written against either tool work unchanged. |
-v, --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 adds the CurrencyIsoCode field to standard and custom objects and makes UserInfo.isMultiCurrencyOrganization() return true; without it the field does not exist, matching a single-currency org. PersonAccounts is reserved for future schema-level behaviour. |
nimbus.org.faketime | (real clock) | ISO-8601 instant that pins Date.today() / DateTime.now() / System.now() for deterministic time-dependent tests. |
# 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.
MultiCurrency changes the shape of your schema, not just what UserInfo reports. On a single-currency orgCurrencyIsoCode does not exist at all — the describe omits it and SOQL rejects it by name — so Nimbus leaves it out by default. Toggling the feature re-syncs the schema cache automatically; you do not need to clear anything.
Permission Seam Visibility
Nimbus runs tests as a sysadmin-equivalent default user. Outside System.runAs(), FLS checks return true, WITH SECURITY_ENFORCED passes through, and $Permission.X / FeatureManagement.checkPermission() resolve against the default mock user (005000000000000AAA). This is faithful to Salesforce's "tests run as the configured user" model — but it creates a silent divergence: a test can pass under Nimbus and fail in a real org when the org's running user isn't assigned the perm.
Permission seams make every such case visible per-test. When a passing test consulted a permission outside a runAs block, the runner records a seam: the kind of check (FLS, object perm, SOQL user-mode, custom permission), the subject, and how it resolved (default-allow for FLS/object/SOQL; default-mock-user for custom perms granted via seeded nimbus.mock.permission-sets or test-inserted PSAs). Custom-permission seams use a distinct CUSTOM PERM prefix because they look like business logic, not security, in the test code — the most damaging variant of the divergence.
By default the runner shows a single end-of-run summary line with totals. --show-permission-seams expands it to a per-test list. --strict-permissions fails any test whose passing path consulted a permission outside runAs — opt-in for CI users who want to enforce the discipline.
# 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.
Under --json, a run that crossed at least one seam adds a top-level permission_seams block — { total, customPermission, flsObject, seams: [{ kind, subject, path, file?, line?, custom }] } — so agents and the release gate can read the divergence without scraping human output. It is omitted entirely on a clean run (zero seams). This is the block a release profile's permissionSeamPolicy reads to block a release that passed only through the default-allow path.
Read-only Mode
Refuse any DML inside tests via --readonly. Useful for CI stages that should only observe — e.g. post-merge sanity checks that re-run the suite against frozen data. When enabled, any insert, update, delete, orupsert statement inside a test throws System.DmlException with a diagnostic message.
# 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.
On GitHub, the Nimbus action is the shortest path: one file and one secret gets the suite running on every pull request, with failures, reproduction commands, and a coverage delta posted back to the PR.
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.
Sessions authenticated via NIMBUS_LICENSE_KEY register a machine on your license, just like nimbus login does. CI usage registers a single machine per CI provider (named CI — GitHub Actions etc.) no matter how many runners execute; an AI agent driving a local CLI registers that actual machine. Both appear in your portal and can be deactivated there.
After a Pro license validates, Nimbus records one privacy-minimal headless activation per license fingerprint, CI provider, and installation source. Repeated pipeline runs do not create additional activation rows. No license key, repository, branch, workflow, command arguments, or project content is sent. Set NIMBUS_TELEMETRY=false (or NIMBUS_NO_TRACKING=1) to disable it.
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.
Salesforce CLI Plugin
Install Nimbus through the Salesforce CLI and run the local runtime under the familiar sf command. The plugin installs the native Nimbus binary automatically on first use, verifies its release checksum, and preserves Nimbus output and exit codes.
sf plugins install @nimbus-solution/nimbus-sf-plugin
sf nimbus login
sf nimbus test "*"
sf nimbus validate --sema error
sf nimbus deploy --target-org staging --source-dir force-app
sf nimbus release validate --release-profile production
sf nimbus exec --code "System.debug(1 + 1);"
sf nimbus mutate AccountService --min-score 80
sf nimbus doctorThe plugin uses the same Nimbus account and ~/.nimbus/license.json as the native CLI. Run sf nimbus login once to activate Pro features on the machine, sf nimbus whoami to inspect the active plan, andsf nimbus logout to deactivate the machine and free its license slot. This login is separate from Salesforce org authentication and survives runtime updates.
Salesforce-style Apex aliases are available when they fit existing CLI muscle memory:
sf nimbus apex run test AccountServiceTest --coverage
sf nimbus apex validate --json
sf nimbus apex run --code "System.debug('local');"Every native Nimbus flag passes through unchanged. Use sf nimbus installto update the plugin-managed runtime, --version to pin a release, or set NIMBUS_BINARY_PATH to run a development build.
Native nimbus sf is intentionally not mirrored assf nimbus sf: from inside Salesforce CLI, run the desiredsf command directly.
sf nimbus install
sf nimbus install --version 1.7.0
sf nimbus versionGitHub Actions Pro
The Nimbus action runs your Apex suite on the pull request itself. No Salesforce org is involved — no JWT certificate, no DevHub, no scratch org pool to exhaust, no queue behind another team's deploy. The runner installs a binary and executes the tests locally, on the runner.
One file, one secret:
name: Apex Tests
on:
pull_request:
push:
branches: [main] # publishes the coverage baseline PRs compare against
permissions:
contents: read
actions: read # read the base branch's coverage baseline
pull-requests: write # write the report comment
jobs:
apex-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nimbus-solution/nimbus-test-action@v1
with:
license-key: ${{ secrets.NIMBUS_LICENSE_KEY }}What lands on the pull request
One comment, updated in place on every push rather than appended. Failures come first, because that is what a reader on a red build came for:
- Each failure with its exception type, message, and the assertion's expected and actual values.
- The exact commands to reproduce it locally —
nimbus test "Class.method"and, when a trace reproduced the failure, thenimbus tracereplay one-liner for it. - A coverage delta table against the base branch, showing only the classes that moved.
- A link to the failure traces, downloadable as a job artifact.
- A parity receipt: the Nimbus version, and the corpus totals from the published parity record.
The two triggers do different jobs
pull_request runs the suite and posts the report. push to your default branch runs the suite and publishes the coverage baseline as a job artifact. Pull requests download that artifact and diff against it.
Re-running the base branch inside every pull request would double CI time to recompute a number the base branch already knows, so the action does not do that. Until the base branch has run the action once there is no baseline; the comment reports absolute coverage and says so. That is a normal first-run state, not an error.
Licensing in CI
Running tests in CI requires Pro. This is a hard gate, not a degradation: Nimbus detects the CI environment and refuses to run tests without a Pro licence. The action checks for the key before it starts, so a missing secret produces one clear message instead of a confusing mid-run refusal — and it fails rather than skipping, because a green check on a suite that never ran is worse than a red one.
The Free tier is for local development, where it runs single-threaded and without the XML report formats. Pro is currently free: create an account in the portal and set the key as a repository secret.
CI runs register a single machine per licence per CI provider however many runners execute, so a busy pipeline does not consume licence slots.
Fork pull requests: GitHub withholds repository secrets from pull_request runs that originate in a fork, so the key is empty there however correctly it is wired. The action detects the fork case and says so. Run the suite on push, or restrict the job to same-repo pull requests.
Installation and caching
The binary is cached per version under the runner tool cache. latest is resolved to a concrete version before the cache key is built, so a cache entry can never pin a stale “latest”. Every download is checked against the release's checksums.txt; an asset missing from that file is refused rather than installed. Pin version when you want runs to be byte-reproducible.
Linux and macOS runners are supported. On Windows, install with nimbus-solution/setup-nimbus and call nimbus testdirectly.
Driving the CLI yourself
The action is a convenience over the CLI, not a requirement. For GitLab CI, Jenkins, or a pipeline that already has its own reporting, install the binary and call nimbus test:
- uses: nimbus-solution/setup-nimbus@v1
- name: Run tests
run: |
nimbus test "*" \
--json \
--coverage --coverage-output none --coverage-report coverage.xml \
--results-xml results.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ci--coverage-output none suppresses the inline coverage panel so stdout carries the JSON envelope and nothing else. Set NIMBUS_PROFILE=ci to activate CI-specific settings from your nimbus.properties. For GitLab CI, SonarQube, external database setup, and caching, see the full CI/CD guide.
Action Reference Pro
Inputs
| Flag | Default | Description |
|---|---|---|
version | latest | Nimbus version to install, or "latest". Pin it to make runs reproducible |
license-key | - | Pro licence key. Required — CI test execution is Pro-gated |
test-pattern | * | Pattern passed to nimbus test, e.g. AccountServiceTest or Calculator* |
coverage | true | Collect coverage and compute a delta against the base branch |
trace-on-failure | true | Record a replayable trace per failing test and upload it as an artifact |
working-directory | . | Directory containing sfdx-project.json |
comment | true | Post and update one sticky comment on the pull request |
comment-key | nimbus | Distinguishes comments when one PR runs the action more than once (a matrix, or several packages) |
github-token | github.token | Token for reading base-branch artifacts and writing the comment |
parallel | - | Worker count. Empty uses the Nimbus default (NumCPU on Pro) |
fail-on-test-failure | true | Fail the job when tests fail. The comment is posted either way |
agent-fixtures | auto | Run Agentforce conversation fixtures (**/*.agenttest.yaml). "auto" runs them only when the repository carries any, so a repository with no agent pays nothing and the comment says nothing. Give a path to run one file or directory; "false" to skip |
Outputs
| Flag | Default | Description |
|---|---|---|
version | - | The Nimbus version that was installed |
status | - | Run status reported by Nimbus: "passed" or "failed" |
total | - | Total tests executed |
passed | - | Tests passed |
failed | - | Tests failed |
results-json | - | Path to the extracted nimbus test --json envelope |
coverage-percent | - | Line coverage percent for this run, empty when coverage is off |
coverage-delta | - | Percentage-point delta vs the base branch, empty when there is no baseline |
agent-status | - | Status of the Agentforce fixture run: "passed" or "failed". Empty when no fixtures ran |
agent-total | - | Agent action cases executed. Empty when no fixtures ran |
agent-failed | - | Agent action cases that failed. Empty when no fixtures ran |
Artifacts
| Flag | Default | Description |
|---|---|---|
nimbus-traces-<key>-<run> | 14 days | Replayable traces for failing tests. Unzip into .nimbus/traces/ and replay with nimbus trace |
nimbus-results-<key>-<run> | 14 days | The JSON envelope, JUnit XML, and coverage reports for the run |
nimbus-coverage-baseline-<key> | 90 days | Published by the default branch only. The yardstick pull requests are measured against |
Permissions
contents: read to check out, actions: read to fetch the base branch's coverage baseline, and pull-requests: write to post the comment. Without actions: read the run still succeeds — the delta reports as unavailable rather than failing the job.
Release in CI Pro
The assured release workflow is two commands with a human decision between them, and it is built to run across two separate CI jobs.nimbus release validate runs the local gates and a Salesforce check-only validation, then writes an immutable receipt (.nimbus/releases/<releaseId>.json) and its content-addressed bundle. A later nimbus release deploy deploys that exact receipt — it does not re-validate. It reuses the validated org-side job where Salesforce supports quick-deploy and otherwise re-sends the stored bundle, so the deployed bytes are always the validated bytes.
The split is deliberate: validate and deploy run in different jobs on different runners, so a platform approval can sit between them. The receipt and bundle travel as a pipeline artifact — uploaded after validate, downloaded unchanged into the gated deploy job. The receipt is self-verifying: at deploy time Nimbus checks the org fingerprint, the quick-deploy expiry, the bundle digest, and the toolchain version policy, and fails closed on any drift. That verification is the safety net for the artifact hand-off.
The human gate is your platform's own environment protection — GitHub required reviewers, GitLab manual jobs, an Azure Environment approval check, or a shell prompt. Nimbus's --confirm-production is not that approval; it is the non-interactive acknowledgement that the gate was already passed, needed because a CI runner has no terminal to type the confirmation into.
Ready-to-copy templates for the four major platforms:
- GitHub Actions — a gated
environment: productiondeploy job. - GitLab CI/CD — a
when: manualdeploy on a protected environment. - Azure DevOps — a deployment job to an Environment with an approval check.
- Generic shell — a portable two-phase script for self-hosted runners.
All four read the same ci release profile and pin the Salesforce CLI with nimbus toolchain sf install --version + nimbus toolchain sf status, so the runner provably reproduces the configured toolchain. See therelease-in-CI guidefor the secrets, the profile keys, and the exit-code contract.
Coverage Reports
Feature overview: code coverage.
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 |
Marketing User | 00e000000000007AAA | 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 System Administrator, which is who runs a suite against a scratch org or sandbox. Override it with nimbus.mock.user-profile when your tests target a restricted persona.
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, and null when that profile names no default. Nimbus reads the default from the recordTypeVisibilities in your project's *.profile-meta.xml, matching the profile named by nimbus.mock.user-profile (or the impersonated user's profile inside System.runAs). If your profiles aren't in source, set per-object defaults explicitly — this also overrides whatever the profile says:
# 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.
These rows are never inserted into the database. They are handed to getAll() / getInstance() during tests, so SOQL against the setting's table still starts empty and each test stays isolated. Naming any other SObject here does nothing — use nimbus.seed.row below.
Arbitrary org-resident rows
Some setup data exists in the org, is not in your repo metadata, and has no dedicated seeder — OrgWideEmailAddress, an org-only PermissionSetGroup, BusinessHours, Holiday. Code that dereferences one of these directly fails uncatchably when the row is missing. nimbus.seed.row inserts a real database row for any object:
# nimbus.properties — format: nimbus.seed.row.<Object>.<Name>=<Field=value,...>
nimbus.seed.row.OrgWideEmailAddress.Support=Address=noreply@acme.com,DisplayName=Acme Support
nimbus.seed.row.PermissionSetGroup.For_Everyone=MasterLabel=For Everyone
nimbus.seed.row.BusinessHours.Weekend=IsActive=trueThe Name half of the key fills the object's identifying column — DeveloperName where it has one, otherwise Name — so a lookup by that name resolves without repeating it in the value. Values coerce to the column's type (true/false, numbers, null); a field your local schema doesn't have is dropped rather than failing the whole row. IDs are deterministic, so re-running reuses the same row.
Rows are seeded once per environment, not per test. That is safe because each test runs in a transaction that rolls back, so a test deleting a seeded row doesn't affect its neighbours.
Everything on this page seeds for nimbus exec, nimbus app, the language server, the debugger and the MCP bridge as well as for nimbus test — so anonymous Apex is a reliable way to check whether a seed key took.
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.
Feature-gated standard fields
Standard object definitions ship with nimbus, but they cannot know which features your org has switched on. Case.EntitlementId and Case.ServiceContractId exist only with Entitlements and Service Contracts enabled; Field Service and CPQ add their own. Pass an org and sync folds in whatever it has that the bundle lacks:
nimbus sync -o myorgThe merge is additive — the bundled describes stay authoritative for everything they already carry, so behaviour never depends on which org you last synced against. What the org adds is remembered, so later runs keep those fields without needing the org again. Run this once after connecting an org, or after enabling a feature in it.
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).
Pulling permission sets from the org
Most orgs grant object access through permission sets rather than profiles. If your tests build a user, assign a permission set or permission set group, and then query under WITH USER_MODE or inside System.runAs, nimbus needs to know what those grants contain:
nimbus sync --include-permissionsThis pulls PermissionSet, their ObjectPermissions and FieldPermissions, and PermissionSetGroup composition into .nimbus/permissions.json, which the test runner seeds before each run. Profile-owned rows are excluded — every profile has a shadow permission set on the platform, and nimbus models profiles separately through their default footprint.
Permission sets defined in your own project metadata always win: local .permissionset-meta.xml files are loaded after the synced data, so the repo stays the source of truth and the sync only fills what isn't checked in.
Without this, a permission set group whose contents nimbus has no record of is treated as unknown rather than empty, and access is left unrestricted — better a missed denial than failing a test that passes in the org. Once permissions are synced, groups are enforced for real.
Pulling record types from the org
Record types are otherwise read from your project source, and source can only say what exists. For your own custom objects that's the whole definition — an object shipping no record type has none. For a standard or packaged object the record types live in the org, so silence means nimbus wasn't told, not that there are none. Contact has none on one org and several on the next.
nimbus sync --include-record-typesOne unfiltered query covers every object at once, and that's the point: an object's absence from the org's complete record-type set is evidence it has none. The snapshot lands in .nimbus/record-types.json.
What it buys you is faithful rejection of RecordTypeId on objects that don't have it. The platform exposes that column only on objects with record types; naming it elsewhere is a compile error, not a null read. Without a snapshot nimbus can only enforce that on objects your project defines, so SELECT RecordTypeId FROM Contact passes locally and fails to deploy. With one, it's rejected here too.
Record types defined in your own project metadata always win, per object: if your source defines any record type for an object, that source is the whole definition for it and the org's rows aren't mixed in. Org record type Ids are discarded and the deterministic local ones re-derived, so Ids stay stable across machines and joins keep working.
If the org paginates the result, the snapshot is marked partial and reported as such. A partial snapshot still supplies its rows but is never read as proof that an object has no record types — a gap in the data must not become a rejection.
The same pull also fetches your running user's profile into .nimbus/org-profiles/, for its record type defaults. An insert that leaves RecordTypeId unset picks up the default from that profile, and a default is something only a profile can express — a permission set can make a record type visible but has no way to mark one default. So a project that syncs permissions rather than committing profiles to source had no route to them, and inserts left RecordTypeId null where the org sets it.
Profiles in your own project source still win. And note this cannot be done with a profile alone: the Metadata API reports a profile's record type visibilities only for record types named in the same request, so asking for the profile by itself returns one that appears to grant nothing — which is why the two are pulled together.
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. Syncing with an org connected additionally folds in feature-gated standard fields the org has enabled (Entitlements, Field Service, CPQ). |
.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.
Member completion, hover and signature help cover the Apex standard library — full curated signatures and one-line documentation for the core types, and name-level completion for the long tail — plus your own classes, including chained calls, generics and inherited members. The same server also serves LWC templates and modules inside lwc/ folders: c- component tags and attributes from the workspace inventory, template directives, {binding} completion from the component's own class, and @salesforce/apex import completion sourced from the workspace's actual @AuraEnabled methods. Completing a member of one of your own classes carries the first sentence of its ApexDoc as the item's documentation.
A component's class may be JavaScript or TypeScript, and the whole LWC surface reads either: a <name>.ts class supplies the same {binding} completions, the same @api attribute list to a parent's template, the same LWC003 unknown-binding diagnostic, and the same @salesforce/apex import completion inside the module. Type annotations, accessibility modifiers, optional (recordId?: string) and definite-assignment (label!: string) members, typed getters and generic signatures all read as the members they declare. A template completing against a TypeScript child, or a TypeScript component composing a JavaScript sibling, is the same lookup either way.
It serves Visualforce too — .page and .component files, wherever they live. Component tags and their attributes (required first, with types), attribute values where the domain is closed (mode, layout, booleans, action targets), and {!expression} completion resolved from the page's own controller stack — controller, every extensions class, getter conventions (getTotal() completes as {!Total}), standardController merge fields from the synced schema, and globals whose contents nimbus can actually see on disk: $Label from your .labels-meta.xml, $Resource from staticresources/, $Page from your other pages, $Component from the id= values in the buffer. Every item says where it came from. Components are split into the ones nimbus's own renderer composes locally — the same table nimbus app previews from — and the org-only long tail, labelled so a completion list is never mistaken for a promise that a preview will render it. Diagnostics (nimbus/visualforce) flag an unknown apex: component, an attribute a component does not have, and a {!binding} the controller stack provably does not declare — each one silent unless it can prove the case, and scored at zero findings across 288 real Visualforce files.
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). Faded-text hints on unused local variables (nimbus/unused) — a name is only flagged when it appears nowhere else in the file, and a variable read from a dynamic-SOQL :bind inside a string counts as used. Source labels nimbus/parse, nimbus/soql, nimbus/dead-code, nimbus/unused, or nimbus/sema. |
| 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 — plus your own ApexDoc: @description (or a plain lead paragraph), a parameter list matched against the real signature, @return, @throws, @see, @example as a code block, and tags like @author/@group verbatim. Works cross-file — the documented class doesn't have to be open. A @param naming a parameter the signature no longer has is marked as such rather than rendered as if it were real. |
| Signature help | Floating parameter tooltip while typing a method call. Active parameter bolded, comma-counted even through generics like List<Map<String, Object>>. For your own methods, each slot carries its @param text. |
| Semantic tokens | Theme-aware highlighting: annotations, keywords, stdlib namespaces, types, numbers, comments, strings. Custom required modifier reserved for <required>true</required> fields. |
| Formatting | Whole-document, range (format selection) and on-type (closing-brace re-indent). Indentation, spacing, blank lines and inline SOQL layout, configured per project. See Formatting. |
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. |
| Remove unused variable | On an unused-local hint. Removes the declaration outright when the initializer can't do anything (a literal, a field read, an SObject or collection construction); strips it down to the bare call when the initializer is a call, so the call survives; offers nothing at all when neither is true. |
| Fix a modifier the platform rejects | On nimbus/sema warnings whose repair is unambiguous: adds static to a @future/@InvocableMethod/@TestSetup/@IsTest/webservice/@RemoteAction/@Http* method; makes an @InvocableMethod public; removes static from a constructor, virtual from a static method, or an illegal modifier from a parameter; rewrites System.String to String; marks the defining type abstract when it holds an abstract method. |
| Qualify a static call | On a static method reached through an instance: rewrites the receiver to the type name. Only offered when exactly one call on the line matches. |
| Did you mean… | On a member that doesn't exist on its receiver but has a near-neighbour that does. Only offered where the member list is provably complete — an SObject with a synced schema, or a user class whose whole inheritance chain resolves in the workspace. |
| Generate ApexDoc | On an undocumented method or class: inserts a /** */ stub with @description, one @param per real parameter, and @return when something is returned. Lands above the annotations, indented with the member. Never offered on a declaration that already carries a comment. |
| 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). |
| Extract variable | On a selected expression: hoists <Type> name = expr; above the statement and replaces the selection. The declared type is inferred from the surrounding slot first (a [SELECT …] assigned to Account acc is an Account) and from the expression's structure otherwise. Not offered when the type cannot be inferred, or when the hoist would change the program — a while / C-style for header re-evaluates, a when value must stay constant. |
| Extract constant | On a selected literal: private static final <Type> NAME = literal; at the top of the class body, named in SCREAMING_SNAKE from the literal's content. A separate N occurrences action replaces every copy in the same class. |
| Extract method | On a selected run of complete statements: a new private method after the current one, static-ness inherited. Parameters are the locals the selection reads that were declared above it; the return value is the single local it produces that the code below still uses. Not offered when two locals would need returning (Apex has no tuples), when the selection contains a return, or when a break/continue's loop is outside it. |
| Inline variable | On a local's declaration or any use: substitutes the initialiser, parenthesised where precedence needs it, and removes the declaration. Not offered for a reassigned variable, for an effectful or allocating initialiser facing more than one use, or when anything the initialiser reads is mutated before that use. |
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, inline- method, and long-tail protocol polish (pull-model diagnostics, completion resolve, cross-workspace monikers). SeeLSP_ROADMAP.md in the source for the full plan.
Formatting
Three LSP formatting capabilities: whole-document (textDocument/formatting), range — format-selection — (textDocument/rangeFormatting), and on-type re-indent of a closing brace (textDocument/onTypeFormatting, registered for the closing brace only).
What it guarantees
The formatter only ever changes whitespace. Every token it writes is a token it read, so the code after formatting parses to the same thing as the code before — including inside [SELECT …] literals, where whitespace is significant and a careless formatter would quietly rewrite 2024-01-01T00:00:00Z or LAST_N_DAYS:30. Formatting is also idempotent: running it twice gives the same result as running it once. Both properties are enforced on every release against the full open-source Apex corpus — 24,343 files.
Two things make it safe to leave on for format-on-save. A file that does not parse gets no edits at all, rather than a partial re-layout of a token stream you are halfway through typing. And any region between // nimbus-format:off and // nimbus-format:on comes back byte-identical:
// nimbus-format:off
private static final Integer[][] KERNEL = new Integer[][]{
new Integer[]{ -1, -1, -1 },
new Integer[]{ -1, 8, -1 },
new Integer[]{ -1, -1, -1 }
};
// nimbus-format:onWhat it does not do
It does not move braces — Apex brace style is settled, and offering alternatives would only produce diff noise between teams. It does not re-flow your line breaks either: a statement you broke across lines stays broken, and a query you spread over five lines stays over five lines. The formatter adds line breaks (long queries, optionally long call chains) and fixes indentation and spacing; it does not overrule where you chose to breathe.
Configuration
Every key goes in nimbus.properties at the project root, so a team shares one answer and CI can check it. Project settings outrank your editor's own tab width on purpose: an editor configured for 2 spaces must not quietly reformat everyone else's 4-space code. Keys you leave out fall through to the editor's settings, then to the defaults below.
| Property | Default | What it does |
|---|---|---|
nimbus.format.indent-size | 4 | Width of one indent level, in spaces. |
nimbus.format.use-tabs | false | Indent with one tab per level instead of spaces. |
nimbus.format.max-blank-lines | 2 | Cap on consecutive blank lines. 0 removes them all. |
nimbus.format.spaces-around-operators | true | Normalises spacing: a=b+c becomes a = b + c, generics stay tight (Map<String, List<Id>>), casts get one space ((Integer) o), unary and postfix operators stay attached. Set false to reproduce your spacing exactly and fix only indentation, trailing whitespace and blank runs. |
nimbus.format.soql.style | aligned | aligned breaks a query at its clause keywords when it exceeds the width below, or when you already broke it. single-line always collapses to one line. preserve leaves queries byte-identical. |
nimbus.format.soql.max-line-length | 120 | Width past which an aligned query breaks. A SELECT list still longer than this stacks one field per line. |
nimbus.format.annotations | preserve | own-line moves an annotation off its declaration (@TestVisible private String x; → two lines). preserve leaves it where you put it. |
nimbus.format.chain-wrap-threshold | 0 (off) | Width past which a method chain of two or more call links breaks before each link. Qualified names (Schema.SObjectType.Account) are never broken. |
nimbus.format.trim-trailing-whitespace | true | Strip trailing spaces and tabs from every line. |
nimbus.format.insert-final-newline | true | End the file with exactly one newline. |
SOQL layout
A query that fits stays on its line. One that does not is broken at clause keywords:
// before
List<Account> found = [SELECT Id, Name, Industry, BillingCity, Phone FROM Account WHERE Name LIKE :term AND Id IN :ids ORDER BY Name LIMIT 100];
// after
List<Account> found = [
SELECT Id, Name, Industry, BillingCity, Phone
FROM Account
WHERE Name LIKE :term AND Id IN :ids
ORDER BY Name
LIMIT 100
];When the SELECT list alone is longer than soql.max-line-length, the fields stack:
List<Account> found = [
SELECT
Id,
Name,
Industry,
BillingCity,
BillingCountry,
Phone,
Website
FROM Account
];Example
# nimbus.properties
nimbus.format.indent-size=4
nimbus.format.max-blank-lines=1
nimbus.format.soql.max-line-length=100
nimbus.format.chain-wrap-threshold=100
# Reproduce the pre-1.x layout exactly
#nimbus.format.spaces-around-operators=false
#nimbus.format.soql.style=preservenimbus config properties lists these alongside every other supported key, and nimbus config init writes a commented example file.
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.
New install? Open Help → Welcome → Get Started with Nimbus for a seven-step walkthrough covering the test runner, watch mode, coverage, the dependency graph, and the sidebar views. Common actions — run tests in file, validate, execute anonymous, dependency graph — are also on the right-click menu in any Apex file, and the dependency graph has a dedicated hierarchy icon in the editor title bar.
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 via nimbus dap — breakpoints, stepping, expression evaluation, variable inspection and editing, and the call stack.
Debug a test method
- Open a test class
- Set breakpoints by clicking the gutter on any line — right-click a breakpoint to add a condition, hit count, or turn it into a log point
- 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 — edit a value in place, or add expressions to Watch; view the call stack in Call Stack
The Breakpoints pane also lets you break on uncaught or caught exceptions, each independently filterable to specific Apex exception types.
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 — see nimbus dap for the full argument reference, including method-mode and trace-replay sessions:
{
"type": "nimbus-apex",
"request": "launch",
"name": "Debug My Test",
"program": "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
Feature overview: Nimbus for JetBrains.
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, wired through LSP4IJ to nimbus dap: set line breakpoints in.cls/.trigger files — with conditions, hit counts, or as log points — then Debug a test. Step over/into/out, evaluate expressions in the console or as watches, inspect and edit variables, view the call stack, and break on caught/uncaught exceptions, all driven by the local runtime. Recorded traces can also be replayed — stepping forwards and backwards through a past run with no Apex re-executing. A default-off Use the legacy debugger setting falls back to the pre-DAP daemon bridge if needed.
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.
Performance Lens
Tick Record per-line evidence on a Nimbus Apex Test run configuration, then turn on Nimbus → Toggle Performance Lens. Lines the run reached carry a muted end-of-line reading of what they spent — executions, SOQL, DML and database time.
A line the run never reached shows nothing, and a project with no recording shows nothing anywhere. The lens never falls back to zeros: a line that ran and issued no queries and a line no test came near are different claims, and only the recording can tell them apart.
A recording run goes through the CLI rather than the resident daemon, so its test tree appears when the run finishes instead of filling in as tests complete, and coverage gutters are not refreshed by it. The console says so on every recording run. The same numbers are readable from a terminal with nimbus coverage lines.
Rename Field or Object
Right-click a *.field-meta.xml file or an object directory in the Project view, or open Nimbus → Rename Field/Object…, and Nimbus plans the rename across Apex, SOQL, metadata XML, LWC, Aura and Visualforce before writing anything.
The preview dialog shows the same plan the CLI prints: edits grouped by kind, each one carrying the reason that occurrence was believed to mean your field, and a separate UNRESOLVED section listing every mention that could not be attributed. Those are never edited and never hidden. Apply writes the plan and refreshes the project tree.
This renames your source. It does not touch the org: after applying, deploy, and use Setup → Object Manager for the org-side rename that moves the data. It is also not wired into the IDE's own Refactor → Rename in this release — the explicit action is what can show the refusals honestly.
Tool Window
The Nimbus tool window (right dock) has seven tabs:
- Tests — every discovered
@IsTestclass → method; double-click to jump to source, or run/debug the selection from the toolbar. - Coverage — per-class line coverage from the last coverage-enabled run.
- History — past runs → classes → methods, with a Trends chart and run comparison.
- Schema — the local PostgreSQL tables, columns, and PK/FK relationships.
- Workbench — the rows themselves, in an editable grid. See below.
- Releases — validation receipts and the deploy queue for
nimbus releaseworkflows. - Governor — per-method SOQL/DML usage vs. limits from the last run.
A graph button on the tool-window header opens the interactive dependency graph — also available from the Apex editor right-click menu and Nimbus → Show Dependency Graph.
Graph, trace, Trends, and run-diff visualizations open in embedded browser panels (JCEF), the same views shipped in the VSCode extension.
Data Workbench
The Workbench tab is the local database's rows in an editable grid: pick an object from the searchable list, page through its rows, sort by clicking a header, filter with the search box, and edit a cell in place — Enter commits, Escape reverts.
A cell edit is not a SQL update. Nimbus runs it as Apex DML on the same interpreter a test uses, so triggers fire, before-save flows run, and a validation rule that would have rejected the change rejects it here too, with the platform's own message. Each write is a single DML statement, so a refusal leaves the row exactly as it was and the grid reverts to it. Add Row and Delete Selected follow the same rule; deleting asks first and names what is not affected.
Save as Fixture… turns the rows in view — or just the selected ones — into an @IsTest Apex class beside your other classes: the same artifact nimbus fixture writes, and it opens in the editor once written. Lookup fields keep the Ids they have now, so they resolve on the branch they were captured from.
The header carries a branch control over the nimbus data branch family: switch the whole panel onto another copy of the local data, or create one from the current branch. A branch another nimbus process is mid-run on is reported as in use rather than as a failure — nimbus never severs somebody else's run. Switching is only offered when the project uses nimbus's own embedded database.
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 |
Use the legacy debugger | false | Fall back to the pre-DAP daemon debug bridge instead of nimbus dap |