Changelog

What's changed.

Every release, every fix. Nimbus is in active development — this page tracks every change since the first public release.

August 24, 2026
1.26.0
Added
  • nimbus bisect — find the commit that broke a test. Give it a failing test and a known-good commit and it walks the history, running the test at each candidate, and names the culprit along with the command to reproduce it. Every candidate runs in a temporary worktree with its own database, so your checkout never moves and nothing you have open is disturbed; commits from before the test existed are skipped rather than counted as passes. --json keeps stdout clean for CI.
  • nimbus fuzz — property-based testing for triggers. Generates records that are valid according to your real describes — required fields, restricted picklists, lengths, precision, one level of required parents — salted with the values that break things: nulls, boundaries, unicode. When something breaks it shrinks the input to the smallest case that still fails and prints it as paste-ready Apex. A validation rule rejecting bad data counts as the object working, not as a find. --seed replays a run byte-for-byte. Around 4 ms an iteration, which is why this can exist locally and cannot exist against an org.
  • nimbus compare — run the same Apex or SOQL here and in an org, and see what differs. Results are compared structurally, and the comparison tells you what it excluded rather than pretending to compare it: record Ids are never comparable across sides, nor are audit stamps, nor row order when no ORDER BY makes it contractual. Differences it can explain it explains; differences it cannot are reported as unknown instead of guessed. Equal rows with differing cells are never called an engine bug on results alone.
  • nimbus budget init and nimbus budget check — governor budgets that fail CI before they fail in production. init measures a suite's per-test governor usage and writes nimbus.budgets.json with headroom; check re-runs and fails with the test, counter, usage, budget and overshoot named — plus which budget entry governed, since resolution is per-counter with exact names beating wildcards beating defaults. CPU budgets are checkable in CI at all because nimbus counts operations rather than wall time. The gate is honest about the platform's blind spot: Test.startTest resets the counters, and both commands say so rather than quietly passing a test that measured nothing.
  • Flow debugging — breakpoints in the flow XML itself. Set a breakpoint anywhere inside an element's block in a .flow-meta.xml and execution stops before that element runs. Paused, you see the flow frame, $Record and $Record__Prior, the flow's variables, and the element's type, label and next connector. Step lands on the branch a decision actually took, step-in enters an Apex action, and a test's own insert stops mid-pipeline inside the flow it fires. Flow node coverage is recorded per run.
  • A NullPointerException now tells you why it was null. When Apex dereferences null, the failure explains the cause from the values actually in scope at the raise site — never guessed from the message text. The common one reads: "c.Account was null. The Contact was loaded by the query at ContactService.cls:5, which selected Id, LastName — not Account." Eight cause kinds cover unset fields, null variables with the site that last changed them, null elements, null returns and null operands, naming only what is known. The explanation rides beside the exception, never inside it — getMessage() is byte-identical to before. It appears in the runner's failure block, in nimbus explain, and as a trace event.
  • A real Apex debugger, over the standard debug protocol. nimbus dap is now a documented command, and it has grown up: watches and a debug console that run full Apex — method calls, SOQL, DML — against the hermetic local database, which no org-backed debugger can offer; variable editing that accepts whole expressions and writes into locals, list elements, map entries and SObject fields; conditional, hit-count, logpoint and exception breakpoints that behave as documented; a pause request that works; per-frame Locals, Arguments, this and Statics instead of one flattened bag; and Step Back over a recorded trace. Large collections cost nothing until you expand them.
  • Traces show the order of execution, and what each step cost. Triggers, record-triggered flows, validation rules, roll-up summaries, DML verbs and async jobs all emit spans now, nested under whatever fired them — so a trace reads insert → before trigger → its query, with the after trigger and flows as siblings. Each span carries the governor resources it consumed as deltas — SOQL, query rows, DML statements, DML rows, CPU — so a trace answers which step spent the limit, not just the test total. Updates additionally carry a field-level before/after diff per record, with bookkeeping fields excluded so the fields your code changed are not buried.
  • Every field write records who wrote it. A new class of trace event captures each write to a record field — user assignments, constructor arguments, dynamic put(), metadata defaults applied at insert, flow write-backs, roll-up recomputes (carrying the definition that produced them), formula resolves inside the save pipeline, and platform stamps — each with the record's identity, the field, the old and new values, the kind of writer and its source location, in a per-test total order. A record keeps one identity from the caller's constructor through a before-trigger's overwrite to the save, so the whole story of a value reads in sequence.
  • Preview Lightning Web Components and Visualforce locally, with no org. nimbus app is now daemon-managed: it boots a bundle's dev server as a supervised child, binds the port before configuring it so the address it reports is real, treats a clean exit before ready as the failure it is, and sweeps every server on shutdown. LWC bundles and Visualforce pages are both previewable as targets, and every API call the running app makes — method, path, status, duration — streams out live.
  • A local API request and the Apex it ran share one trace. Requests served by the local API surface now open a root span with the interpreter running underneath it, so a trace shows this GET /services/apexrest/Ping ran PingResource.doGet, which called PingHelper.compute rather than two unrelated views of the same second. The request feed carries trace and span ids so a client can follow one to the other. Off by default; untraced requests allocate nothing.
  • Editor completion resolves classes from the whole workspace. Typing OtherClass. or someVar. where the type lives in another file completed nothing — completion never consulted the workspace index that go-to-definition already used. It now offers statics on type receivers and instance members on typed variables, with same-file names shadowing workspace names, and respects Apex visibility including @TestVisible inside test classes.
  • Semantic diagnostics appear in the editor. The analyzer nimbus validate already runs now publishes through the language server as warnings. Rules whose verdict needs types from outside the file are excluded — they produce tens of thousands of false rejects without a full registry — while the file-local rules score zero false positives on every deployable project tested, which is what makes them safe on by default. Toggle with nimbus.diagnostics.sema.
  • The dependency graph shows its evidence. Clicking an observed edge answers how do we know: the tests that cover it and the file that witnessed it. Nodes can show a coverage percentage behind a toggle, drawn as an amber ring — never green, which stays reserved for tests that passed.
Fixed
  • Network.getNetworkId() returns null outside a community context, which is what the platform returns. It was returning an empty string — harmless until 1.25.0 began validating Id assignments, at which point ordinary Id networkId = Network.getNetworkId(); failed and took down every test that touched the class holding it. #458
  • Id.valueOf checks the 18-character checksum only from API 54.0 onward. The check is version-gated on the platform, and applying it to every class broke code compiled before the cutoff. Verified by deploying one probe class per API version: the behaviour flips at exactly 53 → 54. #429
  • Custom metadata record Ids are the canonical 18 characters. They were 15, and the length was the least of it — the same record's Id read back as 15 or 18 depending on the static type of the collection holding it, so querying a record by the Id it had just handed you returned nothing. #433
  • RecordTypeId describes as a reference, with the platform's label, and appears in the object's field list — on the objects that actually have record types, and absent on the ones that do not. It was describing as a string with a generated label. #432
  • A Map argument's key type now selects between overloads. A class declaring both f(Map<Id,Object>) and f(Map<String,Object>) resolved every call to whichever was declared first; the platform resolves the pair by the key's declared type.
  • stub pull records what the org actually holds. A relationship to another custom metadata record is recorded by the target's developer name rather than its Id, so traversals resolve (#437); recorded numbers are written as literals Apex can parse instead of scientific notation (#438); a dotted receiver in your own code is confirmed to be a namespace before it is queried (#439); and --namespaces "" no longer quietly becomes an org-wide pull (#441).
  • Governor usage accounts for work done inside Test.startTest. The platform resets the counters around that block, and nimbus reproduces that faithfully — which meant a reading taken at the end of a test reported zero for the many tests whose whole body sits inside it. Usage is now tracked as a high-water mark, so budgets and reports see what the test really spent.
  • A conditional breakpoint's condition is honoured rather than silently ignored, and a condition that cannot be evaluated says so once instead of quietly changing behaviour. #456
  • Trace trees nest by actual call structure. A span set the current context on entry and never put the previous one back, so everything created after a call returned was parented under the call that had already ended.
  • nimbus config properties and nimbus doctor agree with the code. Thirteen real configuration keys were missing from the registry, so they never appeared in the listing or the scaffolded example file; doctor warned about working per-profile release keys; and two documented defaults named settings that do not exist.
  • The local preview server starts cleanly or explains why it did not. Node compatibility is checked before launch instead of failing obscurely, each preview owns its own endpoint, launch diagnostics name the actual problem, and the Data SDK's CSRF bootstrap is served.
August 22, 2026
1.25.0
Added
  • SOQL and Apex consoles in the IntelliJ plugin. Run a query or a block of anonymous Apex against your local database from the tool window. The console shares the same configuration your tests run under, so a query answers there exactly as it would inside @isTest — previously it saw none of the seeded data or stub namespaces a test sees.
Changed
  • A child subquery that matched nothing now returns null, not an empty list. That is what the platform returns, and code guarding with != null before indexing depends on it. If you have code that iterates the result without a null check, it was relying on nimbus being more forgiving than the org and will now need the same guard it needs in production. Reading a relationship the query never selected raises SObjectException for the same reason — though only for relationships nimbus can describe, so an unrecognised name stays permissive rather than guessing.
  • Errors added to a record are reported in the order they were added. They were being returned reversed, so a record with two problems reported the second one first — and DmlException led with the wrong message. Assertions written against the first error will now see the error your code added first.
  • Assigning a malformed value to an Id variable now fails. Id x = 'foo'; was silently accepted, while the equivalent cast and Id.valueOf both rejected it. The declared-type assignment was doing half the work — expanding a 15-character Id to 18 — and skipping the validation.
  • Values too long for their field raise a proper error. Saving an over-long value surfaced raw database text; it now reports STRING_TOO_LONG with the field label and maximum length, matching the platform. Validation runs before your before-save logic, so a trigger cannot rescue an over-long value by trimming it — the save is refused first, exactly as it is on the org.
  • Deleting a record you do not own is refused under System.runAs. Public Read/Write grants read and edit but not delete; delete needs ownership. This applies only in user mode — an explicit system-mode or without sharing operation is unaffected.
  • Deleting a record clears lookups that point at it. Clearing a lookup on delete is the platform default rather than something a field opts into, and nimbus was treating it as opt-in — so those fields kept pointing at deleted records. Undeleting the parent restores them, and a child repointed in the meantime keeps its new parent.
  • Datetime.format() renders in the running user's timezone. It rendered in GMT, so it disagreed with hour() and date() on the same value.
  • Sets and Maps consult equals(), not just hashCode(). A class whose hashCode() keys on fewer fields than equals() compares — the correct way to write the pair — had distinct instances silently collapse into one entry.
Fixed
  • A record built by copying a blank field from another record no longer skips validation. Copying an empty field produced a value that looked populated, so a required field could be left unset and the save still reported success — returning an Id for a row that was never really there. This is the most serious correctness fix in this release.
  • Updating several records at once derives the same fields as updating one. Saving a list skipped the step that keeps IsWon, IsClosed, Probability and ForecastCategory in step with a stage change, so a single-record update was right and a bulk update was stale — invisible unless you compared the two.
  • Database.upsert(records, false) saves the rows that are valid. A partial-success upsert failed the whole batch and reported the first row's error against every record, so nothing was saved where the platform saves everything that passes.
  • A bind variable joined to a literal works in a filter. WHERE Name LIKE :prefix + '%' reached the database as unfinished SQL and failed outright.
  • Two-argument addError keeps your message. addError(message, false) read the escape flag as the message, so the error surfaced as the word "false" with your real text recorded as a field name.
  • Enum constants report their declared spelling. name() returned whatever casing the caller typed, so a value written into a record and compared against the declared set never matched — and a Map keyed by an enum disagreed with == about its own keys.
  • Aggregate queries name every unaliased column correctly. Only the first row resolved expr0, so grouped results after the first came back under the wrong key.
  • Task.IsClosed follows its status, and roll-ups grouped on it stop reporting zero.
  • Checkbox defaults reach before-insert triggers. A declared default never arrived on the record, so logic branching on whether the field was set took the wrong path.
  • Dependent picklists know which values are valid. The controlling-value mapping was never read from field metadata, so every dependent value appeared invalid — and the mapping is now preserved when a project's schema cache is written.
  • State and country pairs settle the way the platform settles them. Whichever half you write wins and the other is derived; clearing either clears both. The pair is reconciled when the record saves, not before your triggers run.
  • A multi-select picklist containing only separators saves as blank.
  • Numeric formula fields round to their declared scale, so a currency formula compares equal to the number it displays.
  • Double.valueOf returns a Double regardless of where the result is used.
  • Custom metadata queries count against the query limit only when they select a long text area field, matching the platform.
  • Field-level security on an unqualified query follows the code's API version. A rule measured on newer API versions was being applied to every class, so projects on older versions saw denials the platform would not raise.
  • Documents, email templates and their folders are available to query. Controllers that look up a template by folder found nothing, and templates had no body.
  • Sending email to a contact with no address reports the failure instead of reporting success.
  • ApexPages messages behave. addMessages records at error severity, hasMessages(severity) filters by the severity you pass, errors added outside a trigger reach the page, and StandardController.view() returns a page reference instead of null.
  • A field token reports the field's real name rather than the spelling used to reach it, so two references to the same field compare equal.
  • Schema.DescribeFieldResult.getLength() reports a text area's real length instead of a long text area's.
  • A filtered COUNT() over profiles counts matching rows instead of returning zero.
  • nimbus triage reproduces your test run. It built its own configuration, so it could report failures your actual run does not have.
  • BR() renders per context — a line break inside a stored formula field, a newline at runtime.
  • Casting an SObject to an unrelated class raises a catchable TypeException instead of failing later in a way no catch could reach.
August 20, 2026
1.24.0
Changed
  • Tests now run as a System Administrator by default. The mock running user was a Standard User for identity purposes while validation rules were already evaluated as an administrator — two halves of the same run disagreeing about who was executing it. Both now report the same profile, and it is the one most developers actually run their tests under. If a test asserts on the running user's profile, or depends on starting as a restricted user, set nimbus.mock.user-profile in nimbus.properties to pin the old behaviour. Permissions inside System.runAs are unaffected.
  • Profiles now carry their permission sets. Salesforce exposes each profile's access as a permission set owned by that profile, and code that inspects permissions that way — checking whether the current user is an administrator, or building a deliberately restricted user in a test — found nothing to read. Those records now exist, with the object permissions each standard profile actually grants.
Fixed
  • Field help text is available from a describe. getInlineHelpText() always returned null, even though the text ships in both field metadata and the bundled describes. Anything that maps or documents fields through help text — a pattern several managed packages rely on for their own configuration — read nothing and silently did nothing, with no error to show for it.
  • Custom metadata child queries follow the declared relationship. A subquery over custom metadata guessed the joining field from the parent type's name instead of reading the relationship the child declares, so any parent whose lookup field is not named after its parent returned no children at all. Traversing __r inside such a subquery, and an unset checkbox on a child row, are fixed alongside it.
  • Lead conversion carries its side effects. Database.LeadConvert was missing most of its accessors, converting a lead did not copy the lead's address onto a contact that had none, and no primary OpportunityContactRole was created. The mapping fills blank contact fields only and leaves values that are already set alone, matching the platform.
  • SOQL date literals follow Apex's clock. TODAY, LAST_N_DAYS and the rest were resolved against the database's clock rather than the one Date.today() reads. The two agree for most of the day and disagree either side of midnight in timezones ahead of UTC, so a suite that passed all afternoon could fail overnight for no reason anyone could reproduce the next morning. --faketime now reaches them too.
  • A stub can no longer replace something nimbus implements. A generated stub whose name matched a built-in type quietly took its place, and because stubs return empty values the real behaviour disappeared with no error — in one case switching off field-level security stripping across an entire project. Stubs are now only used for types nimbus does not already provide.
  • Managed-package stubs keep their roll-up summaries, formulas and picklist definitions. Only the plain shape of a stubbed field was being read, so a roll-up arrived as an ordinary number: empty where the platform reports zero, and never recalculating when its child records changed.
  • HAVING comparisons against null match the right rows. HAVING SomeField != null matched nothing at all, so aggregate queries written that way came back empty and any roll-up built on one computed zero.
  • Datetime values stay findable in a Set or Map after a round trip through a record. Storing a Datetime on a record trims it to whole seconds, and although the trimmed value still compared equal, Set.contains and Map.get missed it.
  • Mutating a collection and leaving the loop is allowed. Apex raises its error on the next step of a loop, not at the moment of the change, so removing an element and then breaking out is legal — nimbus was refusing it.
  • Failed all-or-none DML leaves nothing behind. When a save covering several record types failed partway, earlier groups stayed saved, and the records handed in kept the Ids they had been given — so retrying the same list was rejected for already having them.
  • State and country picklist labels are correct again. Codes shared between countries had collapsed onto a single label, so a state code under one country resolved to a different country's state.
  • Records report only the fields a query asked for. getPopulatedFieldsAsMap() included a relationship the query never traversed, which broke code that inspects a record's populated fields and looks each one up.
  • Field sets return the fields they display. getFields() also returned the fields merely available to add to the set.
  • An SObject type token compares equal to itself. The two ways of writing the same token were not equal to each other, so code that dispatches by comparing them silently took the wrong branch.
  • String sorting works on systems with no locale configured. In that environment — common inside CI containers — every query with an ORDER BY on text failed outright instead of falling back to a simpler sort order.
  • Organization and PermissionSetAssignment can be queried. Both were refused for users without administrative setup access, where the platform allows an ordinary read.
August 17, 2026
1.23.0
Added
  • nimbus record sets up managed-package record/replay in one command. Recording answers calls into stub classes, so a first recording took two commands in a particular order — pull the package's stubs from an org, then run the tests with recording on — and running the second one on its own recorded nothing. nimbus record -o <alias> [pattern] now does both against one org, after which plain nimbus test replays offline with no flag and no org connection. Both halves stay available separately: nimbus stub pull when you want the package's shape without running anything, nimbus test --record to re-record once stubs exist.
  • --target-org is accepted on every command. It is the Salesforce CLI's spelling for what -o and --org already named, and all three are now one flag throughout, so a script written against either tool reads the same and runs unchanged.
Fixed
  • An org named with -o on a deploy or release command is no longer ignored. Those commands read the target org from --target-org only, while -o and --org were accepted on the command line without complaint and then had no effect — so nimbus deploy -o staging could deploy to whichever org a release profile or your Salesforce CLI default named, rather than the one you typed, with nothing in the output to say a different org had been chosen. All three spellings now resolve to the same org on every command. --target-org behaves exactly as it always did.
  • nimbus test --record says when there is nothing for it to record. Recording answers calls into stub classes, so on a project that has none the run finished clean, reported nothing unusual, and left an empty recordings directory behind. It now says so before the tests start and names the command that pulls the stubs first.
August 17, 2026
1.22.2
Fixed
  • nimbus sync -o <alias> reads the org again on a current Salesforce CLI. Recent CLI versions redact the access token from sf org display, handing back a placeholder sentence instead. Nimbus sent that placeholder to the org, got a 401, and reported it as a rejected session — advice to log in again that could not possibly help, since the redaction has nothing to do with your session. The effect was that the org describe augmentation silently never ran: standard objects came from the bundle and every feature-gated field your org has stayed missing. Nimbus now asks the command the CLI names, and accepts both payload shapes that command has used across versions. An older CLI does not redact and is unaffected. #442
  • A bind operand in parentheses is the expression, not a cast. WHERE Name = :(value) matched nothing, and IN :(new List<String>{ … }) failed with an internal name that should never have been visible. :(...) is ambiguous — :(Set<Id>) ids is a cast applied to an operand, :(ids) is the operand in parentheses — and nimbus read every one of them as a cast, so the parenthesised form bound nothing at all. They are now told apart by whether an operand follows the closing paren. Casts are unchanged. #444
  • nimbus validate no longer reports warnings that depend on formatting. A comment inside a SOQL literal became part of the next column name, so the warning named a column that appears nowhere in the file — with the comment text and an escaped newline in it, and the line number pointing at the SELECT rather than the comment. Block comments did it too. Separately, a child subquery whose opening parenthesis sat on its own line had its relationship name reported as an unknown SObject, while the same query written on one line validated clean. Both queries ran correctly throughout, so this was the validator disagreeing with the runtime in the same project. #448 #449
August 17, 2026
1.22.1
Fixed
  • A synced org's packaged fields are no longer demanded on insert. After nimbus sync -o <alias> against an org with a managed package installed, insert new Account(Name = 'x') failed with REQUIRED_FIELD_MISSING naming every packaged Checkbox the org happens to have. A Checkbox is never nillable — a Boolean is always true or false — so the platform expresses "the caller need not supply this" through its defaulted on create answer instead, and nimbus was not carrying that answer: it was dropped when the schema was written to the project cache, and the runtime then fell back to the shipped describes, which cannot know about a field that exists only on your org. Affected every insert of a standard object the package extends, not only the fields it named. Re-syncing after upgrading is not required — the cache regenerates itself — but nimbus sync -o <alias> settles it if anything looks stale. #447
August 17, 2026
1.22.0
Added
  • Record what a managed package really returns, then run offline against it. A stub carries a package class's shape, but its method bodies return type-defaults — fine until a test depends on the actual answer. nimbus test --record -o <alias> now forwards static calls on stub-loaded classes to an org that has the package installed, once, and writes what came back to .nimbus/recordings/ — one plain-JSON file per test method, meant to be committed so the rest of the team and CI run against the same captured answers with no org access at all. Later runs replay them with no flag and no org connection; --no-replay ignores them and runs the stub bodies. Anything that can't be answered falls back to the stub body rather than failing — a call that was never recorded, an argument with no JSON form, a package that has been upgraded since — so adding recordings to a suite cannot break it. Static methods only, since a local stub instance has no counterpart in the org to forward to; callouts stay out, because the platform refuses one in a test without Test.setMock and a deployable test therefore already ships its own mocks.
  • nimbus stub pull generates package classes and records their constants. Previously it fetched fields, objects and labels; it now also writes stubs/<namespace>/<Class>.cls for the classes a package exposes to subscriber code — the real method signatures, constructors and enums — so builder chains construct and calls resolve instead of warning. Global constants are recorded with their actual values from the org. --data additionally captures custom-setting rows and custom-metadata records, which land as ordinary seed data your tests can query.
  • Geolocation fields work end to end. A custom geolocation field now materialises as a System.Location with its latitude and longitude components, and DISTANCE() and GEOLOCATION() translate in both WHERE and ORDER BY — so proximity queries return the rows the platform returns, in the same order, rather than failing to translate. #161
Fixed
  • A 15-character Id is the same record as its 18-character spelling, everywhere. The two spellings now converge at every Id-typed boundary — assignment, casts, map keys, set membership, comparison — where previously only some paths agreed. The visible symptom was a lookup that should have hit and didn't: a Map<Id, SObject> populated from a query and then read with an Id obtained some other way returned null, and the record looked absent rather than differently spelled. #427 #384
  • String.valueOf renders from the argument's declared type, not the value it happens to hold. A Date held in a variable declared Object rendered with a time component, and the same mismatch ran through nested maps, sets and SObjects — so a serialized payload differed from the platform's in a way that only surfaced downstream, in a comparison or an assertion far from the call. #422 #426
  • Cross-object formulas resolve through custom relationships. A formula reaching a parent through a custom __r relationship was left stale after a bulk write, so the child carried a value computed from data that had since changed. #431
  • Custom share records behave like platform sharing state. Database.delete(Id), Database.delete(List<Id>) and Database.undelete(List<Id>) were unsupported for every object and failed outright where the platform accepts them; they now resolve the record and run the ordinary DML path, with the same triggers, limits and sharing treatment as any other caller. On top of that, a share record now describes with no key prefix and as not undeletable, its Id falls in the domain every share table shares, and a share inserted without one reads back RowCause = 'Manual' — all as the platform does. #193
  • Apex sharing reasons resolve to their API name. Schema.MyObject__Share.RowCause.MyReason__c evaluated to null, so any assertion comparing a queried RowCause against a sharing reason was comparing null to null and passing without testing anything. Both share forms now resolve, with or without the Schema. prefix; Schema.AccountShare.RowCause on its own remains the field token it has always been. #193
  • Org-default custom-setting rows survive a cold run. Rows seeded as org defaults were set up only in one phase, so whether they existed depended on whether a warm daemon happened to be running — the same project passed or failed on the same code depending on how it was started.
August 14, 2026
1.21.0
Added
  • Managed-package custom labels resolve, and nimbus stub pull fetches them from an org that has the package installed. System.Label.ns.LabelName previously warned that a label named after the namespace was missing and handed back nothing, so code guarded by a packaged label silently took the wrong branch — most visibly leaving a required field unset and failing rooms away from the cause. Labels now load from a stubs/labels/ directory alongside your project's own: CustomLabels.labels-meta.xml for ordinary labels, and <namespace>.labels-meta.xml where the file name declares the namespace. nimbus stub pull --org <alias> writes those files for you, along with the package's fields and any objects your project references but doesn't define — the org where you hit a managed-package gap is by definition an org that has the package. nimbus stub label scaffolds one by hand. Your own labels always win; a label that resolves nowhere still returns empty with a warning naming what to add, rather than failing the run. #406
  • ApexPages.StandardSetController supports pagination. The controller constructed and then did almost nothing: setPageSize, setPageNumber, next, previous, first and last didn't exist and aborted the run, six getters silently returned null, and getRecords() handed back the QueryLocator it was built from. All of it now behaves as the platform does, verified against a live org: a fresh controller reports page 1 of 20 over the full result size, setPageSize returns to page one, next() past the last page is a no-op rather than an error. Note that getRecords() now returns the current page rather than the whole set, which is the platform's behaviour. #416
  • Standard describes carry the platform's org-wide defaults. Sharing decisions previously fell back to a hand-written table covering eight objects. #410
Fixed
  • A 15-character Id matches in SOQL. WHERE Id = :fifteenCharId returned zero rows — bound, inlined as a literal, or inside an IN list — where the platform treats the two spellings as the same record. The common way to meet this is ApexPages.StandardController.getId(), which correctly returns 15 characters, so the ordinary Visualforce constructor idiom [SELECT … WHERE Id = :controller.getId()] loaded nothing and failed a frame or two later. Both spellings are now matched at the predicate itself, so nothing about how Ids are stored or compared elsewhere changes. Id-typed == between the two spellings is still being worked on separately. #417
  • Malformed and unresolvable Ids are rejected by DML. An Id whose checksum doesn't match its first fifteen characters now fails with MALFORMED_ID, and a well-formed RecordTypeId naming a record type that doesn't exist fails with INVALID_CROSS_REFERENCE_KEY — both matching the platform's wording. Id.valueOf validates the checksum too; assignment and the (Id) cast deliberately do not, because the platform doesn't either. The record-type check only applies where nimbus actually holds record-type data, so a project that doesn't model them is unaffected. #429
  • Map<Integer, V> and Map<Decimal, V> keys keep their type. Built as a literal, such a map handed back String keys. Indexing a list with one threw Index operator not supported; worse, three consequences were silent — list.get(key) returned null, sum += key + 1 concatenated to '041' instead of adding to 5, and sorting the keys ordered 10 before 2. Map<Id, V> is unaffected: its keys are strings on the platform too. #411
  • Cross-object formulas refresh when the parent changes. A formula reading a parent through a lookup was computed when the child was written and never recomputed, so changing the parent's field afterwards left the child's value stale — the platform computes formulas when you read them. #430
  • Queries survive a missing space before FROM. Dynamic SOQL assembled by concatenation often produces …ORDER BY CreatedDate ASC)FROM Account…. SOQL is tokenised rather than whitespace-delimited, so the platform reads that fine; nimbus took the subquery's FROM for the query's own and then rejected every field the query selected, blaming a field on the wrong object. #430
  • A date function alongside a parent reference, COUNT_DISTINCT, and toLabel() all translate. WHERE Account.Name = 'x' AND CALENDAR_YEAR(CloseDate) = 2019 emitted invalid SQL and surfaced a raw database error; COUNT_DISTINCT wasn't treated as an aggregate when it stood alone or appeared in ORDER BY; and toLabel() only worked in its barest form, failing with a mangled entity name when given a field alias, a parent field, or a place in WHERE. #412 #413 #414
  • System classes resolve under their System.-qualified spelling. System.Crypto.getRandomInteger() reported an unknown method while the bare spelling worked — and several others failed silently, with System.Date.today(), System.Datetime.now(), System.String.join() and System.Schema.getGlobalDescribe() returning null or empty. Both spellings are legal Apex, and the qualified one is the habit in namespaced codebases. #418
  • Overload resolution matches a generic type across the Schema. prefix. A List<Schema.FieldSetMember> argument didn't match a List<FieldSetMember> parameter, and rather than failing, resolution quietly fell through to a List<String> overload — so field set members were added to a set of strings and the query built from them came out empty. #428
  • JSON.deserialize accepts ISO date/times without a zone. 2026-05-13T00:00:00.000, the same without milliseconds, and a bare date were all rejected where the platform reads a missing offset as GMT. This bites hardest on the common idiom of seeding a normally read-only audit field, where the throw lands on the deserialize rather than on the field access that motivated it. #415
  • A child subquery's soft-delete guard follows ALL ROWS rather than the text of the WHERE clause. A subquery whose filter merely mentioned IsDeleted lost the guard entirely and returned deleted rows. #409
  • A query's own WHERE is parenthesised before nimbus's guards are added to it. An OR at the top level of a filter could return soft-deleted rows through its non-final arms. #403
  • The project schema cache invalidates from the describe bundle's content. It depended on a constant somebody had to remember to bump, so a corrected describe stayed inert on any project that had already synced. #396
  • Namespaced API names keep their namespace, and date fields order by calendar date. #397
August 13, 2026
1.20.2
Fixed
  • merge reparents related records through relationship fields only. Merging two records updated every column in the database that happened to hold the duplicate's Id — including plain text fields. A Description that contained the merged-out record's Id came back rewritten to the master's Id, where the platform leaves it untouched: reparenting is driven by field metadata, not by matching values. Org-verified on both directions — the child lookup moves to the master, the text field keeps the old Id. Lookups whose targets don't include the merged object's type, and external-Id lookups (which describe as references but hold external keys, never Ids), are left alone. #400
  • DescribeFieldResult.isPermissionable() answers the platform's flag instead of a blanket true. Every field claimed to be permissionable, so code enumerating FLS-manageable fields saw system and audit fields it can't actually manage. The answer now comes from the describe's own flag, measured 99.9% accurate against a live org; fields the platform never permissions — OwnerId, RecordTypeId, LastActivityDate, and IsDeleted — answer false even on custom objects, where no bundled describe exists to say so. #377
  • Attachments and Notes follow per-record access to their parent under sharing. Under with sharing, a user's view of an Attachment or Note is decided by whether they can see the specific parent record — ownership and an explicit share are equally good, and object read on the parent is a separate, additional requirement. Nimbus either showed all of them or none. The rules were org-verified against a Private org-wide default, including the case the platform decides on record access a share row grants to a user who still lacks object read: hidden. Custom-object parents participate for the first time, whatever their sharing model. #393 #171
  • SOQL's NOT placement rules are enforced, and every negation tolerates nulls. The platform refuses WHERE A AND NOT (B) with a parse error but accepts a leading NOT (…) and NOT before a bare comparison; nimbus accepted all of them, so a query that throws QueryException in an org ran locally. Every condition site is now validated — WHERE, HAVING, relationship subqueries, semi-join bodies (where a leading NOT is legal and stays legal), dynamic queries, aggregates, and custom-metadata queries — and each negation matches rows whose field is null exactly as the platform does, including a leading EXCLUDES followed by further conditions. Apex line comments inside a query string no longer confuse the validator. #392
Performance
  • Suites that read packaged fields or merge records run up to several times faster. Two independent costs were being paid per test instead of per run: the inference that types a packaged Checkbox field from the project's own code re-derived its evidence for every test that touched such a field, and every merge issued one UPDATE per column per table in the schema. The largest suite in our open-source compatibility corpus (3,986 tests) went from 98s to 31s wall on 8 workers with a byte-identical failing set; its slowest test — a three-way contact merge — from 38.7s to 3.0s; a DML-library suite from 4.0s to 0.8s. The inference also now draws on the whole project's code deterministically rather than on whichever classes a test happened to load, which flips two relationship-package tests in that corpus suite from failing to passing.
  • nimbus sync is 0.5–1s faster in projects with no configured org. Every sync spawned sf org display — a Node process — to ask about an org alias nimbus itself had already determined doesn't exist. The probe now only runs when an org is actually configured or requested; what sync writes is byte-identical. Test startup also drops ~20% on small projects: two passes over the embedded describe bundle were computing two views of the same data and now share one parallel pass.
August 12, 2026
1.20.1
Fixed
  • Unqualified DML inside System.runAs enforces the running user's object permissions at API 67. Suites that write records as a restricted user will newly fail — that failure is the platform's answer. Summer '26 defaults database operations to user mode, with as system (or AccessLevel.SYSTEM_MODE) as the opt-out. Nimbus enforced user mode only when it was asked for explicitly, so every System.runAs block that wrote a record succeeded locally whatever the running user could actually do — the widest false pass in the tracker. All six statement verbs and the bare Database.* calls now raise System.SecurityException; the partial-success Database calls do not throw and instead fail every row. The API version that decides this is the one on the code unit containing the statement, so an API-66 class still writes when called from API-67 code. Explicit as user is unchanged at every API version, as system waives it, and outside System.runAs nothing changes. The mixed-DML idiom System.runAs(new User(Id = UserInfo.getUserId())) keeps full access. Field-level security under user mode and read CRUD on SOQL are deliberately still unenforced. #388 #168
  • A relationship subquery's WHERE clause follows SOQL's null rules. (SELECT Id FROM Contacts WHERE Email != null) matched no child rows — an empty list rather than an error, so code skipped work and returned empty payloads while the parent query looked perfectly healthy. The gap was wider than the null comparison it was reported as: inside a subquery, != and NOT IN dropped rows whose field is null where the platform matches them, < and <= did the same because nulls sort before any value, and only TODAY, YESTERDAY and TOMORROW were understood — every other date literal matched nothing. Thirteen predicate forms were affected in all. A flat query, an unfiltered subquery, IN lists and case-insensitive text comparison were correct throughout and still are. #391
  • A user-mode DML refusal uses the platform's two shapes. No access to the object at all raises System.SecurityException; the object visible but the verb not granted raises a per-verb shape that is never SecurityExceptionSystem.TypeException (DML operation INSERT not allowed on Account) for insert, update and delete, and distinct DmlException messages for upsert, undelete and each half of merge. A suite asserting on the refusal saw one shape where an org gives two. The shape also depends on the operand: an untyped List<SObject> defers the entity check for exactly undelete and upsert, so those two surface on record visibility and on the field pre-check instead. Two further divergences went with it — Database.<verb>(records, false, AccessLevel.USER_MODE) threw where the platform fails each row and returns results, and Database.undelete under user mode checked the wrong permission and invented its message. #390
  • undelete under user mode is gated on the delete permission. It was checking a permission of its own that no user carries, so an undelete the platform allows was refused locally for any user carrying permission-set data — a false failure in the expensive direction. A user with full object CRUD undeletes under default mode and undelete as user alike; read, create and edit without delete is refused. #388
August 12, 2026
1.20.0
Performance
  • Parallel runs no longer pay a growing tax for wide transactions. A test that issues many DML statements — usually through trigger cascades rather than its own fixture code — used to slow down every other worker's queries, so SOQL time inflated far more than DML time under -p 8. Each statement opened its own PostgreSQL savepoint, and past 64 subtransactions a backend forces every other backend's visibility checks onto a shared cache: a read-path cost, which is why queries paid it and writes did not. Statements past a threshold now share a span-scoped checkpoint with a journal that restores the exact prior state when one fails, so a caught DmlException, a rolled-back savepoint and a partial-success call all behave exactly as before. On an 8-worker reproduction issuing 800 statements per test, parallel query time fell from 5.8–7.4s to 4.2–4.6s and serial write time by about 30%; suites below the threshold are untouched. #9
Added
  • An allOrNone DML failure reports every failing row, not just the first. The platform validates all rows before throwing, and the exception carries each one: getNumDml() counts them and getDmlIndex(i), getDmlMessage(i), getDmlFieldNames(i) and getDmlType(i) answer per row. Nimbus stopped at the first failure, so a caller looping the accessors saw one problem, fixed it, and met the next on the following run. The per-row detail is genuinely per row — two rows missing different required fields report different field lists. The message itself is unchanged and still names only the first failing row, and a statement with a single failing row reports exactly what it did before. Failures raised during the write or trigger phase still surface the first row alone. #366
Fixed
  • A roll-up recalculation saves the parent once, not once per roll-up field. Inserting a single child drove its parent through a full save for every roll-up summary field the parent carried, so every query and statement the parent's triggers issue was multiplied by a number that is a property of the schema and invisible from the code under test. A parent with five roll-ups fired its after-update trigger five times where the platform fires once. That is why governor counts in deep object graphs came out well above what the same code costs in an org, and why the overage tracked trigger surface area rather than how carefully fixtures were batched. Roll-up values themselves were always correct and still are. #375
  • Feature-gated standard fields arrive from an org that has them. An object your org does not have — of the 187 standard objects Nimbus ships, many are gated behind features like Field Service, Communities or Entitlements — was counted as a failed describe request, and 10% of them failing discarded the entire org merge. The fewer features an org had, the more certain it was to lose the fields it did have, which is the opposite of the intent. Coverage is now measured over the objects the org actually answered for, and nimbus sync -o <alias> reports what it read and added, or names the reason it fell back — so a sync that quietly used bundled describes is no longer indistinguishable from one that did not. #302
  • String.valueOf(record) omits nested records, and record identity stops following the rendered text. A queried parent, an assigned parent and a child subquery were all printed; the platform prints none of them, while keeping them reachable and in getPopulatedFieldsAsMap(). This reaches past logging: library code builds cache and de-duplication keys from the rendered string, so an extra entry silently changed identity. Correcting it exposed a second problem underneath — a list took its identity from how its elements print, while a record's own hashCode() used its fields. The platform keeps the two apart: assigning a parent changes a list's hash even though the parent is never rendered. Both now use one field-based implementation. #379
  • A polymorphic Type predicate filters whatever it is compared against. WHERE What.Type = :someType and WHERE What.Type IN :types matched nothing — a filter that silently returns zero rows, so a selector processed no records and looked like it simply had no work. Only string literals filtered. Negation was wrong in the other direction: != dropped rows whose lookup is null and NOT IN matched everything, where the platform includes null-lookup rows in both. #362
  • A platform event describes the four fields the platform defines. ReplayId, EventUuid, CreatedById and CreatedDate — and nothing else. Nimbus injected its standard system-field set onto __e objects, so fields.getMap() answered for nine fields an event does not have, including Id and Name, while missing ReplayId and EventUuid entirely. Code that read them worked locally and failed in an org. Event publishing and delivery are unchanged. #364
  • A bulk insert no longer loses earlier rows when a later one needs recovery. Inserts are sent in chunks; a recoverable failure in the second or later chunk rolled the whole statement back and then retried only the failing chunk, so earlier rows vanished while their Ids were still handed back as successes. Rare — it needs a failure whose cause first appears mid-batch — but silent, and the returned Ids made it look fine. #9
  • nimbus sync without an org no longer warns about one. A project with no default target-org resolved a placeholder alias, and the new sync diagnostics reported that its session could not be obtained — an org the user had never named. nimbus sync -o <alias> reports as before. #302
August 11, 2026
1.19.2
Fixed
  • A Map<Id, V> built by JSON.deserialize can find its own keys again. This is a 1.19.1 regression — upgrade past 1.19.1. size() reported 1, keySet() handed you the key, and get() on that very key returned null; a map that cannot find a key it just gave you is self-inconsistent whatever the platform does. The cause was older than the release that exposed it. Id map and set keys were lowercased throughout, on the assumption that Ids are case-insensitive in Apex — which was never true, and stayed invisible only while the deserializer lowercased keys on the way in too. Once that stopped, one side kept the document's spelling and the other kept lowercasing. An Id key is now stored exactly as spelled and compared case-sensitively, the same rule String keys got in 1.19.1. Three false passes went with it: get() with a differently-cased Id, Set<Id>.contains(), and Set<Id> de-duplication all used to match where the platform does not. #380
  • insert as user is refused on a field-history object at every API version. The 1.19.1 note said user-mode DML was deliberately unchanged because the describe still reports these objects as not createable. The describe does — but the user-mode permission check only runs inside System.runAs, so outside it the object-level gate was the only thing refusing the write, and moving that gate to API 67 took the refusal along with it. Default mode and as system below 67 are unaffected and still match the platform. #365
  • User-mode DML raises System.SecurityException, not NoAccessException. These are distinct types — catch (NoAccessException) does not catch the platform's exception — so a test written that way passed locally and would not have caught the refusal in an org. as user on a DML statement also enforces the running user's object permissions now; the clause was previously parsed and discarded, making insert as user x identical to insert x. Outside System.runAs nothing changes, which is the same answer the platform gives for a full-access context. NoAccessException is unchanged everywhere it is genuinely raised, including Security.stripInaccessible. #383
  • JSON.deserialize into a scalar type now matches the platform, including where it throws. An array or object handed to a scalar returned the whole rendered list. The platform returns the token's opening character for String[ or {, leading whitespace skipped — and refuses every other scalar: JSONException for Integer, Long, Double, Decimal and Boolean, a format error for Date, Datetime and Time, and a bad-id error for Id. Four of those cells used to return a value where an org throws, so defensive try/catch (JSONException) never fired locally. #372
  • Object is refused as a JSON.deserialize target type. Map<String, Object>.class, List<Object>.class, Set<Object>.class, a bare Object.class and nested forms such as Map<String, List<Object>>.class all raise JSONException on the platform, with a distinct message when Object is a map's key type. All were accepted locally, so the code compiled, ran, and threw in the org. JSON.deserializeUntyped is the supported way to get an untyped structure and is unchanged. A class whose own name merely ends in Object is unaffected. #382
  • A Map<Id, V> deserialize target validates its keys. A key that is not an Id was accepted and became an entry no correct code could look up. The platform refuses the document — a well-formed Id whose case-safe checksum does not verify is reported as malformed, a string that is not Id-shaped at all as bad. Key validation runs before value-type validation, as it does on the platform. #381
  • The read-only-object DML refusal uses the platform's two message shapes. Profile, RecordType, ApexClass, ApexTrigger, ApexPage, ApexComponent, StaticResource, Folder and ApexEmailNotification drop the verb — DML not allowed on Profile — while the field-history family, Report, Dashboard, Organization and the rest keep it. Both are System.TypeException, so only text that a suite asserts on changes. Thirty objects were compared to establish which is which: no describe property separates the two groups, so this is a verified list rather than a rule. #373
  • nimbus validate no longer reports RecordTypeId as an unknown column on objects that have record types. It fired on queries the runtime executes successfully in the same project, on standard and custom objects alike, because RecordTypeId is synthesised rather than carried as a describe field. Record types are now read from both the bundled schema and your project source, so a custom object with recordTypes/ metadata is recognised. An object with no record types genuinely refuses the column, and that is still reported. #387
  • A port held by an unrelated process is no longer reported as a stranded Nimbus server. The error named no owner, and the guidance underneath listed only Nimbus-shaped causes — so the obvious next step was to find whatever held the port and kill it. Since the port is derived from the project path across a thousand-port range, it can land on anything; one report had it held by a VPN client. Nimbus now identifies the holder, names it, and says plainly when it is not ours, pointing at moving Nimbus rather than stopping that process. When it really is a stranded Nimbus server, it says so and tells you to stop it. #385
Added
  • nimbus validate reports a trigger declared on an SObject it does not recognise. Such a trigger was accepted silently and simply never fired — nothing mentioned it, and the cost lands somewhere else entirely: the trigger's effects are missing, so assertions about them fail pointing at the product code while the file that is actually wrong is never named. One report reached "roll-up recalculation does not fire parent triggers" from a mistyped object name, and only avoided filing it because a control case failed too. The new diagnostic is a warning rather than a deploy blocker, and says the trigger will not fire locally rather than claiming the file is invalid: a real platform object that Nimbus has no schema for looks identical to one that does not exist, and refusing valid code would be worse. #386
August 10, 2026
1.19.1
Fixed
  • Map<String, V> keys and List.contains/indexOf now compare case-sensitively, as Apex does. This can turn a passing test red, correctly. Apex is genuinely inconsistent here — == on String ignores case, but collection identity does not go through == — and Set<String> was already right, which is what made the disagreement visible. The worst of it was not the false pass: m.put('alpha', …) on a map already holding 'Alpha' overwrote that entry and rewrote its key, so a map accumulating records by name or external id silently dropped rows and any assertion on size() reported a number that looked like a product bug. A real casing bug — the kind that costs an afternoon in a sandbox — now fails locally instead of passing. Removing the old behaviour also surfaced seven places Nimbus itself was folding case where the platform does not: getPopulatedFieldsAsMap(), JSON.deserialize and deserializeUntyped, getAll() on custom metadata and custom settings, String.template, Test.createStubQueryRow, getRemovedFields() and getOutputParameters() all keep the real spelling now, each confirmed against an org rather than restored to lowercase. #370
  • Record-type maps keep their DeveloperName and label casing, and match keys exactly. getRecordTypeInfosByDeveloperName() and getRecordTypeInfosByName() returned lowercased keys, so anything iterating the key set rather than looking up a known name — building a picklist of record types, logging what is available, comparing a key against a constant — got a value the platform never emits, and a label like Affiliate Case Team was not recoverable by any transform. The lookup was wrong in the other direction too: these maps were case-insensitive, where the platform refuses a differently-cased name. Worth knowing, because it is the opposite of the two describe maps beside it: fields.getMap() and getGlobalDescribe() really do lowercase their keys and really do match case-insensitively. The pair is consistent rather than arbitrary — a map that has thrown away its key casing has to fold case to stay usable, and a record-type map has not thrown anything away. #371
  • Fields no permission set can ever grant are no longer denied for lack of a grant. Under WITH USER_MODE and WITH SECURITY_ENFORCED, RecordTypeId was refused with the unknown-column message for any user who reached the field-permission check. Field-level security cannot be set on it, so its absence from a permission set says nothing about what was granted. The damage ran in the direction that costs the most trust: a user on a profile alone never reached that check, so assigning a permission set — which only ever widens access — was what took the field away, and a suite went red after a permissions change that had granted strictly more. Verified across seven standard objects, where OwnerId, RecordTypeId and LastActivityDate all report that no field-level security is settable on them, while ordinary optional fields report that it is — those are still denied when never granted. #374
  • A record's Id key now follows the type of the map it goes into. Map.putAll(List<SObject>) and new Map<K, SObject>(List<SObject>) both keyed by a lowercased record Id whatever the map's declared type, and putAll reported that lowercased spelling back through keySet() as well. The platform keys a Map<String, SObject> by the canonical 18-character Id and matches it case-sensitively. Invisible until the change above, then unmissable: a lookup by the Id you are holding found nothing, so code that carries accumulated per-record state in a Map<String, SObject> across passes picked up a fresh record each time instead of the accumulated one, and silently dropped whatever the previous pass had contributed — most visibly in rollups, which came out holding only the last contribution. Map<Id, V> is unchanged. #378
  • A queried parent record now serializes under its relationship name, not a lowercased one. JSON.serialize of a record carrying a queried parent emitted "account" where every real field on the same record came out properly cased and the platform emits "Account" — a relationship is not a field, so the name resolution had nothing to answer with and fell through. This surfaced through the case-sensitivity change above: the common idiom of round-tripping a record through JSON.serialize and deserializeUntyped, replacing the parent by name, and deserializing back began writing a second key beside the stale one, and the stale parent won — silently discarding the fields that had just been re-queried. Library code that re-queries parent fields on demand is the main thing affected. #378
  • The unknown-column message echoes the field name as the query wrote it. A field that could not be resolved — misspelled, or refused by field-level security — was reported after resolving an API name from the schema, which title-cased anything the schema had no answer for. The platform never normalises the token: wEbSiTe2 comes back as 'wEbSiTe2' and RECORDTYPEID2 as 'RECORDTYPEID2'. A title-cased name reads like a typo in your own query rather than a permission decision. #376
  • Field-history objects accept DML again below API 67. insert on AccountHistory, CaseHistory or a custom *__History was refused at every API version. The platform only refuses from 67: below it there is no object-level check at all, and the statement runs into ordinary row processing, where whether it succeeds is decided by the row. Confirmed with classes whose bodies are byte-identical and differ only in their declared API version — at 66 the insert reaches row-level validation, at 67 it never gets that far. A class that declares no API version keeps the 67 behaviour, as does anonymous Apex, which compiles at the org’s own version. The describe is unchanged and still reports these objects as not createable at both versions, so user-mode DML is refused below 67 exactly as before. #365
August 10, 2026
1.19.0
Added
  • nimbus sync --include-record-types now pulls your profile’s record-type defaults too. An insert that leaves RecordTypeId unset takes the default from the running user’s profile, and that default was readable only from profile metadata checked into project source — a project syncing permissions from the org had no route to it, and the resulting null is also the platform’s answer for a profile with no default, so the two were indistinguishable. Record-type defaults are a profile-only concept, which is why permission data cannot supply them even in principle. The finding that shaped this: a Profile retrieved on its own comes back with no record-type visibilities at all — the same profile requested alongside the record types returned seventeen. The Metadata API reports a profile’s permissions only for metadata named in the same request, so asking for a profile by itself yields one that appears to grant nothing. A profile you keep in source still wins. #334
Fixed
  • Every DML failure now arrives in the platform’s envelope, attributed to the row that caused it. A thrown DmlException carried a compact CODE: message string instead of the shape Apex actually produces — no verb, no row index, no id — so anything parsing getMessage() read something the org never emits. getStatusCode() answered UNKNOWN_EXCEPTION on the partial-success path, getNumDml(), getDmlIndex() and getDmlId() answered constants, and the : [Field] suffix that belongs in getFields() was inside the message. Failures now read Insert failed. First exception on row 0 with id 001…; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, …: [Field__c], with the accessors populated to match. Verified against an org down to the details that are easy to get wrong: the row index is the first failing row’s position, not the first row; with id appears exactly when the failing row has one, so never on insert; and the empty field tail is : []. One project’s error-handling framework, which parses these messages, went from failing to passing on the strength of it. #293 #341
  • Writing a non-createable field is now rejected inside test methods too. The platform refuses these identically in a plain class and an @isTest class — verified by deploying both side by side, there is no field a test may write that ordinary code may not — but Nimbus skipped the check in test context, so a suite could set IsWon, IsEmailBounced or a compound Address and pass on code the org will not deploy. This can turn a passing test red, correctly. The subtlety that shaped the fix: the platform’s rule is a compile-time one, so only writes the compiler can see count — a value a record merely carries at run time is accepted. Re-inserting a clone of a queried record, describe defaults loaded by newSObject, a JSON-deserialized payload and compound parents are all exempt for that reason, each confirmed against the org rather than assumed. #276
  • A Minimum Access user got rows back from queries the org refuses. Object-level read was only enforced for queries that said WITH USER_MODE, and Security.stripInaccessible(READABLE) handed back an emptied record where the platform refuses outright. Both are false passes — the platform enforces object read in every query shape. Verified across six probe rounds as freshly created Minimum Access users: every shape but a bare COUNT() throws QueryException with the full custom-object hint, a bare COUNT() throws SecurityException instead, and stripInaccessible with no object access throws NoAccessException — with the type name appended only when access is partial, and never for an empty list. AccessLevel.SYSTEM_MODE is exempt, and now on every entry point rather than just one. #350
  • Setup objects were readable for a Minimum Access user without the permission that grants them. Organization, Profile, ApexClass, PermissionSet, PermissionSetGroup and PermissionSetAssignment answered rows where the org refuses. Each cell was probed directly rather than reasoned about, which was necessary because the two message forms are not derivable — the first three are refused with a short hint-less message and the last three with the full custom-object hint. The ViewSetup system permission restores every one of them; an ordinary permission set carrying only object and field grants restores none, which was probed with exactly that shape. ViewSetup declared in your permission-set metadata is now honoured. #360
  • A polymorphic parent traversal read from the wrong object. A relationship whose foreign key can point at more than one object — TopicAssignment.EntityId lists 35 targets, ContentDistribution.RelatedRecordId eight — was joined against the first declared target, so a query returned data for rows pointing at that one object and null for every other row. The platform resolves these per row: one query returns Entity.Type of Account for the account-pointing row and Contact for the contact-pointing row. Parents now resolve per row across the whole polymorphic family, driven by the describe data rather than a fixed list. #327
  • Fields on a polymorphic parent came back null, and Type predicates silently dropped every row. Those parents carried only a type and an id, so SELECT What.Name answered null — as it had since the beginning — and the fields a query explicitly named were not read at all. Worse, WHERE What.Type = 'Opportunity' did not filter permissively as intended: it matched nothing and removed every row from the result, which is the silent-wrong-answer direction. Named fields are now read from whichever object each row points at, and =, != and IN against a type filter per row. A related fix: ORDER BY on a polymorphic type sorts by type name, verified with a pair whose name order inverts their internal key order, so a prefix sort returns them backwards. #361 #362
  • Feature-gated standard lookups stayed invisible even after syncing against an org that has them. Case.EntitlementId and Case.ServiceContractId appeared to exist but carried no relationship information, so traversing them failed and their describe answered null. Two independent causes: nimbus sync was not asking the org for standard describes at all — only the automatic sync did — and even once merged the result did not survive, because the schema cache is rebuilt on every sync and the next run without an org alias quietly discarded it. Both are fixed, and what the org adds is now remembered so ordinary runs keep it. Re-run nimbus sync -o <alias> once after upgrading. #302
  • A picklist bound to a global value set ignored the value set’s default. The values resolved but the declared default did not, so the field described as not defaulted, isDefaultValue() was false for every entry, and an insert that omitted the field stored null. Where the field is also required — which is usually why a default is declared — that became a hard REQUIRED_FIELD_MISSING on a row the platform accepts, concentrated in @testSetup methods that take their whole class down with them. The same picklist written with an inline value list was always correct, which is what isolated the indirection. A default declared on the field itself still wins, and a default on an inactive value is ignored. #367
  • A variable named after a system type resolved to the type. String json = 'hello world'; json.contains('world'); failed with Unknown method contains on Type json, and the same held for system, database and every other system class name, for locals and method parameters alike. String json = JSON.serialize(x) is close to idiomatic, so this reached ordinary code. There had been a deliberate carve-out letting the type win, meant to keep a variable and the type usable in one scope — and the org settles that: a class declaring a local json and calling JSON.serialize in the same scope does not compile. The carve-out was protecting a shape that cannot be deployed, at the cost of breaking one that can. #368
  • JSON.deserialize of a JSON object into a list type returned a corrupt value instead of throwing. The result was labelled as the list type but held the object’s entries, so it escaped the surrounding try — nothing was thrown inside it — and the caller then failed on the first list method, at a line with no visible connection to the JSON. The common defensive parse-and-fall-back-to-empty helper therefore never fell back. Any non-array handed to a List or Set, and any non-object handed to a Map, now throws the platform’s JSONException. Two carve-outs are load-bearing and were confirmed against an org: a literal null deserializes to null rather than throwing — code that round-trips JSON.serialize of an empty value depends on it — and a scalar target is left alone, because the platform does not throw there either. #369
  • A field explicitly set to null vanished from String.valueOf(record), and fields printed alphabetically. The platform prints an assigned null and omits a queried one — a distinction with a real basis, since a null read from disk never enters the record’s field map at all, which is also why getPopulatedFieldsAsMap() excludes it. It also prints fields in the order they entered the record, not sorted. Both now match, including the details that are only findable by probing: an Id assigned by an insert renders last, a clone keeps its source’s order, reassigning a field keeps its original position, and a queried null that is later assigned moves to the end rather than reclaiming its original slot. #279 #358
  • newSObject(recordTypeId, true) left its loaded defaults unmaterialized. The defaults were applied but the record’s field map did not reflect them, so equality comparisons and anything walking the populated fields saw a different record than the platform does. Materializing them flushed out four separate places that had been reading “this field is present in the map” as “the caller wrote this field” — an auto-number writability check, insert validation, JSON.serialize, and the copies made by stripInaccessible, clone() and trigger context. Each was found by a corpus run rather than by inspection. JSON.serialize of a defaults-loaded record emits only the populated fields, verified against an org. #359
  • In a namespaced project, a qualified spelling of your own member did not resolve. The platform treats ns__Member__c and Member__c as the same member for the project’s own code, and renders a type token as the namespace-qualified name. Nimbus did neither: String.valueOf(X__c.class) printed the local name, breaking registries keyed by qualified type names, and a qualified spelling in member access missed the stored field entirely. Both are fixed for dynamic access, typed reads and writes, and constructor arguments. Scoped deliberately to your own namespace: verified against an installed managed package, the local spelling of another package’s member throws on the platform, and that stays true. #165 #259
  • A managed package’s protected custom metadata records were visible to subscriber code. Verified against an installed package shipping protected and public records on one type: a protected packaged record is invisible everywhere — absent from getAll(), getInstance() returns null, SOQL returns no row — while getAll() keys stay the local developer name and the row carries the namespace separately. Custom metadata reaches Apex by two independent routes, and both enforce this now; fixing either alone looks correct and is not. Your own unpackaged and own-namespace protected records stay visible, as on the platform. #194
  • A custom platform event described like an ordinary custom object. It answered true for queryable, updateable, deletable, undeletable, searchable and mergeable, and served record-type information it does not have. Verified against an installed event’s describe: only createable and accessible are true, isCustom is true, MRU is false, and the record-type list is empty. #184
  • A flow traversing a standard relationship from $Record got null. $Record.Account.Id on a record carrying only AccountId resolved nothing, because relationship resolution handled custom __r names only — and standard relationships cannot be guessed from the name, since Account resolves through AccountId only because the schema says so. Cross-object $Record references are a core flow feature, so this reached flows that work on the platform. Single-target relationships resolve; a polymorphic one keeps the previous behaviour. #151
  • The ConnectApi.ManagedContent methods accepted any community id. Every one of them validated nothing and returned an empty collection, so a test could pass a null, an empty string or an obviously wrong value and see success. Verified against an org, and the rule holds across the family: null or empty throws with one message, a malformed id another, and a well-formed id of the wrong type is refused on its key prefix. A well-formed id that simply does not exist is deliberately still answered with the empty collection — Nimbus has no content store, and a real community id must never become a false failure. #159
August 8, 2026
1.18.2
Added
  • nimbus sync --include-record-types pulls the org’s record types. Record types were read from your project source and nothing else, and source can only ever say what exists — so an object having none was indistinguishable from Nimbus never being told. For your own custom objects that was fine, since source is the whole definition; for a standard or packaged object the record types live in the org, and Contact has none on one org and several on the next. One unfiltered query returns the org’s entire set, which is what makes an object’s absence from it evidence rather than silence. That is the fact the RecordTypeId rule below needs to reach standard and packaged objects. Record types defined in your own metadata still win, per object; org record type Ids are discarded and the deterministic local ones kept, so Ids stay stable across machines; and a result the org paginated is marked partial and never read as proof that an object has none. #355
Fixed
  • A static initializer that never finished hung the entire run. Loading a class runs its static blocks and static field initializers, which is ordinary Apex and can loop forever — but loading sat outside the per-test timeout. For a class grouped around @testSetup there was no watchdog at all, so the run stopped dead: no output, no result for any test, nothing to report. For an ungrouped test the timeout did fire, but the abandoned work could not be stopped and kept running at full speed for the rest of the process — the same leak fixed in the previous release, surviving on this one path. Class loading now runs under the same timeout, so a non-terminating initializer fails its class cleanly and reports every method in it, the way a failed @testSetup does. #349
  • isCreateable(), isUpdateable() and isDeletable() answered true for objects the platform never lets you write. These are not permission questions a profile could answer differently — the platform describes Report and Profile as not createable for a system administrator, the most permissive user there is. So the common defensive shape if (Schema.SObjectType.Profile.isCreateable()) { insert p; } took the guarded branch locally and skips it in the org, meaning the suite exercised code the org never runs. All 163 bundled describes were re-checked against an org and now match it object for object; sixteen were wrong, in both directions — Campaign was marked unwritable, which would have refused every Campaign insert had the data been served unchecked. The answer is also per verb rather than per object: User can be created and updated but never deleted, GroupMember created and deleted but never updated, ContentDocument the reverse. #351
  • DML against a read-only object was accepted. With the describe data verified, the write itself is now refused as the platform refuses it — insert new Report(), insert new Profile(), delete on a User, update on a GroupMember. Field-history objects were already covered; this is the rest of the family, and it is per verb for the same reason as above, so nothing is refused that the platform allows. undelete is deliberately not covered: the platform exposes no flag for it and gets past this check to fail on the record id instead. Note this can turn a locally-passing test red — correctly, since the operation cannot run in an org. With a statically-typed operand the platform refuses at compile time and Nimbus raises the same rule one phase later, at the DML. #318 #352
  • RecordTypeId was queryable on every object. The platform exposes that column only on objects that actually have record types; on an object with none it does not exist, and naming it is an error rather than a null read — so code that cannot deploy was passing locally. It is now refused in the SELECT list, in WHERE, ORDER BY and GROUP BY, in aggregates, inline and dynamic alike, and through get() / put(). A child sub-query is checked against the child object it actually reads, and a reference qualified by a FROM alias or by the object’s own name counts as naming that object’s own field — while a genuine parent traversal such as Account.RecordTypeId FROM Contact stays legal, because it reads a different object’s field. Without an org record-type snapshot this applies only to objects your project defines, where source is the whole definition. #333 #353 #356
  • A restricted user lost fields the org hands back. Security.stripInaccessible(READABLE) removed ungranted optional fields from records read by a Read Only profile, where the org keeps them — a false denial, which is the damaging direction, because records come back thinner locally than in production. Measured across four users on an org: a profile that carries field-level security grants standard fields, so Standard User and Read Only reach every field in every shape and nothing is stripped. Only “Minimum Access - Salesforce”, which grants nothing without an explicit permission, refuses — and it refuses in every shape, including plain system-mode queries. Read-side answers now agree with the org and with each other; the write access types keep their stricter reading, since the platform’s rule for them differs. #344 #346
  • A field-security denial reported a message the platform never emits, and two query paths disagreed about the denial itself. The text was invented — the platform reports a denied field as absent from the entity, exactly as it does a misspelled column. Behind it the dynamic-query path was a second copy of the check that denied fields to a Standard User the inline path allowed, so the same query gave different answers depending on how it was written. There is now one implementation, so they cannot drift again. The message has two forms and the field’s position picks between them, not whether the query aggregates. Separately, Database.query(q, AccessLevel.USER_MODE) ignored its second argument entirely, so a query that explicitly asked for enforcement got none. #345
  • isDefaultedOnCreate() answered false for every standard field. OwnerId is defaulted on create everywhere — on standard and custom objects alike, verified against an org — and the correct answer was in the bundled describe data all along, simply never read. It matters most to test-data frameworks, which build a parent record for every field that is createable, not nillable and not defaulted: on the platform that walk skips OwnerId and stops, while locally it descended through User and Profile to UserLicense and failed there. #354
  • A never-terminating static initializer could wedge the language server, MCP server or dev UI. Those hosts load the same classes the test runner does, and had no way to interrupt one that did not finish, so the request hung with no recovery short of a restart. Loading now runs under a deadline whose scope ends when loading does — the interpreter handed back afterwards is unrestricted, exactly as before, so nothing that drives it on its own schedule is affected. #357
  • --dry-run listed the tests it would run, then reported that none existed. The summary said Tests: 0 total and No tests ran directly beneath a listing of three. Under --json there was no contradiction to notice — the document reported zero tests and an empty array, so the enumeration a dry run exists to produce was missing from the machine-readable output altogether, and a caller could neither distinguish “nothing matched” from “three matched” nor recover the names. The selection now reaches both surfaces, marked as skipped. A dry run also no longer claims tests passed: nothing was executed to pass. An empty selection still reports zero and warns. #348
  • Excluding a path removed the class from the run, and references to it became null rather than errors. -e/--exclude keeps paths out of compilation — that is what it is for — but nothing said so when it mattered: a test needing an excluded helper failed with a null dereference or a wrong value pointing at the code under test, never at the flag. At suite scale that reads as a scattering of genuine-looking product bugs; one report traced roughly 280 of them across four hand-split shards. The run already knew both halves — which files a pattern dropped, and which references went nowhere — so it now names the class and points at --shard, which splits a suite without removing code. #347
  • Test selection and exclusion disagreed about capitalisation. The pattern matched case-insensitively while --exclude did not, so selecting a pattern and excluding the same pattern overlapped instead of splitting the suite — quietly running some tests twice for anyone partitioning that way. Exclusion is now case-insensitive too, matching how Apex treats type names. Extra positional patterns are also rejected rather than ignored: nimbus test A B C ran only A’s tests and said nothing about the rest. #348
August 7, 2026
1.18.1
Fixed
  • A full-suite run could grow to tens of gigabytes and exhaust swap. A test that hit its per-test timeout was reported as failed but never actually stopped. Cancellation arrived as a failure of the test’s next database operation — a catchable exception — so a retry loop wrapping DML in try/catch swallowed it and kept executing at full speed for the remainder of the run, holding onto everything it touched. Every timed-out test of that shape added another: on a 2,469-test suite this reached 13–19 GB and a full swap file, with CPU well above one worker’s share, while the same tests split across separate processes stayed in the hundreds of megabytes — the observation that pinned the mechanism. A timed-out test now halts with an uncatchable fault the moment the runner abandons it, the way the platform’s own limit breaches halt code. Two fixes fell out: the configured per-test timeout (--timeout / nimbus.test.timeout) was silently ignored for classes grouped around @testSetup, which always waited 60 seconds; and when one test in such a class timed out, every remaining test in the class was reported as skipped — they now run. #298
  • A roll-up summary read 0 while its children were provably linked, and nothing was logged at any verbosity. Recalculating a roll-up saves the parent, and the parent’s validation rules and update triggers all run inside that save. If one of them rejected it, the rejection was discarded: the child’s DML reported success and the roll-up kept its previous value — 0 for a record that never had one computed, which is indistinguishable from a correct 0. Every roll-up over the same object pair fails together, because each one’s parent save trips the same rule. The two silent paths instrumented in the previous release both report, which is exactly why no diagnostic appeared: this was a third path, and it reached neither. The child statement now fails, carrying the parent’s message and the verb the caller actually wrote — a child insert reports "Insert failed", not "Update failed" for the parent save underneath. #308
  • A DML statement that failed after its rows were written left them behind. The platform rolls the whole statement back — that is what its per-statement savepoint does. Nimbus already rolled back correctly when a record was rejected before the write; the gap was everything after it, where the rows were already in. Verified against an org across all three verbs: an insert whose roll-up trips a parent validation rule keeps none of its rows, an after-insert trigger calling addError on one row of two keeps neither (the rollback unit is the statement, not the offending row), an update has every field it changed reverted, and a delete has its row restored. A test asserting a row count after a caught DmlException was reading a number the platform never produces, and a half-applied @testSetup left later methods running against state that cannot exist in production. Writes made by triggers and flows during the statement are not rolled back yet. #340
  • An AutoNumber Name arriving through JSON.deserialize failed the insert. Exporting records to JSON and re-importing them elsewhere is an ordinary configuration-migration shape, and Name rides along in the payload because it was in the export projection. Where the platform refuses depends on whether the compiler can see the write: a source-level assignment is a deploy error, while the same value arriving at run time is accepted and silently ignored. Verified against an org — a payload naming 000124 produces a stored row reading 000000. So the insert now succeeds and the supplied value is discarded, rather than being honoured, which would store something the org never stores. Two neighbouring divergences went with it: an upsert was writing the supplied Name over the stored sequence number, and put() threw at the DML instead of at the put() call. A source-level assignment is still rejected. #343
  • Configured seed data existed under nimbus test and nowhere else. nimbus.seed.row.PermissionSetGroup.X resolved to one row inside a test method and zero from nimbus exec — which is the first place anyone checks whether the config took. The same split affected named users, public groups, roles, queues, networks, list views, Sites, org defaults, and the project’s own permission sets and permission set groups. All of it now seeds for nimbus exec, nimbus app, the language server, the debugger and the MCP bridge as well. The divergence was invisible on any database that had run tests at least once, since those rows commit outside the per-test transaction and survive — so it appeared on a fresh clone, after a reset, or for anyone who had only ever run exec. #300 #336
  • Field labels were reconstructed from the API name rather than read from the describe. Contact.AccountId described as "Account Id" where the platform says "Account ID", and the fault ran far wider than casing: the derivation agreed with the platform for only about a third of standard fields. Account.NumberOfEmployees is "Employees", Account.BillingPostalCode is "Billing Zip/Postal Code", Account.Sic is "SIC Code" — none of which any derivation can recover, because a label is not a function of the field name. The correct labels were already bundled and were being discarded. A 19-field spread was verified against an org: all 19 now match exactly, where 10 did not before. #335
  • Schema.RecordTypeInfo.isAvailable() reported whether the record type was active, not whether your profile grants it. Every active record type read available for every user, so code guarding on isAvailable() before selecting a record type took a branch the org would not. Verified against an org: two active custom record types report unavailable until the profile is given visibility of them, and eight active packaged Account record types assigned to no profile all report unavailable. The grant is now read from the same <recordTypeVisibilities> block the default record type comes from. It is consulted per object and only where your profile actually speaks — a profile retrieved with a partial manifest routinely omits objects the org does grant, and nothing distinguishes that from a real denial, so an object your profile does not mention keeps the permissive answer. #332
  • nimbus reset left the database running, then failed its own re-initialisation. It removed the project’s .nimbus/ directory but never stopped the embedded PostgreSQL, so re-initialising hit "process already listening on port N" — and a second reset failed identically, meaning the command you reach for when something is already wrong could not fix the state it had just created. Two faults produced it, and each hid the other: the port sweep covered a fixed ten-port window while a project’s port is derived from its path across a thousand-port range, and the graceful stop looked for the database binary somewhere it does not live, so no clean shutdown was ever attempted. Reset now stops the server by its recorded process id, clears the project’s actual port, and waits for that port to be released before continuing. A current build also recovers a project an older one left stranded. #342
  • --json output could be interrupted by progress and warning text. Anything written to standard output alongside the JSON document makes it unparseable, which breaks a CI step that pipes the result to a tool rather than a person — and it broke conditionally, so it survived testing. Warnings now go to standard error, leaving --json stdout a single parseable document. #337
  • Stack-trace frames were rendered in two different shapes. A freshly-constructed exception reported Class.Foo.bar: line 12, column 1; a caught or propagated one reported Class.Foo.bar: line 12 — and, in practice, no line at all, because nothing ever recorded where each frame was stopped. An exception raised and caught inside a single method came back with no stack whatsoever, which is the ordinary shape for a query exception from inline SOQL. The platform draws none of these distinctions. Verified by running an identical class against an org: every frame of a caught custom exception, a runtime null-pointer, a DML exception, a query exception, a rethrow, and an exception constructed but never thrown carries the column suffix and a real line — and Nimbus now reproduces the org’s line numbers exactly for the same source, including keeping the original throw site through a rethrow. This changes the failure output shown on every failing run, and anything asserting on getStackTraceString(). #296
  • A standard object Nimbus does not ship a describe for could not be reached at all. Standard-object definitions are bundled, so when one is missing there is nothing a project can do about it: there is no metadata file anyone could write for a standard object, and the org route only ever asks about objects already in the bundle. Both remedies were closed by the same cause, and the only fix was to wait for a release. Naming the object explicitly now asks your org for it directly — nimbus sync -s WorkOrder -o myorg — and remembers it, so later syncs keep it rather than regenerating without it. A release that adds the describe quietly takes over from the pulled copy. #326
  • A field-level security check ignored everything outside the SELECT list. Under WITH USER_MODE, a user who cannot read a field could still filter on it, order by it or group by it — so a test written to prove a restricted user cannot reach a field passed locally whenever the field was filtered rather than projected. That is the direction that costs the most trust in a local runner, because the test looks green. Verified against an org: as a user granted object read and no field permissions, all four clauses are refused exactly as projecting the field is, while the same query without that field succeeds. The clause reader is deliberately cautious — a subquery’s fields belong to the other object, and bind variables, literals and date literals name nothing — since a misread name would refuse a query the org accepts. #328
  • isAccessible() reported a field readable because the object was. A field with no grant at all answered true, and contradicted WITH USER_MODE on the same field, in the same transaction, for the same user — which is part of why field-access reports were hard to attribute. Verified against an org: Name rides on object read and answers true, while three ungranted optional fields on the same object all answer false. The two now agree. The denial is confined to objects Nimbus actually holds field grants for: a seeded standard profile such as “Read Only” contributes no field permissions locally and grants plenty in the org, so reading its silence as a refusal would invent a denial rather than model one. #329
  • A database column was never widened after the schema said it should be. Column types are fixed when a table is first created, so a project whose database predates a width change kept the old one indefinitely — and the insert that no longer fits failed with a raw database error on a value the platform accepts. It surfaced as org-synced permission grants that still would not seed for objects in a managed package, where the API name alone can exceed the old limit, but the same freeze applied to an ordinary text field whose length was simply increased in project metadata. Columns are now widened in place — never narrowed, so nothing already stored is at risk — and the permission seeder repairs the columns it needs without waiting for a sync. Starting from a fresh database is no longer the only way out. #306
  • nimbus triage listed present, queryable objects as missing metadata. Platform-namespaced types were reported as absent — and written into the manifest it generates — while the same run warned about a lookup filter on one of those very objects. Retrieving them cannot fix anything, so the manifest sent you after metadata you already have. #304
Performance
  • A warm test run no longer spends about a third of its time asking the Salesforce CLI which org is the default. Running nimbus test without -o invoked the CLI as a subprocess on every single run — a Node process costing more than the rest of startup combined, and the largest single source of run-to-run variance. The answer is decided by files on disk and changes approximately never during a working session, so it is now cached against those files and the subprocess runs only when they change. On a small project a warm run went from 1.18s to 0.60s. The "no default org configured" answer is cached too, which was the worst case: a full subprocess on every run to be told nothing. #338
  • SObject field lists are no longer rebuilt and re-sorted on every lookup. The list was walked, filtered and re-sorted each time it was asked for, on a path the DML and trigger engines use continuously — 27% of engine CPU on a real suite, with over a tenth of all CPU in the sort alone. It is now built once per object. Callers still receive their own copy, so nothing that appends to or sorts the result changes behaviour. #339
August 7, 2026
1.18.0
Added
  • Field history objects are provisioned and queryable. Every query against a history object previously failed outright with a missing-table error, so a class that reads field history could not run at all — the query threw before any assertion, making those classes untestable regardless of what they asserted. The two halves follow the platform’s own rules: a custom X__History exists only when the parent’s metadata carries <enableHistory>true</enableHistory>, while a standard history object is present whether or not tracking is configured. Provisioning every history object unconditionally would accept queries the org rejects, and gating the standard ones on your metadata would reject queries it accepts. The tables are empty — writing history rows on a tracked-field update is a separate, larger piece of work. #303
  • nimbus.seed.row seeds real database rows for objects that live in your org but not in your repo. nimbus.seed.record deliberately never inserts anything — it feeds getAll()/getInstance() for list custom settings and stops there — which left no route at all for an object that exists in the org, has no dedicated seeder, and is dereferenced straight through: orgWideAddress('x@y').Id, or a PermissionSetGroup looked up by DeveloperName on a User-trigger path, where the failure is uncatchable. One line per row: nimbus.seed.row.PermissionSetGroup.For_Everyone=MasterLabel=For Everyone. The name half of the key fills whichever identifying column the table actually has — DeveloperName where there is one, otherwise Name — so a lookup by that name resolves without repeating it in the value. Rows land once per environment setup rather than per test, since a test already runs inside a transaction that rolls back. #316
  • Inserting a Campaign now creates its two default member statuses. The platform creates Sent and Responded on every Campaign insert; Nimbus created none, so code querying or asserting on campaign member statuses saw an empty list. Both of the platform’s delete protections come with it, and they are two separate rules rather than one: deleting the default status fails, and deleting the last status marked Responded fails — a campaign with two responded statuses can still lose one. Setting a new default clears the previous one, on insert and update alike, which is what lets a package install its own statuses over the seeded ones. #175
Fixed
  • An insert that omitted RecordTypeId stamped the Master placeholder instead of your profile’s default. Every such record got 012000000000000AAA, a describe-only value the platform never stores. This was harmless until lookup filters began being enforced, at which point a filter constrained on record type rejects the Master-typed row and takes the whole @testSetup with it — reporting the filter’s own message, with nothing in it pointing at record types. The running user’s profile default is now applied, read from <recordTypeVisibilities> in your profile metadata and matched by either the Setup label or the metadata API name ("System Administrator" and "Admin" both resolve), with System.runAs following the impersonated user. When the profile names no default the field is left null, which is what the org does. The value is stamped before before-insert automation runs and is never written back onto your in-memory record, both matching the platform. Permission sets are deliberately not consulted — they cannot carry a default. #331
  • Seven describe methods on the fields shortcut returned null instead of an answer. getLabel, getRelationshipName, getReferenceTo, getSoapType, getDefaultValue, getDefaultValueFormula and getLocalName fell through to a generic fallback that answers null for any qualified class name, so a field-metadata helper branching on getRelationshipName() != null to spot lookups saw every field as a non-lookup and nothing raised. isNillable and isMRUEnabled threw "Unknown method" — the mild half, since at least it surfaced. All 43 describe methods now answer on the shortcut, delegating to the same describe result getDescribe() builds so the two spellings agree by construction rather than by keeping two lists in step. #330
  • A query touching a required field was denied under WITH USER_MODE, and no metadata change could fix it. Any query naming a required custom field, a required lookup or a master-detail field failed with "You don’t have access to one or more fields" for a user whose grants arrive through permission sets. The check treated "no field permission grant" as a denial — correct for an optional field, wrong for a required one, because the platform stores no FieldPermissions row for a required field at all: its field-level security is forced on and an entry naming it is not accepted. So the absence of a grant means two opposite things, and only one of them is a denial. Verified against an org: required, master-detail and auto-number fields return zero permission rows org-wide while optional fields on the same objects return one per granting profile or permission set. An ungranted optional field is still denied, which is the half that keeps this from becoming "user mode stops checking". #82
  • Roll-up summaries went stale when children were deleted by a query that selected only Id. delete [SELECT Id FROM Child__c WHERE ...] left every roll-up above the deleted rows at its pre-delete value, because the parent key was read off the in-memory record and a row queried Id-only carries none. Silent — no error, and a stale number is indistinguishable from a correct one. With a second roll-up level above the first, both stayed stale. That is the ordinary way to delete a working set in Apex: a selector returning Ids, a batch’s start() scope, a test cleanup step. The platform recalculates in every case because the server knows each row’s parent regardless of what the client selected, so the key is now recovered from the stored row. A key that is present and null is left alone — that is a record with genuinely no parent. #324
  • INCLUDES and EXCLUDES are real multipicklist set operators. Both compared against the stored string as a whole rather than asking about its members, so INCLUDES ('A') missed every row with more than one value selected — a row holding A;B holds A, and the platform matches it. EXCLUDES was wrong at both ends: it kept rows that do hold the value and dropped rows where the field is null. INCLUDES ('') answered the exact complement of the platform, which reads it as "has nothing selected". Silent in every direction and shaped like a filter that works, so a selector over an optional multipicklist quietly processed the wrong rows. 25 query shapes were verified against an org and all now agree, including that a comma is OR, a semicolon inside one operand is AND, and EXCLUDES is the exact complement of INCLUDES on every row, null ones included. #323
  • Restrict delete constraints were ignored whenever more than one record was deleted at a time. A list delete took a batch path that ran no checks, so every parent went and every child the constraint exists to protect was orphaned — succeeding locally and failing on the platform, which is the direction that defeats the point of running locally. The single-record case was fixed previously; its regression test passed only because its list happened to hold one element. The platform answers per row, so a partial save (allOrNone = false) now produces a result per record with the unblocked rows really deleted, while an all-or-none delete throws and deletes nothing. Both shapes match the org exactly, including which row survives. #319
  • NOT LIKE dropped rows where the field is null. SOQL treats a negated predicate as true when the field is NULL. A pattern matching neither of two records returned only one of them, and NOT Type LIKE 'Exc%' returned nothing where the org returns the null-Type row. Compound forms — the predicate inside an AND or an OR — were wrong the same way and now agree with the org too. Plain LIKE is untouched: a null field doesn’t match it on either side. #322
  • NOT IN dropped rows where the field is null when the operand was a bind variable. The literal form was already correct, so the two disagreed: Type NOT IN ('ExcludeMe') matched a null-Type row and Type NOT IN :typeSet did not. Any selector excluding values on an optional field — Type NOT IN :excluded, RecordTypeId NOT IN :ids — returned nothing where the platform returns everything, so a roll-up or batch processed no rows and reported success. != against a bind had the same gap. IN is deliberately unchanged: a null field doesn’t satisfy it on either side. #321
  • The four fields the Opportunity stage drives were never derived from it. IsClosed and IsWon read false on every opportunity ever written, including one staged "Closed Won", and Probability and ForecastCategory were never filled either. The stage data was already correct locally — only the join was missing. The rule, re-verified against an org: the stage wins unless the same DML statement also carries the field, in which case your value wins; an update touching neither leaves both alone. There is no stickiness across statements — a Probability of 77 set on insert becomes 100 after a later move to Closed Won, and an explicit ForecastCategory is overwritten the same way. Telling "the caller set this now" from "this was merged in from the stored row so triggers could read it" is what makes that precise, and it now uses the same comparison the update path already applies to decide which columns to write. #320
  • Three standard objects had database tables but no schema, so no parent traversal through them could resolve. ContentDistribution, Topic and TopicAssignment carried no relationship metadata at all, and Site shipped 13 of the org’s 51 fields, missing every reference field. The previous release’s stricter relationship checking turned that from a silent wrong answer into a hard error with no remedy available to you — a standard object can’t be authored in your project, and syncing from an org only augments objects Nimbus already bundles. A parent traversal now also requires the field’s own relationship name to agree with the name being traversed, so bundling these didn’t start accepting queries the platform rejects: ContentDistribution.ContentDocument has no relationship name on the org and is refused there and here alike. #325
  • Limits.getCpuTime() read about ten times high. The reading is derived from work done rather than the wall clock, so identical code reports identical CPU on every machine — that part is deliberate and unchanged. The conversion factor was not: it divided a count of syntax-tree nodes by a figure meant for whole statements, and one statement is several nodes. The everyday casualty was the standard Apex trick of waiting out a wall-clock second to force an observable LastModifiedDate change, which reported the entire synchronous budget as consumed after one second of real time against the org’s 992ms; anything branching on getCpuTime() then took its limits-exceeded path locally and its normal path on the platform. The same loop now reports 998ms. Readings are roughly a tenth of what they were, so a test that deliberately exhausts CPU needs ten times the work to do it. #299
  • isCreateable() and isUpdateable() answered false for every field on 39 standard objects. Those describes stated neither flag anywhere, and an omitted flag and an explicit false are indistinguishable once parsed, so nothing was enforced on them while every field reported uneditable. For 22 the org agrees there is nothing createable; the other 17 now match the org exactly. Truncated field lists were completed in the same pass — ListEmail shipped 4 of 28 fields, MobileApplicationDetail 12 of 30 — adding roughly 22,000 previously absent field details. The merge only adds what was missing and never replaces a bundled answer, so corrections accumulated against the platform over past releases are preserved. #277
  • getRemovedFields() returned field names in lower case. Security.stripInaccessible reported {Obj__c={opttext__c}} where the platform answers {Obj__c={OptText__c}}. Set<String>.contains() is case-sensitive in Apex, so any caller checking the map for a specific field name never matched. The ordering of that set is now stable between runs as well. #285
August 6, 2026
1.17.4
Fixed
  • An unset Checkbox on a field Nimbus has no local metadata for read null instead of false. A Checkbox never reads null on the platform — an unset one is false, on a brand-new in-memory record as much as a stored one. Nimbus already did that for fields it knows, but a field belonging to an installed managed package has no local metadata, so the read fell through to null and code like settings.isEnabled() && !System.isFuture() threw a null dereference inside a trigger. The metadata default is still applied server-side on insert only, and getPopulatedFieldsAsMap() stays empty, both matching the org. #315
  • getInstance() returned null for a hierarchy custom setting that lives inside a managed package. The platform never returns null here: with no stored record it hands back an empty record whose Id is null. Nimbus only did that when it could confirm the object was a hierarchy setting, and that check reads local object metadata — which a packaged setting does not have, since your project carries only its own extension fields on that object. So Settings__c.getInstance().SomeField__c, an everyday shape in package-aware code, died on a null dereference. An object Nimbus knows about and knows is not a custom setting still returns null, so nothing it can classify changed. #314
  • Syncing permissions from your org destroyed the permission set groups it was meant to refresh. sync --include-permissions reported success while every custom PermissionSetGroup vanished, along with a large number of permission sets and grants. Replacing an existing row deleted it and re-inserted it — but that delete is a soft delete, so the row stayed, its primary key stayed taken, the re-insert failed, and the row was left flagged deleted and invisible to every query. A group correctly seeded from the org was therefore destroyed by the attempt to overwrite it with the identical definition from your repo. Rows are now updated in place, and any row an earlier version left in that state is revived rather than needing a fresh database. Two further causes fixed alongside: a permission row naming an object from a managed package overflowed a column sized from the field’s describe, and all three failures were written to a stream discarded at default verbosity — they now print unconditionally, because a missing grant surfaces much later as a confusing "List has no rows". #306
  • A relationship the local schema resolved to nothing produced a query with a leading dot. AsyncApexJob shipped a field literally named ApexClass beside the real ApexClassId; no such field exists on the platform, where ApexClass is the relationship name. A query builder resolving ApexClass.Name matched the phantom, found no relationship name on it, and emitted SELECT .Name — so the column read null on every row. Two other bundled objects carried the same shape and were corrected against the org, where the right answer differed for each. A new check over every bundled describe keeps the class closed. #311
  • Grouping by a field on the queried object could fail with "column reference is ambiguous". A plain field inside GROUP BY ROLLUP(...) or a multi-term GROUP BY was written without its table name, so as soon as the query also traversed to a parent whose table has a column of the same name — OpportunityContactRole.ContactId with a hop through Opportunity is the everyday case — the database could not tell them apart. Both are valid SOQL and both now run. #312
  • Aggregate queries now report an unresolvable relationship the way every other clause does. GROUP BY and HAVING over a relationship the local schema cannot resolve used to surface the database’s own complaint, naming an internal alias that appears nowhere in your query. They now throw the same catchable System.QueryException naming the relationship that the SELECT list, WHERE and ORDER BY have thrown since the previous release. #309
  • Inserting a record whose lookup points at a deleted record now fails, as it does on the platform. Salesforce rejects the row with ENTITY_IS_DELETED even when the parent is only in the recycle bin. Nimbus accepted it, so a try/catch that rolls back to a savepoint, an allOrNone = false partial save, or an error-logging framework asserting it wrote a record all took the success branch instead. The check is evidence-based: a row is rejected only when the referenced Id is found locally and marked deleted, so an Id with no local row — an org-real Id, a stub, a forward reference — is still accepted. Updates are not covered yet, because the platform only rejects a reference being assigned to a deleted record, not an unrelated edit to a row that already holds one. #313
  • A roll-up summary that cannot be calculated now says so instead of reading 0. A roll-up whose aggregate could not run kept its previous value, which for a record that never had one is 0 — indistinguishable from a correct 0, so the problem surfaced as a wrong number far from its cause. Three silent paths now report. The one behind them was the metadata cache: it decided it was current by asking whether any file was newer than the newest it had seen, which cannot detect a deleted file, nor one restored from version control or copied with its original timestamps. Either way the cache looked fresh while your metadata had moved on. It now compares how many files there are as well, and will rescan once on first use after upgrading. #308
  • Error messages name the object with its API casing. "No such column ‘X’ on entity ‘Y’" built the entity by lowercasing it and capitalising the first letter, so every multi-word API name came back wrong — Recordtype, Servicecontract — while the column beside it kept the casing you typed. #310
  • nimbus.seed.record says when it cannot do anything. It seeds list custom settings only — the rows are served to getAll()/getInstance() during tests and never inserted, so each test stays isolated. Naming any other object was accepted and silently did nothing, which is indistinguishable from a working line in a file that is committed and shared across a team. It now warns, and the header Nimbus generates documents the real contract instead of advertising a general record seeder. #307
August 5, 2026
1.17.3
Fixed
  • A relationship path the local schema can't resolve now fails instead of quietly returning nothing. Querying a parent field through a relationship Nimbus doesn't know about — a managed package that isn't stubbed, a field that hasn't been synced, a typo — used to succeed and hand back every row with that parent set to null. A selector or batch filtering on the path then processed nothing, so the failure showed up as missing work rather than as an error naming the cause. Salesforce throws System.QueryException for this in every clause, and Nimbus now does the same, naming the first hop that couldn't be resolved. Ordering by such a path previously failed too, but blamed a column that does exist on an entity whose name was spelled wrong; it now reports the relationship. Dynamic queries throw a catchable exception, matching the platform, where a string built at run time has no compile step to reject it; queries written inline keep failing hard, because the platform rejects those at compile time and no catch block can observe that. Polymorphic lookups (What, Who, LinkedEntity, RelatedTo, Parent, and SetupOwner on a hierarchy custom setting) are unaffected — the platform resolves those per row. If a suite was silently relying on one of these paths reading null, it will now surface as a failure, which is the point. #305
  • The account portal's machines card never reached the licensing service, so it showed "0 of N machines registered" for everyone. The card asks the licensing provider for your registered machines from the browser, and the portal's own content-security policy had never allowlisted that origin — so every request was blocked before it left the page, since July 1. The card collapsed the blocked request into an empty answer and rendered "Not connected" even when the machine was registered and healthy, and the Deactivate button was dead the same way. Nothing surfaced it, which is why three earlier releases were verified against the licensing API — where everything worked — while the browser never got that far. The origin is now allowlisted, and the page no longer guesses when it can't ask: a failed request reads "Couldn't load your machines" with the CLI status marked unavailable, rather than asserting that you have none. An outage and an empty licence must not look alike. Machines also no longer claim "last used never" now that the heartbeat pings are gone, and if nothing is connected the overview leads with activating this machine instead of subscription management. #301
August 5, 2026
1.17.2
Fixed
  • Machine activations no longer disappear minutes after a run. This is the actual root cause behind the dropped activations in #301. Nimbus sent the licensing server a single "last used" heartbeat after each validated run — and a machine that sends one heartbeat is treated as heartbeat-monitored: miss the next window (ten minutes) and the server deletes the machine. Nimbus pinged at most once a day, so every activation was removed shortly after every run, which is why machines kept vanishing no matter how often you logged in. Nimbus no longer sends heartbeat pings — a machine that never pings is never monitored and never removed — and the server-side policies have been updated so that even older Nimbus versions, which still ping, keep their machines. Verified end to end: a machine registered by the previous build was removed by the server eleven minutes after a run with no action from this side; a machine registered by this build survives untouched.
August 5, 2026
1.17.1
Fixed
  • nimbus login now reconciles this machine with the license your account actually holds. Previously, a machine with stored credentials reported "already signed in" and stopped — so if your account's license had been re-issued or changed server-side, the machine stayed registered against the old one: the CLI looked signed in while the portal showed no machines at all. Login now always signs in through the browser; if nothing changed it says so, and if the license differs it deactivates the machine from the old license and registers it under the current one. Running nimbus login once fixes an out-of-sync machine end to end. #301
  • Automatic re-registration labels the machine with your account. The v1.17.0 activation recovery created the machine without the account metadata a login attaches — and that metadata can never be added after the fact — leaving recovered machines uncounted by the per-user machine quota. The account id is now stored at login and carried by every later activation, including recoveries. #301
August 5, 2026
1.17.0
Added
  • Sync reads standard-object describes from your org. Standard-object schemas previously came only from the bundled describes, so any standard field gated behind a feature — Entitlements, Field Service, CPQ — was invisible even when your org has the feature enabled: Case.ServiceContractId simply did not exist, and a SELECT traversing the relationship returned nothing. A sync with an org connected now asks that org which standard fields it actually has and folds in what the bundle lacks, relationships included. The merge is additive — the curated bundled metadata stays authoritative for everything it already carries — and fully best-effort: no org, no network, or a partial response leaves the bundled schemas in place rather than failing the sync. #302
  • A dropped Pro activation heals itself. When a licence is still valid but this machine's registration disappeared server-side, the next run re-registers it in place — exactly what nimbus login would do, without you having to notice first. The recovery re-uses an existing activation rather than adding one, so it can never consume a licence slot, and the machine cap is still enforced server-side: a licence at its limit stays at its limit. The run says so when it happens. #301
Fixed
  • Machine identity no longer follows your network. The activation fingerprint was derived from the hostname, which on macOS is network-assigned — joining a different Wi-Fi minted a new machine identity, so each nimbus login registered another activation until the licence policy evicted the oldest: your own machine, forcing another login. Identity is now keyed on a stable per-install id, logging in re-uses the machine already registered (including one registered under the old derivation), and a run that resolves to one worker because Pro stopped applying now says so and why, instead of silently running serial. #301
  • The missing-metadata report blames the table the database named. Any query error containing "does not exist" recorded the query's FROM object as missing — so a query joining or sub-selecting one missing table reported an object that is present and queryable, a false positive sitting between genuine ones with a suggested retrieve for something already on disk. The report now names exactly the relation the database could not find, and errors that name no table at all — a missing column, for instance — no longer register an object as missing. #304
  • Custom fields serialize under their API casing. JSON.serialize, String.valueOf on an SObject, and duplicate-field query errors rendered fields the bundled describes don't carry — custom fields, and the org-augmented standard fields above — as their lowercase storage keys: amount__c where the platform writes Amount__c. All three now use the field's declared casing, matching what the platform produces.
August 4, 2026
1.16.3
Fixed
  • A Restrict-delete rejection names the blocking child object the way the platform does. The message used the object's API name pluralised, which left the __c suffix in the middle of a sentence — rstchild__cs where the platform says rstchilds. It now uses the configured plural label, so an object labelled Member Factory reads "member factories" rather than "member_factorys". Cosmetic unless a test asserts on the full message, in which case it diverged. #295
  • An interrupt no longer truncates a heap profile that is being written. Sending SIGINT while a SIGUSR1 snapshot was still in flight exited the process under the writer, leaving an unreadable file — reported by someone profiling a large run, where the lost snapshot was the one nearest the moment worth capturing. Profile writes and exit are now serialized. #298
Diagnostics
  • A lookup filter that is not enforced now says so. Several filter shapes are deliberately left unevaluated — booleanFilter expressions, $Source and $User criteria, field-to-field comparisons — and all of them were silent, so a save could succeed locally that the org would reject with nothing to indicate why. The run now names the field and the reason. A warning rather than an error: every skipped shape is one the platform accepts, so failing the run would reject projects that deploy cleanly. Inactive and optional filters report nothing, since Salesforce does not enforce those on DML either. #294
August 4, 2026
1.16.2
Fixed
  • Regression in 1.16.1: a lookup filter listing several allowed values matched none of them. A filter criterion whose value is a comma-separated list was compared as one joined literal, so an equals filter with three permitted values accepted no record at all and rejected every insert that populated the lookup. Enforcement was new in 1.16.1 and is correct; it turned a latent parsing bug fatal. A reported suite lost 169 tests. Verified against the platform for all five operators: equals, contains and startsWith match any listed value, notEqual and notContain match none. A filter on RecordTypeId also works now — its values spell record-type names while the column holds an Id, and the two were never reconciled. #286
  • A SOQL statement resolves every bind expression, not just the first one carrying a string. A query with two method-call binds failed with Variable does not exist on the second, and a single bind failed the same way whenever an earlier literal appeared in the WHERE clause. Two binds differing only in their string argument also shared one entry, so one of them silently ran on the other's value — that one produced wrong rows rather than an error. #288
  • OwnerId is populated during before-insert triggers. The platform applies the implicit owner default before before-save automation runs, and the field appears in getPopulatedFieldsAsMap() there. Locally it was applied at save time, so a trigger branching on OwnerId == null took the null-owner path on every local insert and never took it in the org. CreatedById, LastModifiedById and Id are genuinely null at that point and are unchanged. #289
  • A before-save flow no longer fires an extra before-update trigger during an insert. On an object with a record-triggered before-save flow, inserting a record entered the object's trigger twice in the before phase — once in BEFORE_UPDATE context, with Trigger.oldMap populated, against a record that was being created. A before-save flow writing to $Record is an in-memory field assignment on the platform: it consumes no DML and fires no trigger. #290
  • The running user, standard profiles and system tables exist outside a test run. They were populated only when tests ran, so nimbus exec, nimbus app, the language server, the debugger and the MCP bridge all queried empty User and Profile tables. SELECT ... FROM User WHERE Id = :UserInfo.getUserId() returned no rows in anonymous Apex and one row in a test — and in its usual single-record form it threw rather than degrading, so any trigger containing that idiom broke every insert run through nimbus exec. #291
  • nimbus exec -c runs a script that ends in a statement, instead of rewriting it. Inline code ending in insert a; was wrapped in System.debug() and failed to parse, reporting errors at column numbers past the end of what was typed, with nothing to indicate the input had been rewritten. Only a trailing bare expression is wrapped now, and semicolons inside string literals and for headers are no longer read as statement boundaries. #292
  • A single-record Database result carries the platform status code. Database.delete(record, false) reported UNKNOWN_EXCEPTION where the platform reports DELETE_FAILED, while the list form of the same call was already correct — so the two disagreed with each other. Upsert and undelete had the same gap. #287
  • Deleting a parent blocked by a Restrict lookup fails, instead of orphaning its children. A lookup declared with a Restrict delete constraint carried the constraint as false on every relationship, so the delete went through. Soft-deleted children correctly do not block. #191
  • CurrencyIsoCode exists only when MultiCurrency is enabled. The field was present on every object while UserInfo.isMultiCurrencyOrganization() reported false — an internally inconsistent org, and a false pass: code referencing it compiled and queried locally, then failed against any single-currency org. A single-currency org now answers No such column 'CurrencyIsoCode', character for character, and --feature MultiCurrency matches an enabled org. #270
  • A finalizer's wrapped exception names where the failure was thrown. A governor-limit failure reaching a finalizer arrived as a bare System.AsyncException with no location. The platform appends the whole stack, innermost frame first, on its own line. #266
  • An invocable action reports its output keys as declared. getOutputParameters() lowercased them, so iterating or comparing keySet() saw outvalue where the platform returns outValue. Lookups already worked, being case-insensitive. #284
  • Content objects describe as readable to any profile. Inside System.runAs for a minimum-access user, ContentVersion and Attachment described as unreadable at object and field level; on the platform both are readable to every profile. Record visibility is enforced separately and is unaffected. #144
  • Id is settable in a constructor, and Contract and Profile no longer gain fields they do not have. Every describe reports createable=false for Id, but the compiler special-cases it — new Account(Id = '001…') is valid Apex and one of the most common test idioms there is. Separately, Contract was given a Name field (its name field is ContractNumber) and Profile an OwnerId; neither exists on the platform. #297
Diagnostics
  • A run that has to be killed can now be profiled. NIMBUS_MEMPROFILE wrote only at clean exit and only the cumulative-allocation view, which is the wrong tool twice over for a run growing without bound: such a run gets killed, and the question is what is being retained. It now writes the heap profile, is flushed when the run is interrupted, and on macOS and Linux SIGUSR1 writes a snapshot mid-run without stopping it — so growth can be sampled at several points and diffed. #298
August 3, 2026
1.16.1
Fixed
  • Regression in 1.16.0: an external lookup rejected a key that looked like a Salesforce Id. External and indirect lookups began describing as references in 1.16.0, which was correct — but the save path then validated their values as Salesforce cross-references. An external lookup holds the external record's External ID, an ordinary string, so any key that happened to be Id-shaped failed with INVALID_CROSS_REFERENCE_KEY while a non-Id-shaped one passed. That is not exotic data: an external system keying people by their Salesforce User Id is common, and one reported suite lost 512 tests to it. The same mapping also gave these fields an 18-character column, so an external key longer than an Id would have failed to store — fixed in the same change, before anyone hit it. #283
  • Describe attributes reached Apex for the first time. isGroupable(), isSortable() and getByteLength() answered false/false/0 for every standard field, in every project — the shipped metadata carried the real values and three separate conversions dropped them on the way. Account.Name now reports groupable and sortable with a byte length of 765, and Account.Description neither with 96000, matching the platform exactly. #275
  • Numeric fields report their own precision, scale and digits. Every field answered a precision of 18 — including text fields, which is what gave it away. The platform is per-field: currency 18, percent 3, integer 0, and digits is 8 on an Integer and 0 on Currency even though both carry a precision. #281
  • Strings sort by Unicode collation rather than by code point. An underscore sorts before a dot on the platform, and both before letters; locally the dot came first, so a query ordering on a name containing punctuation came back in a different order than in the org. Case-insensitivity is unchanged. #257
  • A change event's fields reject a dynamic write. put() on a change-event field now raises System.SObjectException: Field X is not editable, as the platform does. Constructing one with field values, and assigning to them, stay allowed — the platform permits both inside a class, which is where test code lives. #267
Added
  • Lookup filters are enforced on save. Salesforce evaluates an active, required lookup filter on Apex DML, not only in the UI — a record pointing at a row that fails the filter is rejected with FIELD_FILTER_VALIDATION_EXCEPTION and the filter's own error message. Locally such a row inserted cleanly, so this was a test passing on code the org refuses. Deliberately narrow: only filters expressed as plain criteria on the looked-up record are enforced, and filters using a boolean expression, $Source or $User are left alone rather than approximated — a wrong rejection breaks working code, which is worse than the missing one it replaces. #258
August 2, 2026
1.16.0
Added
  • Search.find and Apex invocable actions execute instead of returning stubs. Both previously handed back an empty shell, so a class that dispatches to an invocable action or reads SOSL results could be called but never actually exercised — the test passed without testing anything. They now run the real work: an invocable action resolves its target class and invokes it, and SOSL returns matching records. The error contract was verified against the platform in the same pass, including what an empty search snippet returns. #260 #261
  • FormulaEval evaluates real expressions. Formula.builder() returned null for every expression given to it — including a bare string literal — so anything built on it silently produced nothing. It now evaluates, and $User and the other running-user globals resolve to the user actually executing rather than to blank. #241 #251
Fixed
  • Reading a field you did not query throws, instead of quietly returning null. The platform raises SObjectException: SObject row was retrieved via SOQL without querying the requested field; locally the read returned null, so a missing field in a SELECT surfaced as a wrong value somewhere downstream rather than as the error that names it. This is the single most common way a test passes locally and fails in the org. #229
  • An explicitly-null field is part of a record's identity. new Account(Name='X') compared equal to new Account(Name='X', Industry=null), and a Set holding both kept one — the platform treats them as two different records. Sets, Maps and == now agree with each other and with the platform, including the subtler cases: a null a query produced is not part of identity, while assigning null afterwards makes it so. #274 #89
  • Whitespace is trimmed at save, on every text type. The platform strips leading and trailing whitespace when a record is written and stores a whitespace-only value as null; values were being kept verbatim, so an assertion comparing a stored value against a trimmed one failed locally and passed in the org. Text, text areas, rich text and encrypted text all behave the same way, and interior whitespace is preserved. #240 #252
  • Configured field defaults apply at insert, and survive an explicit null. A field left unset now takes the default declared in its metadata, and a standard field explicitly set to null gets the platform default re-applied rather than staying empty — which is what the platform does. A required field given only whitespace is rejected as missing. #156 #250 #272
  • A failed DML row reports its real error. Database.SaveResult.getErrors() carried placeholder data, so error-handling code — the branch a negative test exists to cover — was being exercised against something that did not resemble a real failure. Status code, message and field list now reflect what actually went wrong. #273
  • A DML failure inside a trigger surfaces on the outer operation. When a trigger's own DML failed, the failure did not propagate to the statement that fired it, so the caller saw a successful save of a record whose trigger work had not happened. #172
  • Rich text describes as a text area, and isHtmlFormatted() answers. A rich text field reported STRING where the platform reports TEXTAREA, and the one attribute that distinguishes it from a long text area was hardcoded false — so code branching on either took the wrong path silently. #271
  • Describe answers no longer carry one org's permissions. The bundled standard describes were captured from a user without the Marketing User permission, so every Campaign field — Name and Status included — reported as non-createable to every project. Permission-dependent flags now model a fully-permissioned user, which is what the separate permission layer is for. #262
  • Describe attributes that were being dropped on the way to Apex. nillable, restricted-picklist flags and their labels, groupable, sortable and byte length were present in the shipped metadata but lost before Apex could read them, so every field claimed the same defaults. A lookup to an external object now resolves as a reference rather than as text. #196 #199 #280
  • Declared scale and precision survive, and scale() counts decimal places. A Number(18,2) field described as scale 0 once a project had synced its schema, and Decimal.scale() counted exponent digits on a value in scientific notation. #254 #228
  • SUM and AVG accumulate in exact decimal and come back as Decimal. Aggregates were accumulating in binary floating point, so summing currency drifted by fractions of a cent over enough rows, and the result's runtime type did not match what the platform returns. #228
  • A String no longer casts to a number. (Integer) someString compiled and ran locally where the platform rejects it, so the rejection arrived on deploy. Inner-class references are resolved strictly for the same reason, and a malformed-JSON error now names the position the platform names. #233 #218
  • Invocable and platform-event contracts match the platform's rejections. An invocable request class without a visible no-arg constructor now fails the invocation as it does in the org; a platform event whose field exceeds its declared length is rejected per row at publish; and two APIs that do not exist on the platform — Invocable.Action.Result.getOutputValues and the FeatureManagement String and Datetime parameter methods — are rejected rather than quietly answering. #169 #170 #264 #265
  • A deeply nested relationship query no longer fails on a column name. Long relationship paths generated an alias past PostgreSQL's identifier limit, so the query failed outright on projects with long object and field names. #253
August 1, 2026
1.15.0
Fixed
  • Identical runs now produce identical results. Seven separate sources of nondeterminism made some suites report a different set of failures each time they ran, on unchanged code — the kind of thing easy to dismiss as a flaky test. Test execution order came from map iteration; the table truncate raced the seeding of RecordType rows, so a whole run could see an empty RecordType table and silently match nothing on every RecordType.DeveloperName join; Limits.getCpuTime() read a wall clock, so real code calibrating batch sizes against it planned differently run to run; grouped query results and Trigger.old both came back in whatever order Postgres chose; datetime round-trips lost precision; and distinct child relationships sharing a name collapsed to one arbitrary survivor. All seven are fixed. A run that reports different results from the previous one is now telling you something real.
  • Datetimes are stored at second granularity, so a timestamp survives a round trip. Postgres keeps microseconds and Apex Datetime carries milliseconds, so a value read out of a record was strictly smaller than the one stored: a record could not match its own timestamp. WHERE LastModifiedDate = :recordsOwnValue returned nothing, and <= excluded the record it came from, which is what made time-window queries look intermittent. Verified against the platform, which stores all datetimes to the second — including writable fields, where 05:06:07.123 reads back as 05:06:07.000. #247
  • A run that matches no tests is no longer reported as passing. A typo'd pattern, a class that failed to register, or a stale cache produced All tests passed! and exit code 0 — a green CI run in which nothing executed. An explicit pattern matching nothing is now an error with a non-zero exit; running with no pattern on a project that has no test classes still warns and exits 0. #243
  • Compound assignment on a static held from another class discarded the previous value. MyClass.total += 3 stored 3, whatever total held, so every cross-class accumulator on a static — counters, error strings, log buffers — kept only its last contribution, silently. Writing total += 1 inside the declaring class was always correct, and ++ was too, which is why this stayed hidden. #249
  • Record-level sharing follows the object's Org-Wide Default, and applies to inline SOQL. Sharing was enforced on Database.query but not on [SELECT ...], so a with sharing class read other users' private records through inline SOQL and a negative sharing test passed locally that fails in the org. Separately, the filter also fired for any restricted profile regardless of OWD, hiding rows the platform returns. Verified live: sharing follows OWD and nothing else, and the query form is irrelevant. #244 #245
  • Permission checks at the runAs and test-entry boundaries. FeatureManagement.checkPermission saw a custom permission the test itself had just granted; the platform keeps the outer context fixed for the whole transaction and only a runAs block sees the new grant — and only inside the block. An inherited sharing class called from a without sharing test class also inherited the bypass; a test class's declaration does not propagate, because the test method is the entry point. #174 #167
  • A dynamic query can bind a property with a getter. :someProperty resolved to nothing where a plain field or local in the same query resolved fine, and the unresolved bind then degraded to an empty array — so the query returned no rows instead of erroring. An unresolved bind now raises Variable does not exist as the platform does. In one compatibility project this moved six tests from failing to passing, all of them queries that had been silently returning nothing. #246
  • Rollup summary cascades propagate past one level. A grandparent whose rollup summarizes a parent's rollup field kept its previous value forever — a wrong number rather than an error. Verified against a real two-level master-detail chain, where both levels recalculate and cost the caller nothing. Rollup filters were also corrected: a comma-separated equals list is an any-of set rather than one literal string, notEqual and notContain count null rows, and contains/startsWith match the platform's casing rules. #237
  • Formula null comparisons are type-dependent. ISBLANK-style checks against a blank Text or an empty Lookup evaluate to true, while the same comparison on a Date or Number is undefined — so IF(Lookup__c == null, 'New', TEXT(Lookup__r.Field__c)) returned blank instead of taking the true branch. #239
  • Bind operands are recognised by one scanner instead of three. The rule for how much text after : belongs to the bind expression existed in three hand-written copies that drifted apart, and each reported failure taught only some of them — one fix turned a loud SQL error into a silent zero-row answer. There is now a single implementation, checked in CI against the Apex grammar itself, so a bind operand wrapped across lines by a formatter is read the same way everywhere. #242 #224
  • Twelve reserved identifiers are rejected before they reach a deploy. Naming a variable where, select, limit, and, in or seven others compiled locally and failed on the first deploy. Each was verified against the platform in both directions — order, offset, with, count, first and last read like keywords but are legal, and are not rejected. #94
  • A crash resolving custom Id prefixes under parallel execution. A cache write shared across workers was unsynchronized, so a run with enough concurrent inserts died outright with a Go fatal error rather than a test failure. It needed several tests inserting records at the same moment, which is why it looked like an occasional unexplained abort on large suites.
  • Trigger-local classes, hierarchy fields, and standard objects that were missing. A class declared inside a trigger body parsed but never resolved. Hierarchy custom-setting fields now map to self-referencing lookups. Report, Dashboard, Schema.Location and ProcessDefinition are provisioned rather than resolving to null. #129 #162 #232 #236
  • ORDER BY ties break on Id. Rows tied on the sort key came back in whatever order the query plan produced, so a suite could pass a query's ordering in isolation and fail it under load. The platform resolves those ties by creation order. #234
  • Async jobs run as the user who enqueued them, and checkPermission resolves a permission named with or without its namespace prefix. #145 #166
  • Restricted picklists keep the caller's casing in memory until the record is re-queried, and List.contains/indexOf preserve the runtime numeric type. JSON.deserialize produces typed Time values, and newSObject(null, true) populates the platform's implicit defaults. #96 #101 #113 #98
  • Overload resolution types a parent-relationship argument (x.Parent__r) as its custom object rather than a generic row, so the intended overload is chosen. #70
Added
  • --exclude accepts glob patterns, and the config key works. --exclude '**/dist/**' now spans directories; a trailing slash means the directory and everything in it; plain substrings behave as before. nimbus.test.exclude existed in the schema but was never read — it now works and merges with the flag, and because nimbus.properties is also read from your home directory, it can be set globally. #216
  • Test.testSandboxPostCopyScript is implemented. It was the only entry point for testing a SandboxPostCopy implementation, so those classes could not be covered locally at all. runApexClass is invoked synchronously with a SandboxContext carrying the supplied ids and name. #227
  • Permission set groups seed their companion permission set, so code that queries a group's aggregated permissions resolves them. #99
  • nimbus validate rejects a nested subclass that hides a visible private method. The platform refuses to save it; locally it compiled, so the rejection arrived on deploy. #235
July 31, 2026
1.14.0
Fixed
  • Upgrading may surface new failures, and that is the point of this release. Several fixes below turn code that quietly passed locally into code that fails the way the platform fails it — invalid casts, iterating a query locator, strict JSON deserialization. A test that starts failing after this upgrade was almost certainly failing in the org already.
  • Decimal is arbitrary-precision. Decimal was backed by a float64, so values the platform represents exactly could not be. Decimal.valueOf(1).divide(3, 33) returned 9E-15 and divide(3, 20) returned digits of the 64-bit integer limit rather than a quotient; 2.345.setScale(2, HALF_EVEN) rounded the wrong way because 2.345 has no float64 form; 1.0/3.0 stopped at 16 digits where the platform gives 33; 1E+3 rendered as 1000; and a trailing zero was lost through JSON.serialize. All eight rounding modes now compute exactly, division carries 33 significant digits, and scale survives rendering and serialization. Double keeps IEEE semantics — that is its platform behaviour, and the Decimal/Double distinction is preserved. #217
  • Invalid casts throw TypeException. Casting an Object-held value to an incompatible type silently succeeded, so code that fails in production passed locally. (Integer) someString, (String) someInteger, (Contact) accountRecord, and the boxed narrowings (Integer) objectHoldingDecimal and (Integer) objectHoldingLong now raise the platform's Invalid conversion from runtime type X to Y. Legal shapes are untouched: direct narrowing, widening, Id/String interchange, upcasts, null casts and downcasting a generic row to its concrete type. #218
  • Setup, metadata and content objects pass USER_MODE under System.runAs. Access was derived from an allow-list, so any object absent from it read as denied rather than unknown — twenty objects a non-admin can read on the platform, including User, Profile, PermissionSet, RecordType, ContentDocument, Attachment, AsyncApexJob and ApexClass, failed with sObject type is not supported. Object-level read is granted for these; field-level security still applies, so negative FLS tests keep failing for the right reason. #78
  • Three SOQL bind and alias gaps. A bind in a HAVING clause was never substituted and reached Postgres literally, while the same bind in WHERE worked. A bind operand combining an index with a method call — :entries[0].getValue() — was scanned only as far as the index, stranding the rest in the generated SQL. And relationship aliases in a FROM clause survived into SQL as schema qualifiers, so even SELECT Id, c.LastName FROM Contact c failed. #223 #224 #128
  • Set<SObjectField>.addAll() deduplicates against add(). The two methods keyed elements differently, so the same field token added through each landed twice. A SELECT list built from such a set then carried a duplicate projection, which the duplicate-field check rejects — one reported suite saw 129 failures from this single cause. #222
  • Overload resolution respects Id as a distinct static type. With both a String and an Id overload in scope the String candidate always won, because Ids are stored as strings and the static type was gone by the time resolution ran. Lookup fields, elements of an Id collection and call results now keep that type. In one compatibility project this alone moved 34 tests from failing to passing. #211
  • JSON: strict deserialization, key order, and parse-error wording. JSON.deserializeStrict behaved exactly like the lenient variant for Apex class targets, accepting unknown fields — the one thing it exists to reject; it now throws, descending into nested members and collection elements. Deserializing a map preserves the document's key order and original casing rather than returning lexically sorted, lowercased keys. And malformed input inside an object reports the platform's wording. #210 #141 #221
  • FORMATDURATION and TIMEVALUE match the platform. FORMATDURATION was missing entirely; its two forms also differ — a seconds count renders unbounded hours (86400 is 24:00:00) while the difference between two Datetimes grows a days component (1:00:00:00). TIMEVALUE required a fractional component the platform requires, kept the milliseconds it had been discarding, and reads those digits as a millisecond count — .5 is 5ms, not 500. A formula must also now be a single complete expression: This will not compile!!! was parsing as an identifier and being accepted. #127 #183
  • Private methods are not overridden from API 61.0. A subclass method with the same signature replaced a base class's private method at every API version. The platform stopped doing that at 61.0, gated on the declaring class's version — so a base compiled at 60.0 still calls the subclass method, and one at 61.0 calls its own. #126
  • Describe results reflect what the object actually is. A custom object's Name field reports isDefaultedOnCreate for both Text and AutoNumber, and an AutoNumber name is correctly non-writable. Custom objects expose their generated __Share child relationship, except under master-detail where the platform has none. Five standard objects that ship no bundled describe — BusinessProcess, CaseStatus, ContractStatus, PartnerRole, SolutionStatus — resolve instead of evaluating to null, and the type check and getGlobalDescribe now read one source so they cannot disagree again. #181 #197 #212
  • Rendering matches the platform. String.valueOf of a Map or Set renders sorted — case-sensitive for text, numeric for numeric keys — while iteration stays in insertion order, which is what the platform does. Set<Integer> no longer leaks internal type tags into that output. Date.format() returns the locale form (3/5/2024) instead of ISO, and Date.parse round-trips it. A null element in String.join renders as empty rather than the text "null". #219 #220
  • A custom object's Text Name follows the platform lifecycle. The fallback that fills a null Name with the record Id stored the 18-character form, wrote it back onto the in-memory record, and only ran on insert. The platform stores the 15-character Id, leaves the in-memory Name null until re-queried, and re-derives it when a Name is set back to null on update. #189
  • A query locator is not iterable at run time. Database.QueryLocator compiles as an Iterable but the platform fails on for (SObject s : locator) with an internal fault no catch block stops. That is reproduced faithfully rather than as a catchable exception, because a catchable one would let the guard pass locally and die in production. Assigning the iterator directly, with or without a cast, works. #188
  • Records from Test.createStubQueryRow are read-only. Assignment now raises Record is read-only, and the marker survives clone() and deepClone(). #178 #187
  • Metadata the platform accepts no longer costs a whole file. An HTML entity such as &nbsp; in a custom label, or a bare ampersand in a picklist value, made the XML decoder reject the file — and because labels live in one aggregate file, a single entity silently resolved every unrelated Label.X to its own key. Both are now read the way the platform reads them, and a label that stays malformed costs only itself. #177 #200
  • Custom metadata ORDER BY ties break on DeveloperName. Tied rows kept load order; the platform falls back to DeveloperName, which is also the order getAll().values() returns. #176
  • Trigger.newMap is null before insert. The map is keyed by Id and the records have none yet, so the platform withholds it — nimbus built an empty map, turning a null-reference failure into a silent miss. Trigger.new is still populated. #190
  • Stub and array typing corrections. Test.createStub rejects inner Apex types as the platform does, and a rank-1 array of a generic keeps the generic as its element type so List<String>[] and List<List<String>> agree. #214 #213
Performance
  • A bulk insert no longer consumes SOQL and DML in proportion to the parent records it touches. Rollup-summary recalculation issued one update per affected parent, and each of those fired that parent's triggers — so every statement and query those triggers make was multiplied by the number of distinct parents in the batch. Cost was linear in parent count while staying flat in record count, which is why it only appeared once a batch spanned many parents. Measured on a fixture with 25 distinct parents, a single insert fell from 101 DML statements and 119 queries to 16 and 15 — the same figures as one parent. #225
July 29, 2026
1.13.0
Added
  • SObjects are graph nodes, and a class that writes an object now visibly reaches its triggers. Standard objects, custom objects and custom settings join the dependency graph with edges that distinguish reading from writing, plus an edge from each object to the triggers that run on it. That last one retires the graph's oldest caveat — trigger inbound edges were documented as "not derivable from source". The chains explain edges a coverage run could previously only report as unexplained: on NebulaLogger 110 to 16 across the whole metadata arc, on NPSP 953 to 329, on EDA 890 to 576.
  • Flows are graph nodes, in both directions. DML on an object reaches its record-triggered flows the same way it reaches its triggers, and a flow's own reach — the Apex it calls, the subflows it runs, the objects it writes — is visible from the flow side. A flow's status is carried through: an inactive flow's edges describe what it would reach rather than what runs, and say so.
  • Custom permissions and static resources complete the metadata arc. FeatureManagement.checkPermission with a literal name is a static edge and a computed one is recorded at runtime; only names matching a declared permission count, since an unknown name returning false is not a dependency. Static resources are reached through Test.loadData, PageReference.forResource and exact-name queries — enumerating queries deliberately record nothing, so the unread signal stays meaningful. Both kinds carry real file paths, so double-click opens the source.
  • SOSL supports WITH HIGHLIGHT and WITH SPELL_CORRECTION. Both clauses previously failed to parse. #146
Fixed
  • Writes to platform-maintained fields are rejected, as the platform rejects them. put() on CreatedDate, IsDeleted, LastModifiedById and the like now throws Field X is not editable, and a read-only compound parent such as Contact.Name or MailingAddress reports the platform's distinct Invalid field X for Y instead. Reported against a Change Data Capture event, but nothing enforced this for any object. Id stays assignable — describe calls it non-createable, yet Apex permits it and mocking frameworks depend on that. #158
  • A finalizer receives the exception the way the platform hands it over. Finalizers run in a fresh invocation, so the stack is gone: getLineNumber() is -1 and getStackTraceString() reads External entry point. A custom exception keeps its type and message; only a failure no catch block could have handled arrives wrapped as System.AsyncException, carrying the original serialized into its message. #153
  • ContentVersion keeps its platform-managed lifecycle fields. A new document's first version gets VersionNumber and IsLatest; adding a version to an existing document numbers it and demotes the previous latest, which previously never happened at all. #139
  • instanceof follows interface inheritance transitively. A value typed as a child interface did not satisfy the parent it extends. Not stub-specific — the same check serves ordinary objects, and a plain class had the identical problem. #130
  • addError carries field metadata through to Database.Error. The field overload's token survives, and a component field reports against its compound parent the way the platform does — an error on FirstName comes back as Name. #136 #148
  • ProcessInstance and DuplicateRecordItem are described. Both objects were absent from the schema entirely, so approval child relationships reported none and field describes failed. A field can also be inherently non-updateable regardless of permissions: isUpdateable() consulted only field-level security, so a non-reparentable lookup reported true for anyone who could edit the object. #143 #147
  • Platform-managed field defaults are applied on insert. EmailMessage.Status defaults to Draft, and Entitlement.Status is derived from its active window — Expired once the window has ended, Active inside it, Inactive before it starts or with no dates at all. Both land before before-insert triggers run, so a trigger can still override them. Also adds EmailMessage.IsExternallyVisible, which was missing from the schema. #157 #163
  • Versioned $Api server-URL globals resolve in Flow formulas. $Api.Enterprise_Server_URL_360 and its partner equivalent evaluated to nothing, so any condition referencing one silently compared against null. Host and org identity stay simulated, matching what the interpreter reports elsewhere. #150
July 29, 2026
1.12.0
Added
  • Custom labels are part of the dependency graph. nimbus graph Label.Order_Error answers who reads a label: the classes that name it in source, everything reaching those classes, the tests among them — and, after a coverage run, the readers no source search can find. Nimbus executes your Apex, so when System.Label.get('', 'Order_Error_' + status) runs, the name is resolved at that moment and the read is recorded exactly. The overview counts labels nothing in Apex references; because Flows, LWC, Aura and formulas are not read, that means unreferenced from Apex — the output says so rather than letting it read as a delete list.
  • Custom metadata types are part of the dependency graph, and metadata-driven dispatch is now a visible chain. A TDTM-style dispatcher queries a __mdt type whose records name handler classes — a path that used to reach its handlers with no reference to follow, and the single case the graph's own limits called out as structurally invisible. nimbus graph Handler_Config__mdt reports who queries a type and which classes its records dispatch to (record values are matched against your classes before an edge is drawn), and Type.forName resolutions are recorded at runtime on coverage runs, so dispatch through a computed class name shows up too. Coverage edges the new chains explain stop being reported as unexplained: on NebulaLogger, unexplained runtime-only edges dropped from 110 to 78.
  • The interactive graph adds metadata on your terms. The resting view stays classes and triggers only. Labels (squares) and custom metadata types (diamonds) are switched on from the display panel; searching does not wait for the toggles — a query matching a hidden label or type reveals it while the query is active, and label: / mdt: prefixes narrow the search to one kind. Tooltips carry a label's value and reader count, and a type's record and querier counts. Exports keep the class topology by default; --include-labels and --include-metadata opt metadata nodes into dot and mermaid output, and the JSON payload always carries everything.
  • Triple-quoted multiline strings parse and evaluate. The API 67 literal's semantics were pinned on an org first, because none of them are guessable: the newline after the opening ''' is mandatory — inline content is a compile error on the platform and is rejected here too — that leading newline is stripped while the trailing one before the closer is kept, escape sequences apply inside the body, and a lone quote is literal. #140
Fixed
  • Deleting a record advances LastModifiedDate — it was moving backwards. Delete stamped the transaction's start time, so inside a test transaction a deleted record carried a timestamp earlier than its own insert. Undelete and bulk delete had the same flaw. Code ordering by LastModifiedDate was getting a value that had moved the wrong way, not a stale one. #121
  • ApexPages.Message keeps its severity. The documented new ApexPages.Message(ApexPages.Severity.X, ...) form silently produced an empty severity, and getSeverity() now returns the enum itself so it compares equal to the constant it came from. #134
  • getLabelPlural() returns a value instead of null. The plural is derived from the API name with the platform's own defaulting rules — consonant-y becomes -ies, sibilants take -es, custom-object suffixes stay outside the plural. #97
  • Polymorphic child relationships report a null relationship name rather than an empty string, so Assert.isNull() on them stops failing on a technicality. Named relationships keep their names. #122
  • The missing-feature-parameter message matches the platform. A correction to v1.11.2: the wording shipped there had been reconstructed from a localized org response; verified against an English-locale org it is Unable to retrieve feature parameter X. No results found. #120
  • A typed top-level Map deserialized from JSON keeps the document's key order and original Id casing. Both were lost structurally, so keySet() came back lexically ordered and lowercased. deserializeStrict into Map<Id, SObject> also materialises real SObjects — verified against an org rather than assumed. #141 #142
  • JSON.serialize emits SObject fields in assignment order with attributes first, matching the platform instead of alphabetising. Completes the round-trip work that fixed field casing: queried columns and later assignments trail alphabetically, exactly as before. #138
  • A constructor whose name is not the class name is rejected at load, with the platform's "Invalid constructor name" wording — previously the class ran locally and failed only on deploy. Inner classes compare against their own simple name. #152
  • Unserializable types report the platform's own message. JSON.serialize of a bare Schema.SObjectField (or a list of them) now says Apex Type unsupported in JSON: ..., capital T included. #137
July 29, 2026
1.11.2
Added
  • System.Url is implemented. Constructing and inspecting URLs previously failed outright. Apex wraps Java's URL, so the conventions are Java's and are easy to get wrong from first principles: an absent port reports -1 rather than 0, an absent query or fragment is null rather than an empty string, and getFile() is the path plus query. Relative construction against a base URL, toExternalForm(), and the platform's rejection message for an unparseable spec all match. #109
Fixed
  • Doubles render the way the platform renders them, and cross-type numeric equality is now genuinely numeric. Apex exposes Java's Double.toString thresholds — plain while the magnitude sits between 10-3 and 107, scientific outside it, always with a fractional digit — and Decimal deliberately does not share them. The same value prints as 1.0E7 from a Double and 10000000.0 from a Decimal; a Double zero is 0.0 where a Decimal zero is 0. Fixing that surfaced a deeper problem: comparisons such as Assert.areEqual(150, someDouble) were passing only because both sides happened to render identically, and fell back to comparing text. Integer against Decimal or Double now compares by value. #104
  • Exceptions stringify in the platform's two formats. System-namespace exceptions render as System.NullPointerException: message, everything else as MyException:[]: message. Which form applies is not derivable from the name — IllegalArgumentException, UnsupportedOperationException and InvalidHeaderException are platform types that nonetheless use the second form, each confirmed against an org. #114
  • Multidimensional array creation parses. new String[][]{ ... } failed to parse at all, while String[][] m; as a declaration and m[0][1] indexing already worked. Now N-dimensional, and String[][] interoperates with List<List<String>>. #116
  • Java POSIX character classes compile. \p{Lower} and its twelve siblings made Pattern.compile throw outright rather than match differently — a total failure rather than a subtle one. Both the plain and negated \P{...} forms work; real Unicode categories such as \p{L} are unaffected. #110
  • SOQL rejects a duplicate projected field. SELECT Id, Id FROM Account now raises QueryException naming the field in its canonical API casing. Aggregates, aliases, TYPEOF blocks, subqueries and FIELDS() are all excluded, and detection compares spellings exactly — matching the platform's case-insensitivity would have rejected queries that real applications run successfully. #125
  • Objects that cannot have record types return an empty getRecordTypeInfos(). Standard value sets such as OpportunityStage, LeadStatus and TaskStatus reported a synthetic Master entry. Objects that can have record types, and every custom object, are unaffected. #133
  • A field named SobjectType shadows the universal property. On QueueSObject, ObjectPermissions, FieldPermissions, ListView and RecordType, the real field wins outright — assigning it to a Schema.SObjectType is a compile error on the platform, so the universal property is genuinely inaccessible there. #124
  • An SObjectField token from the wrong object reports which object it came from. new Contact().get(Account.NumberOfEmployees) now says Account.NumberOfEmployees does not belong to SObject type Contact instead of reporting the field as unknown — which sent you looking for a typo in a name that was spelled correctly. Applies to put as well. #132
  • getPopulatedFieldsAsMap preserves constructor assignment order instead of alphabetising. #131
  • Missing custom labels raise an error instead of echoing their own name. Both Label.get and Label.translationExists previously returned the label name (or false) when a label could not be found, so a typo produced plausible-looking output rather than a failure. Both now raise InvalidParameterValueException naming the label alone; a null namespace means the default namespace and is accepted. #135 #112
  • Feature parameters raise NoDataFoundException when never provisioned rather than returning the identity value for their type, which hid missing-parameter bugs entirely. Values written by a setter are still readable. #120
  • Invalid dates in typed JSON raise JSONException. A declared Map<String, Date> also dropped its value type altogether, so entries came back as Strings with no error at all. #95
  • Reflective construction of System.Location returns a Location with null components rather than null itself. #123
July 28, 2026
1.11.1
Fixed
  • Rounding is half-even for Decimal and half-up for Double — a v1.11.0 regression. The v1.11.0 rounding fix was right for Decimal but applied to the Double overloads too, and left Decimal.round(RoundingMode) ignoring the mode it was handed. Apex resolves these overloads from the static type: Math.round on a Double is half-up (Java's floor(x + 0.5), so ties break toward positive infinity and -1.5 gives -1), while on a Decimal it stays half-even. A numeric literal with a decimal point is a Decimal, which is why literal-based checks never exercised the Double path. Carrying the distinction means a declared type now survives assignment, casts and arithmetic — and, as on the platform, Double combined with Decimal widens back to Decimal. #201
  • finally runs when the exception propagates to an outer catch. Cleanup that resets statics, releases flags or flushes logs was skipped entirely whenever the exception was handled further up the stack — a core control-flow guarantee. #88
  • Collection mutators return what the platform returns. Map.put now returns the previous value instead of nothing, so Integer old = m.put(k, v); compiles and runs; Set.add returns whether the set changed, restoring the if (seen.add(id)) dedupe idiom that previously let every duplicate through; and List.remove with an out-of-range index throws ListException rather than silently doing nothing. #203 #204 #205
  • Mutating a collection while iterating it throws FinalException. One of the most common Apex mistakes was invisible locally: the loop iterated a snapshot and completed. As on the platform the exception is uncatchable, so no defensive catch masks it. #202
  • String comparison, slicing and narrowing match the platform. String.compareTo returns the character delta rather than a normalised -1/0/1 ('abc'.compareTo('ABC') is 32); substring throws StringException on an out-of-range index instead of clamping, so index bugs surface locally instead of only in production; and intValue() truncates to 32 bits. #83 #84 #91
  • Numeric types survive where they are declared. A whole number stored in a Currency, Number or Percent field is a Decimal on the platform, so 250 / opp.Amount gives 0.25 where it used to truncate to 0. The same now holds for static and instance fields. An inexact quotient also renders its digits instead of collapsing to 0, and Decimal.scale() keeps trailing zeros. #92 #103
  • String.format stops eating apostrophes. Apex implements only half of Java's MessageFormat quoting: '' collapses to one quote, but a lone quote is an ordinary character and placeholders keep substituting around it. String.format('It\'s {0}', ...) returns It's x, not Its {0}. #93
  • JSON round-trips preserve field casing and number typing. JSON.serialize emits SObject fields in their canonical API casing ("Name", not "name"), and a number from JSON.deserializeUntyped now answers instanceof Integer, Long, Decimal and Double the way the platform's untyped value does. deserializeUntyped also parses the first complete value and ignores trailing text, and malformed-JSON errors name the offending token and its input location. #85 #86 #105 #118
  • JSON.deserializeStrict is actually strict for SObjects. An unknown column now raises JSONException instead of being silently dropped. #119
  • Regex, math and hashing error paths match the platform. Static Pattern.matches is anchored to the whole input, so format checks no longer accept partial matches; Math.sqrt of a negative and asin/acos outside [-1, 1] throw MathException; NaN == NaN is true, as Apex has it rather than IEEE; a dangling $ in a replaceAll replacement throws; and String.hashCode is Java's, wrapping at 32 bits and case-sensitive. #207 #208 #209 #108
  • Exceptions carry their cause, and enum lookups report the platform error. The Exception(message, cause) constructor records the cause so getCause() can walk the chain, and an unmatched enum name — including null — raises NoSuchElementException with the platform's wording instead of returning null or a type error. #87 #115
  • String.join accepts any Iterable, and Type.newInstance reports a missing no-arg constructor. Joining a class that implements Iterable<T> returned an empty string rather than its elements; instantiating a class with no zero-argument constructor returned an object whose constructor never ran, and now throws TypeException. #102 #107
  • getSObjects distinguishes an unknown relationship from an unqueried one. A name that is not a relationship raises SObjectException; a real relationship that simply was not queried still returns null. #111
  • Enums declared in exec scripts work, and escaping covers backslashes. A local enum in an anonymous block no longer fails on first use, and String.escapeSingleQuotes escapes backslashes as well as apostrophes. DateTime.valueOf also accepts the platform's all-zero sentinel strings. #206 #100 #117
Added
  • nimbus validate catches two more save-time rejections. Reserved identifiers (inner, cast, group, join, outer, package and others) and classes extending Exception without an Exception suffix are both rejected by the platform at save time and were accepted locally — code that ran green here failed its first deploy. The reserved list is verified against a live org and deliberately excludes ordinary keywords, which the parser already rejects, and names such as offset, row, share and view that the platform genuinely permits. #94
July 27, 2026
1.11.0
Fixed
  • Rounding now matches the platform: ties go to the even neighbour. Math.round, Math.roundToLong, Decimal.round() and Decimal.setScale without an explicit RoundingMode all rounded .5 ties away from zero; Apex uses banker's rounding for every one of them. Verified live: Math.round(2.5) is 2, (3.5).setScale(0) is 4, (-2.5).round() is -2. Financial code that lands on exact .5 boundaries now computes the same numbers locally as in production. Explicit RoundingMode arguments keep their meanings. #71 #72
  • Integer arithmetic wraps at 32 bits, like the platform. Apex Integer is a two's-complement 32-bit int: 2147483647 + 1 is -2147483648, silently. Nimbus kept growing past the boundary, so hash-mixing, checksums and overflow-sensitive comparisons diverged. Long arithmetic is unaffected, and Long-producing APIs such as Datetime.getTime() stay 64-bit. #73
  • Date.addMonths and addYears clamp to the end of the target month. January 31 plus one month is February 28 (29 in leap years) — never March 3. The same rule now applies to negative offsets, to Datetime variants (time of day preserved), and the formula-engine ADDMONTHS follows its documented last-day rule. End-of-month billing and renewal logic no longer drifts into the following month. #74
  • List.sort() places nulls first and orders numbers numerically. The default sort put nulls last and compared numeric elements as text, so {10, 9, 2} sorted to {10, 2, 9}. Both now match the platform, dates sort chronologically, and — verified live — a supplied Comparator receives the null elements and decides their position itself. #75
  • A null Boolean in a condition throws NullPointerException, as on the platform. if (nullFlag) silently took the else-branch locally and crashed in production — the dangerous direction for a local runner. All condition positions now throw (if/while/for/ternary/!/&&/||, short-circuiting preserved), and null operands in arithmetic throw a catchable NPE. Fixing this exposed a second, mutually-masking divergence, fixed together: custom metadata checkboxes omitted from a record's XML now read back false — a deployed checkbox is never null. #76
  • String.valueOf(Decimal) uses scientific notation where the platform does. 0.0000001 renders as 1E-7, matching the BigDecimal rule Apex inherits; toPlainString() remains the plain-format API, trailing zeros are preserved per scale, and toString() no longer prints large values like 1.234567e+06. #77
  • Single-row SOQL coercion now covers static field declarations. private static Account held = [SELECT ... LIMIT 1]; assigned the raw list, so a later held.Name = ... failed with UNIMPLEMENTED_ASSIGNMENT — the last member of the family fixed in v1.10.1. #66
  • Overload resolution accepts parent-relationship arguments. Passing record.Parent__r straight into a method reported No matching overload even with exactly one matching signature; binding it to a local first worked. The expression's type is now resolved from the relationship's schema target. #70
  • Setup entities pass secured queries inside System.runAs. Group and UserRole queries with WITH USER_MODE or WITH SECURITY_ENFORCED failed as a restricted user with sObject type not supported — on the platform these are readable for any user, so static caches built on such queries no longer explode order-dependently. #78
  • Subquery-only SELECT lists translate correctly. SELECT (SELECT Id FROM Children__r) FROM Parent__c — the "grab the parent just for its children" fixture shape — failed in test context with a mangled query; the platform's implicit Id is now returned. #79
  • SOQL binds with an inline cast and a fluent chain evaluate fully. WHERE Id IN :(Set<Id>) new Collector().of(rows).getSet() leaked the expression into the generated SQL; the whole constructor-plus-chain operand is now captured and evaluated. The simple-cast form fixed in #25 keeps working. #80
  • Aggregate queries disambiguate same-named columns across joins. Grouping or filtering on a field that exists on both the base object and a joined parent (Parent__c alongside Mid__r.Parent__c) failed with column reference is ambiguous; base-table references are now qualified while SOQL aliases stay untouched. #81
  • COUNT() queries under WITH USER_MODE work for users on materialized profiles. The FLS check treated the aggregate itself as a field name, which only profiles with default CRUD forgave — so every filtered count failed for permission-set-driven users on profiles resolved on demand, while the same query passed on Standard User. Aggregates no longer masquerade as fields, and COUNT(Field) checks the field it aggregates. #82
July 27, 2026
1.10.3
Fixed
  • Record-triggered flows bulkify two more Get Records shapes. An element with no filters, and one whose filter compares a field against $Record with anything other than equality (not equal to, greater than, contains, …), each ran one query per record instead of one per batch — a 200-record insert through such a flow burned 200 SOQL queries and blew the limit on work the platform does in one. Both now run once for the batch. The non-equality form has no IN-clause equivalent, so the batched query fetches the superset its literal filters allow and each record narrows it to its own matches; results are identical to the per-record path.
  • Custom metadata queries no longer count against the SOQL limit when written dynamically or in a flow. Verified live on the platform: a query against a custom metadata type leaves Limits.getQueries() untouched — in every form, static or dynamic, with ORDER BY, WHERE, COUNT() or getAll() — while an ordinary query beside it increments by one. Nimbus already exempted static SOQL, but Database.query on a custom metadata type and both flow Get Records paths each charged one, so the same query cost differently depending on how it was written. Configuration-driven code that reads custom metadata on every path no longer inflates a suite's apparent SOQL usage.
  • nimbus exec now reports unresolved types. A type Nimbus cannot resolve becomes null, and the script usually dies later with Attempt to de-reference a null object naming nothing. nimbus test has always ended with a block listing those references; exec stayed silent. It now prints the same block — including when execution fails, which is exactly when it explains the failure.
July 27, 2026
1.10.2
Fixed
  • Coverage-observed trigger execution now appears in dependency graphs. Triggers have no inbound source reference because DML invokes them, but a coverage run can still observe which test classes executed them. Nimbus left trigger nodes isolated even when that evidence existed; graphs now draw those runtime-only test-to-trigger edges, and opening a trigger node resolves its source file. An existing coverage map with no recorded execution is also reported as present rather than mistaken for a missing map.
July 27, 2026
1.10.1
Fixed
  • A single-row SOQL result assigned to a field or property is coerced like the platform does. Assigning [SELECT …] to a member typed as an SObject left a List<SObject> in the member — silently, with no error at the assignment. The corruption only surfaced later, wherever something called an SObject method on that member, as Unknown List method pointing at code that was perfectly valid. Local variables already coerced correctly; fields, auto-properties and static fields now do too, for both static SOQL and Database.query. Collection-typed members — List<T>, Set<T>, Map<K,V>, T[], Iterable<T> — keep the whole result, as before.
  • Patching a field of a queried record held in an instance field works again. this.record = [SELECT …]; this.record.Name = 'x'; failed with an internal assignment error, while the same code against a constructed SObject succeeded. Re-querying a record into a member and then patching it before update is a common fixture-builder shape; it now behaves the same whichever way the member was populated.
  • SELECT COUNT() returns the row count on seeded setup objects. On objects Nimbus serves from its built-in setup seeding — UserRole among them — a count query returned the records instead of the number, so Integer c = [SELECT COUNT() FROM UserRole] ended up holding an sObject. Counts now come back as an Integer on every object.
  • FieldDescribeOptions is recognised by its bare name. field.getDescribe(FieldDescribeOptions.DEFAULT) — the describe-heavy idiom the platform recommends for skipping expensive picklist and reference materialisation — threw a null dereference, because the enum resolved only when spelled Schema.FieldDescribeOptions. Apex resolves it through the implicit Schema namespace, and so does Nimbus now. Both DEFAULT and FULL_DESCRIBE return the same describe result.
  • Custom labels in the decomposed source format are loaded. A project that opts into decomposeCustomLabelsBeta2 gets one <Name>.label-meta.xml file per label and no aggregate file at all — and every Label.* reference in it silently fell back to the label’s own name, turning assertion failures into a hunt for a product bug. Both layouts now load, and a decomposed file is picked up on save like any other metadata change.
  • Decomposed permission sets merge their objectSettings/ fragments. Under decomposePermissionSetBeta2 the parent file keeps only the header while the object and field grants live in per-object fragments. The permission set was seeded but its grants were not, so a user holding it failed WITH USER_MODE queries with sObject type … is not supported — reading like a schema gap rather than a permissions one. The fragments now merge into the parent, as the Metadata API does on deploy.
  • ContentNote can be inserted. Creating a note threw Attempt to de-reference a null object, taking down every fixture that attaches notes to records. ContentNote is now a first-class sObject: the insert stores the note, derives TextPreview from the content along with the note file type, and returns an Id carrying the ContentDocument 069 prefix that works directly as ContentDocumentLink.ContentDocumentId — the standard attach-a-note-to-a-record pattern.
July 27, 2026
1.10.0
Added
  • The interactive dependency graph now reads like a knowledge graph. On a real org the old viewer drew every class and every label at once — a hairball. The rework shows meaning at the point of attention instead: hover a class and its direct neighbourhood lights up while the rest fades, click to pin the highlight, and the pinned class’s edges grow arrowheads so dependency direction is there exactly when you are looking for it. Search filters by name, test classes and unconnected nodes can be hidden, the layout forces are adjustable, and nodes can be dragged. Labels follow a zoom budget — hubs first, the rest as you come closer. A new layout engine keeps all of it fluid at a thousand classes and stops hub classes collapsing the graph into a knot. Live in the Dev UI now; VS Code and IntelliJ pick it up with their next extension updates, rendering the same graph. In VS Code, double-click opens the class — single click is the highlight.
  • IntelliJ: watch runs report into the standard test runner console. A run triggered by saving a file used to surface as a notification balloon; it now opens the same test runner UI as a run you started yourself, with results streaming in as they arrive. A watch run that could not start — or that matched no tests because the saved file no longer compiles — is reported as a failure instead of "0/0 passed".
Fixed
  • nimbus graph no longer mistakes variables for class references. A class that shares its name with a common variable name — Account, User, Logger — collected a dependency edge from every method that merely declared such a variable, inflating its dependents count and distorting the graph around it. Apex resolves a bare identifier to the variable when one is in scope: the variable shadows the class, and the Schema. prefix exists precisely to reach the SObject past that shadow. The graph now applies the platform’s own resolution rule, so dependents counts reflect actual references. Surfaced by a public logging framework whose test suite deliberately ships empty SObject-named classes as a qualification tripwire — their correct dependent count is zero, and now it is.
  • Multi-variable declarations now contribute their references. In Integer a = Util.calc(), b = 2; the initializers were invisible to the dependency graph — a reference that only appeared in a combined declaration produced no edge at all.
  • IntelliJ: run-test icons no longer drop clicks while the IDE is indexing. During indexing the platform silently disabled the gutter run actions; clicking did nothing, with no explanation. They now work during indexing like the rest of the run UI.
July 26, 2026
1.9.2
Fixed
  • Class-level code lenses now sit on the class declaration. In editors that show code lenses, Run class, Bench class and Coverage on a test class — and Mutate on a production class — were anchored to the first line of the file instead of to the class. In the ordinary Apex layout, where a file opens with a doc block or a license header, that put the buttons inside the comment, and collapsing the comment took them with it. They now anchor on the class line, so they stay visible whatever precedes the declaration.
  • Traces and debug output resolve the source file again for classes loaded per test. Apex type names are case-insensitive, and the class-to-file index is keyed accordingly, but this lookup used the name as written — so it missed for effectively every class and the run fell back to no source file. Stack frames and debug lines emitted while running without coverage collection are attributed to the right file again.
  • A watch run that never starts now reports why. When a watch-triggered run could not get as far as executing tests — a compile error in the file you just saved, a database that would not come up — editors received a completion event of nothing but zeros, indistinguishable from a suite that legitimately contains no tests, and some surfaced it as a pass. The reason now travels with the event so editors can show what actually happened.
July 26, 2026
1.9.1
Fixed
  • Id.getSObjectType() now resolves OrderItem and OpportunityLineItem Ids. Both objects have shipped with full schema support for a long time, but their Id key prefixes (802 and 00k, verified live against the platform) were missing from the prefix table — so resolving an sObject type from one of their Ids came back empty while the same code worked for Order or Opportunity. Line-item Ids now resolve like every other standard object.
July 26, 2026
1.9.0
Added
  • Failure intelligence — nimbus explain. When a test fails, nimbus explain assembles the evidence into one versioned failure contract: the assertion, the code path that produced it, the data it saw, and — new in this release — a comparison against the last run where the same test passed, so "what changed" is an answer instead of an archaeology project. The same contract is served over MCP, so an AI agent debugging your suite reads exactly what you read.
  • Suite triage — nimbus triage. A failing run with hundreds of red tests usually has a handful of causes. nimbus triage clusters failures by source-assigned diagnostic codes and ranks the clusters, so you fix the cause with the biggest blast radius first instead of scrolling the list top to bottom. Unresolved managed-package types are reported as run-level evidence — one line naming the missing stub, not five hundred identical stack traces. Available over MCP through the same construction path as the CLI.
  • Dependency navigation — nimbus graph. Reachability queries over your Apex source: what depends on this class, what does it reach, where are the cycles. Scope by depth, export the result (--out writes Mermaid with a Markdown wrapper you can actually open), or explore it interactively in the Dev UI, VS Code, and IntelliJ — all three hosts render the same graph. Agents get the same answers through the query_graph MCP tool. The output is honest about its one structural blind spot: a static graph cannot see trigger dispatch, and says so rather than pretending completeness.
  • Suite health — nimbus doctor --suite. The doctor now examines your test suite itself: flake suspects backed by recorded run provenance, tests whose duration dominates the wall clock, and isolation hazards — the structural debt that makes a suite slow and untrustworthy, surfaced before it costs you an afternoon.
  • Fixture generation understands your schema. nimbus fixture now generates required-lookup parent records automatically and fills fields with schema-valid values — a fixture for a child object arrives with the whole parent chain it needs to insert cleanly, matching how the platform enforces required relationships.
  • Run provenance — nimbus history. Every run records the conditions it executed under — engine version, isolation mode, parallelism, schema fingerprint. A flake claim can now be checked against evidence: same test, same conditions, different outcome — or an environment difference that explains everything.
  • Every Pro session now registers its machine. CI pipelines authenticating with NIMBUS_LICENSE_KEY register one stable machine per CI provider (named CI — GitHub Actions and the like) no matter how many ephemeral runners execute, and AI-agent sessions register the machine they run on — so the machines list in your portal finally reflects where your license is actually used. Registration is automatic, best-effort, and never delays or fails a run.
  • Quote and QuoteLineItem standard objects. CPQ-adjacent test suites that touch the standard quoting objects now run without stubs.
Fixed
  • --impacted now forces a full run when a trigger changes. Impact selection maps changed classes to the tests that reach them — but trigger dispatch is invisible to a static graph, so a changed trigger selected zero tests and reported a green run against a change nothing had executed. A trigger change now runs the full suite, because a selection that cannot see the change must not claim to cover it.
  • Trigger files now receive coverage attribution. Lines executed inside triggers were counted against no file at all, so trigger-heavy projects saw coverage numbers lower than what their tests actually exercised — and a coverage gate could fail on code that was thoroughly tested.
  • Parallel shard coverage now merges from the executable set. Merging shard results from the hit map alone dropped files a shard loaded but never hit, understating project coverage under parallel execution relative to the identical serial run.
  • NullPointerException now carries a source location. An NPE thrown by the interpreter arrived with no file and line, leaving you to bisect by hand. It now points at the dereference that threw, like every other exception.
  • Machine names and per-user quota tags now actually reach your portal. The licensing server rejects attribute updates after a machine is registered — which every previous release attempted, silently, so machines showed up nameless and per-user machine quotas undercounted. Name and quota identity now travel inside the activation request itself. Existing machines pick up their name on their next nimbus login.
  • A machine deactivated from the portal is now detected, explained, and healed. Previously the CLI kept validating as Pro forever with zero registered machines. Now the state is detected within a day, the message says plainly that your license is fine and only this machine's activation was removed, and nimbus login recognises the situation and signs you straight back in instead of insisting you are already signed in. nimbus login also finally exits non-zero when sign-in fails, so scripted setups can tell.
  • The portal's "Last used" column no longer says "never" for machines in daily use. The portal read a field the licensing API does not have; it now reads the heartbeat the CLI has been sending all along.
July 25, 2026
1.8.0
Added
  • Assured releases — nimbus release and nimbus deploy (Pro). Validate a change set locally, then deploy it to your org, with a signed receipt that records exactly what ran. nimbus release plan reconciles your source against the org read-only, so you can see what a deploy would change before it changes anything; changed-set releases enforce drift, refusing to deploy against an org that moved underneath you. nimbus release promote moves the same validated bundle through your environments unchanged, nimbus release rollback restores the org state a deploy overwrote, and nimbus release status / watch show the org's deploy queue and diagnose jobs stuck in Pending.
  • Signed receipts and segregation of duties. Each receipt carries an Ed25519 attestation signed against a code-reviewed roster you manage with nimbus release keygen. Production deploys can require a separate counter-signature, so the person who validates a release need not be the person who ships it. Release profiles fail closed: requireSigned refuses any unsigned receipt, and requireCodeAnalyzer refuses a deploy whose static-analysis gate never ran.
  • Quality gates recorded in the receipt. Static analysis through sf code-analyzer (with a configurable rule policy), a real mutation-score gate, coverage, and a permission-model policy gate — each result is captured inside the signed receipt, so the evidence a release passed its checks travels with the release itself rather than living in a CI log that outlives nothing.
  • The Assurance console — nimbus assurance (Pro). A self-hosted, read-only web view of every release's signed evidence for the whole team, including the people who never open a terminal — release managers, auditors. Per release it shows who validated it, who deployed it, which checks passed, whether it drifted, and — verified live in the browser — whether the bundle and signatures are intact. Add --token to share it beyond localhost. Your code and receipts never leave your infrastructure; the console reads local receipts, never contacts a Salesforce org, and never deploys. For auditors without Nimbus, nimbus release export produces a portable evidence bundle that nimbus release verify-export re-verifies offline.
  • CI templates and editor integration. Drop-in assured-release steps for GitHub Actions, GitLab, Azure DevOps, and shell-based CI — the validate-and-attest stage for the pipeline you already run, not a replacement for it. The VS Code and IntelliJ extensions gain a read-only Releases view showing the deploy queue, receipt history, and Salesforce CLI status. The Salesforce CLI plugin adds deploy and release wrappers, and nimbus sf ... passes commands straight through to the Salesforce CLI.
Fixed
  • Async job Ids are now globally unique across parallel workers. AsyncApexJob and CronTrigger Ids came from per-test counters that restarted at 1, so under parallel execution two concurrent tests that enqueued an async job inserted the same primary key into the shared job table. Postgres parks a duplicate-key insert on its unique index until the first inserter's transaction ends — and a test holds its transaction for its whole run — so a worker silently stalled for the remainder of an unrelated test and long suites blew the test timeout with spurious "possible infinite loop" failures. Ids are now process-global, parallel pass/fail matches serial exactly, and large suites run measurably faster. (#9)
  • A raised DML-statement limit is now honored on every DML path. The combined statements-and-rows increment used by interpreter DML and bulkified Flow elements checked the raw platform constant instead of your nimbus.governor.dml-statements override, so a lifted cap still threw Too many DML statements: 159 while Limits.getLimitDmlStatements() correctly reported the raised value. Both paths now go through the same effective limit. (#50)
  • A Number(18,0) field now accepts fractional values, matching the platform. Declared scale is display-only on Salesforce: verified live, both Test.loadData and plain DML store a value like 2995.2 in a scale-0 field untouched and read it back unrounded. Nimbus had typed the column as an integer, so a single fractional CSV value failed the entire load with a database type error. Scale-0 Number columns now keep 18-digit integers exact while accepting fractions. Existing project databases keep their old typing until a nimbus reset. (#57)
  • Permission seeding can no longer fail silently or remove rows. Reseeding permission sets, their object and field grants, and permission set group composition deleted each row before inserting its replacement and swallowed every insert error — so a failing reseed could net-remove previously-queryable permission rows with no warning, leaving user-mode queries to fail for the wrong reason. Seeding is now insert-first (a row is only removed once its replacement inserts cleanly), and any failure surfaces as a warning naming the count and first underlying error. (#58)
  • Schema describe predicates and property accessors now resolve. Schema.ChildRelationship's isCascadeDelete / isRestrictedDelete / isDeprecatedAndHidden failed with Unknown method; they now dispatch and carry org-verified values. And the property forms of describe getters (fr.type, cr.childSObject, fr.referenceTo, …) returned null while the equivalent getter methods worked — a missing property now delegates to its getter, so the two forms can no longer diverge, matching the platform where every describe getter is also a property. (#60, #61)
  • A lookup set to an Id of the wrong sObject type is now rejected. Inserting or updating a record whose custom lookup pointed at the wrong object succeeded silently and stored the Id verbatim, so every downstream relationship traversal and cross-object formula quietly evaluated to null. Real Salesforce fails the row with INVALID_CROSS_REFERENCE_KEY. Nimbus now validates non-namespaced custom-lookup values by Id key prefix and throws the platform's DmlException (surfaced as a row-level Database.Error under partial-success DML). An Id whose type Nimbus can't resolve is still accepted, so org-real Ids and stubs never produce a false rejection. (#62)
July 19, 2026
1.7.0
Added
  • nimbus validate now catches 233 of 235 org-rejected error classes in its 294-case conformance corpus, up from 178 of 182. The new checks cover inheritance and override contracts, declaration scopes, annotation placement and arguments, parameter modifiers and duplicates, interface implementation visibility and return compatibility, conflicting sharing modes, invalid generic array shapes, direct construction of the platform Exception type, and source-backed custom object and field resolution. All 59 legal controls remain accepted, and validation continues to report zero intrinsic false rejects across 3,350 deployable open-source programs. The two deferred cases depend on managed-package compilation context that is not present in ordinary project source.
  • Nimbus is now available as a native Salesforce CLI plugin. Install @nimbus-solution/nimbus-sf-plugin and run tests, validation, anonymous Apex, mutation testing, local servers, and account commands through sf nimbus .... The plugin reuses the existing Nimbus login, automatically downloads the correct native runtime when needed, verifies it against the release checksum, and preserves native output and exit codes. Salesforce-style aliases such as sf nimbus apex run test and sf nimbus apex validate are included.
July 18, 2026
1.6.1
Added
  • nimbus validate now catches 178 of 182 org-rejected error classes in its expanded conformance corpus. The new checks cover SOQL and SOSL compile rules, switch statements, constructor chaining, interface and inheritance contracts, illegal modifiers across classes and members, annotation placement, REST and Invocable signatures, generic type shapes, enum declarations, and property accessors. Every case is pinned to a real Salesforce check-only deploy, and the four deferred cases require complete synced-schema or managed-package context rather than another context-free language rule.
  • Validation coverage is now tested through generated Salesforce-versus-Nimbus differential batches. Closely related legal and illegal Apex variants are submitted to both validators so gaps are measured against platform behaviour instead of assumed from documentation. The latest 108-case batch finishes with 108 agreements, while deployable-source testing continues to report zero intrinsic false rejects.
Fixed
  • nimbus validate no longer rejects legal generic forms such as List<void>. Salesforce accepts this unusual type even though raw collections, wrong generic arity, and generic scalar types remain compile errors. Nimbus now preserves that distinction instead of treating all unusual generic forms alike.
July 18, 2026
1.6.0
Added
  • Record-triggered Flows are now bulkified the way the platform bulkifies them. Salesforce advances every interview in a batch through the same element together, so an element that touches the database is one operation covering the whole batch — one query, one DML statement — however many records triggered it. Nimbus ran each interview start to finish in turn, so a Create/Update/Delete Records element consumed one DML statement per record and a few hundred records could exhaust the DML limit on a Flow the org runs without trouble. Interviews now advance as a group and their records are pooled into a single statement, with records that diverge at a Decision continuing on their own branch, exactly as the platform runs them. (#50)
  • nimbus sync --include-permissions pulls your org's permission model. Permission sets, their object and field grants, and permission set group composition are pulled from the connected org so WITH USER_MODE and System.runAs reflect how access is actually granted. Most orgs carry no object CRUD on profiles, so a test that builds a user, assigns a permission set or group, and then queries in user mode depends entirely on this. Permission sets defined in your own project metadata still take precedence.
  • nimbus doctor reports permission sets your tests reference but nimbus can't resolve. A permission set group whose contents are unknown leaves access unrestricted rather than denied — safer than failing a test that passes in the org, but it means a user-mode assertion can pass for the wrong reason. That is now named explicitly, with the sync or retrieve command that closes it.
Fixed
  • Cross-object formulas that traverse two or more relationships now evaluate. A formula like CASESAFEID(Parent__r.GP__r.Id) came back blank even with the whole chain populated, while a single hop worked — so an (Id) cast on the result threw System.StringException: Invalid id:, and code that merely read such a field silently took the wrong branch. Relationship paths of any depth now resolve to the traversed value. (#56)
  • A Text formula whose result is blank now reads as null, not "0". A formula assembled from concatenated IF(...) branches that all evaluated blank produced the string "0" — non-null, non-blank garbage that slipped past != null and String.isBlank() guards and then failed far from its source. The + operator is concatenation on Text and addition on Number, and with two blank operands there is no value to tell them apart; operand types are now resolved from the formula itself. Number formulas keep the platform's blank-as-zero behaviour. (#59)
  • Test.loadData generates fresh record Ids instead of inserting the CSV's. On the platform the ID column is only a correlation key used to wire relationship columns across resources; every load receives new Ids. Nimbus inserted the literal value, so two resources for the same object sharing a row Id collided on the primary key — a pattern that runs daily in real orgs. Relationship columns still resolve across resources through the correlation. A deterministic conflict of this kind is also no longer retried as, or blamed on, a parallel-test conflict. (#57)
  • WITH USER_MODE enforces object permissions. A System.runAs user on a Standard User profile could read any object in user mode, including custom objects the profile grants nothing on, so user-mode assertions passed regardless of access. The profile's real default footprint now applies: CRUD on core standard objects, and custom objects only where something explicitly grants them. In the other direction, access granted through a permission set group is honoured rather than denied. (#58)
  • Flow Get Records on a Custom Metadata Type no longer runs once per record. A constant-filter custom metadata lookup inside a record-triggered Flow consumed one SOQL query per record during bulk DML and could exhaust the query limit; it now runs once per batch like every other constant lookup. (#50)
July 17, 2026
1.5.0
Added
  • nimbus validate adds 15 more semantic checks — 57 of 57 error classes in its conformance corpus, up from 40, each verified against a real check-only deploy. New this release: interface conformance (a class must implement every method of an interface it declares, and implements must name an interface, not a class), impossible casts and always-false instanceof, incompatible comparisons, </> on Booleans, ! on non-Booleans and ++/-- on non-numerics, returning a value from a void method, @future methods that aren't static and void, more than one @InvocableMethod per class, inner classes that nest further or hold static members, and global members in non-global classes.
  • The conversion rules validate enforces are pinned to real deploy behaviour, including the surprising ones. Collections are conversion-covariant (List<Long> = new List<Integer>() deploys), numeric literals type contextually, narrowing casts and String/Id comparisons are allowed, and code after an infinite loop is reachable — validate accepts exactly what the platform accepts and rejects exactly what it rejects, holding a zero-false-reject record across thousands of deployable open-source classes.
July 17, 2026
1.4.1
Added
  • nimbus validate adds 19 semantic checks — every error class in its conformance corpus is now caught, 40 of 40, each verified against a real check-only deploy. New in this release: duplicate methods and variables, extending non-virtual classes, missing override keywords, unimplemented abstract methods, unreachable code, break outside a loop, ternary type mismatches, non-Boolean conditions, catching or throwing non-exceptions, DML on non-SObject types, iterating non-collections, @future parameter constraints, unknown enum values, unknown fields on your own classes, instance members referenced from static context, assigning void calls, and constructor arity. The corpus grows with every release and every rule keeps validate's record of zero false rejects across thousands of deployable open-source classes.
July 16, 2026
1.4.0
Added
  • nimbus validate now performs project-wide semantic analysis. Beyond the existing structural checks (unimplemented interfaces, non-writeable fields), validate resolves every type reference, variable, method call, and field access across the whole project and flags the same errors a Salesforce deploy would reject — undefined variables, unknown types, wrong argument counts, missing return statements, duplicate field assignments, type mismatches, and more. Findings are surfaced as deploy blockers by default (matching a check-only deploy), with --sema=warn to downgrade to warnings and --sema=off to disable. The semantic engine is validated against a corpus of 21 probe classes covering all known error classes from real org deploys, with zero false rejects.
  • nimbus validate now detects unknown custom fields. A field reference like obj.Custom__c that doesn't match any field in the project's schema files or standard SObject definitions is flagged — so typos and stale schema references surface at validate time rather than at deploy. The check covers both explicit new SObject(Field__c = ...) constructors and dot-expression field access.
Fixed
  • VSCode extension now bundles its dependencies. The extension no longer fails to activate when installed without a node_modules directory — all required modules are bundled into the VSIX, so installation from the marketplace or a manual .vsix file works out of the box.
IntelliJ
  • IntelliJ plugin 1.0.20. Hard dependency on LSP4IJ (the language-server bridge), gutter run icons for quick test execution, and unlimited until-build for broader IDE compatibility.
July 10, 2026
1.3.4
Fixed
  • Aggregate queries that filter on a formula field and group by a relationship path no longer throw a spurious "No such column". A query that both filtered on a formula field in WHERE and grouped by a relationship-path field in GROUP BY — e.g. SELECT COUNT(Id), Answer__r.Question__c FROM Action__c WHERE isOpen__c = TRUE GROUP BY Answer__r.Question__c — failed at runtime with No such column 'isOpen__c' on entity 'Action__c', even though the field resolves fine in every other query shape. A field whose name starts with a lowercase letter (common for formula flags like isOpen__c) was resolved case-sensitively once the relationship GROUP BY introduced a join, so it missed the actual column; either ingredient alone worked. Such a field now resolves identically whether or not the query also groups by a relationship path, matching the platform. (#55)
July 10, 2026
1.3.3
Fixed
  • Parallel test runs no longer crash on classes that share a static counter. On large projects, running the suite in parallel could abort the whole run with a fatal concurrent map writes error when two tests incremented a static field (a ++/--) on the same managed-package/stub class at once — because that class's static state wasn't isolated per test. Each test now gets its own copy of every class's statics, so parallel execution is stable regardless of how tests touch shared static fields. A timed-out test also now has its transaction cancelled and rolled back instead of being left running (which previously left a database connection idle in transaction and could deadlock the next run), and the timeout message no longer assumes an infinite loop — it points at parallel contention and suggests re-running with -p 1 to confirm. (#9)
Added
  • nimbus validate now catches unimplemented interfaces. A concrete class that declares implements but doesn't define every method of the interface is flagged as a deploy blocker (validate exits non-zero), matching the compile error Salesforce raises — so you catch it before the deploy does, consistent with the language server. Interfaces and base classes are resolved from the local project; the check skips conservatively when a type can't be resolved (a managed-package or cross-file interface) so it never blocks a valid deploy. (#54)
  • nimbus validate now flags setting an AutoNumber field. Assigning an AutoNumber field in an SObject constructor (e.g. new Log__c(Name = 'x')) is reported as a deploy blocker — AutoNumber fields aren't writeable, and Salesforce rejects this at compile. This complements the runtime rejection added in 1.3.2, so the mistake surfaces at validate time too. (#52)
July 9, 2026
1.3.2
Fixed
  • Record-triggered flows now bulkify their Get Records elements. A flow that runs on record create/update executed its Get Records queries once per record, so a bulk DML of 200 records could fire hundreds of SOQL queries and hit Too many SOQL queries — while the same operation runs comfortably on Salesforce, which batches flow interviews. Get Records elements with an equality filter on a triggering-record field now run once for the whole batch (via an IN clause) and distribute the matching rows to each record, so query count scales with the number of elements, not the number of records — matching the platform. (#50)
  • AutoNumber fields now populate on insert, formatted per their display format. An AutoNumber field (including an AutoNumber Name field) stayed null after insert instead of receiving a generated sequence value. Inserts now assign the next value rendered against the field's display format — Log-{000000} yields Log-000001, and date tokens ({YYYY}/{YY}/{MM}/{DD}) are substituted anywhere in the pattern. Setting an AutoNumber field in Apex is now rejected with Field is not writeable, matching the platform (where it is a compile error). (#52)
  • Type.forName() returns null for a type that doesn't exist, instead of a phantom Type. Type.forName('Nonexistent__c') returned a non-null Type whose newInstance() produced a phantom object, silently breaking the standard null-guard pattern for dynamic factories (if (t != null) took the wrong branch). Unknown types now return null as on the platform, while real standard and project custom objects still resolve. (#51)
  • DescribeSObjectResult.childRelationships is now a non-null list. It returned null on every object, so any code iterating an object's child relationships threw a NullPointerException; on the platform it is always a non-null (possibly empty) list. Both access paths — getGlobalDescribe().get(name).getDescribe() and SObjectType.getDescribe() — now return the populated list, each entry exposing getChildSObject(), getField(), and getRelationshipName(). (#53)
  • Bulk inserts of records with a Text formula field that evaluates to a number no longer fail. Following the 1.1.11 fix for single-row inserts, the same case in a bulk insert — the common @testSetup shape — still failed with unable to encode 0 into text. The text coercion now covers every DML path (bulk insert, update, and non-transactional insert/update) and handles a whole affected column across all rows of a large batch in one pass. (#39)
  • nimbus doctor no longer flags valid dynamic seed keys. Config keys like nimbus.seed.queue.<DeveloperName> and nimbus.seed.group.<Name> — documented and working at test time — were reported as unknown config keys by the doctor's syntax check. The doctor now recognises all documented dynamic seed-key prefixes. (#49)
July 8, 2026
1.3.1
Added
  • --local-shards auto now sizes itself by suite length, not just core count. Splitting a run into isolated local shards only pays off once a suite is long enough to amortize the extra process and database startup — on a short suite it can be slower than a single process. auto previously looked only at your CPU count and could pick multiple shards for a suite too short to benefit; it now also considers the wall-clock time of your last full run and stays single-process when sharding wouldn't help. The recorded duration only ever comes from a genuine full run, never a --shard slice or an --impacted subset.
  • --local-shards runs now report where time went. The merged summary from a sharded run now includes the same SOQL / DML / code / setup time breakdown a single-process run shows — summed across shards, in both the human-readable summary and --json — instead of dropping it.
Fixed
  • Validation-rule formulas now follow Salesforce's three-valued null logic. A comparison against a blank field — for example Date__c == null or Field__c != null — was evaluated as a plain true/false, so a rule written that way fired on records the platform accepts. On Salesforce such a comparison is undefined, and a validation rule fires only on a strict true, which means these rules are effectively inert for the blank case. nimbus now matches: == null/!= null against a genuinely blank value no longer triggers the rule, while ISBLANK() remains the explicit way to test for blankness and still fires as before. (#44)
  • Querying a profile by Id now returns the same profile as querying it by name. When a profile that isn't one of the standard seeded profiles was looked up by name, then re-queried by its Id or reached through a User's Profile.Name relationship, each path could return a different profile for the same Id — so profile-gated logic silently evaluated false. A profile resolved on demand is now a real, consistent record: by-name, by-Id, relationship traversal, and an unfiltered SELECT ... FROM Profile all agree on its Id and name. (#46)
  • equals() now works on Boolean values. Calling .equals(...) on a Boolean failed with "Unknown method equals on Boolean", even though every Apex type supports it and String and Integer already worked. This surfaced most often in generic code comparing Object-typed field values, such as dirty-checking a Checkbox field. (#45)
  • SOQL binds that call a method on a string literal now translate correctly. Completing the bind-expression fix from 1.3.0: a bind whose receiver is a string literal — for example WHERE Name IN :'a,b'.split(',') or = :'a,b'.substringBefore(',') — still leaked its raw text into the generated SQL and failed with a Postgres syntax error. Such binds are now evaluated and bound as values like any other. (#43)
  • String filters in relationship subqueries are now case-insensitive, matching flat queries and the platform. A WHERE filter on a text or picklist field was case-insensitive in a flat query but case-sensitive inside a parent-to-child subquery — so (SELECT Id FROM Contacts WHERE LastName = 'SMITH') returned no rows when the stored value was Smith, while the same flat filter matched. Both positions are now case-insensitive, as on Salesforce. Relatedly, DescribeFieldResult.getType().name() now returns the uppercase enum name (STRING, PICKLIST) instead of mixed case, matching the platform and the bare Schema.DisplayType enum. (#48)
  • JSON.serialize now preserves the exact casing of member names. Serializing an Apex object emitted its field and property names in all-lowercase (fileName became filename), producing JSON with the wrong keys — a consumer reading result.fileName found nothing, and nothing threw. Member names now serialize with their declared casing verbatim, in the platform's member order. (#47)
  • Fixed non-deterministic ordering that could make a few tests flaky between runs. A handful of describe- and schema-backed maps (global describe, a field collection, record-type infos) didn't preserve insertion order, so iterating them fell back to Go's deliberately randomized map order — a test asserting on the order of values()/keySet() could pass or fail from one run to the next. Those maps now keep a stable order, and a related dynamic WHERE Id = :x lookup on RecordType now filters correctly instead of matching everything.
  • Cold-starting --local-shards with several shards is more reliable. Launching many shards at once made their embedded databases initialize simultaneously and contend for disk, which could push a shard past the startup deadline and fail the run. Shard startup is now staggered and the readiness timeout is more forgiving, so large shard counts start cleanly on a busy machine.
Performance
  • Lower memory churn across large test suites. A round of interpreter and SOQL profiling removed several hot allocation sources that fire on nearly every method call, field access, and query: return-value tracking is no longer done for every statement unless something consumes it, case-insensitive name resolution reuses cached lowercased identifiers at many more call sites, class- and method-name bookkeeping shares immutable string wrappers, and parsed SOQL bind expressions are cached instead of re-parsed on every query execution. Several fixed regular expressions in the SOQL translator are also compiled once instead of on every query. Results are unchanged; large runs allocate substantially less and spend less time in garbage collection.
July 4, 2026
1.3.0
Added
  • Sharded runs now split by execution time, not test count. nimbus test --shard i/N assigns test classes to balance each shard's wall-clock time, using per-test durations recorded from previous full runs, instead of dividing purely by class count. A few slow classes no longer pile onto one shard while the others finish early — on a large suite we track, the imbalance between two shards dropped from 2.2x to 1.26x. With no recorded timings yet (a new project, or a cleared cache) it falls back to the previous count-based split, and independent CI processes always compute the same assignment without coordinating.
  • Experimental: run isolated shards on a single machine with --local-shards N. nimbus test --local-shards N|auto spawns N shards as concurrent processes against the same project checkout, each with its own isolated local database, and merges their results into one report (human-readable, --json, or JUnit). Today this is about isolation, not speed — a single-process -p N run is still faster, and --coverage is not yet supported with --local-shards. The correctness and merge machinery is complete; a future release turns it into a speed win.
Fixed
  • SOQL bind expressions that call a method with arguments now translate correctly. A bind like WHERE Name IN :someString.split(',') or :MyClass.ids(x) left the argument parentheses stranded in the generated SQL, so the query failed with a Postgres syntax error near (; only no-argument calls such as :UserInfo.getUserId() happened to work. Bind expressions with arbitrary arguments now resolve properly. (#43)
  • Validation rules now run after before-insert triggers, matching the platform's order of execution. On insert, custom validation rules were checked before before-insert triggers had a chance to run, so a trigger that defaults a required field (for example setting a blank Name) could not satisfy a rule that required it — the record was rejected even though a real Salesforce save would have passed. Insert now runs before-triggers first, then validation rules, consistent with how update already behaved. (#42)
July 3, 2026
1.2.0
Added
  • Run only the tests affected by your changes with --impacted. nimbus test --impacted builds a dependency graph from your local edits and runs just the test classes that exercise the changed code, skipping everything else. The class-dependency map is persisted between runs and wired into watch mode, so saving a file re-runs only its impacted tests automatically. Pass --no-impact to force a full run.
  • The MCP server can now run anonymous Apex, SOQL, and schema lookups against the local runtime. New execute_anonymous, query, and describe_schema tools let an AI agent or editor execute a snippet, run a SOQL query, and inspect an object's fields without an org connection. Test failures returned over MCP and JSON now carry structured diagnostics — the failing assertion, the exception, and the execution path that reached it — instead of a bare message.
  • Time-travel debugging: record a test run, then replay it in the debugger. The debug adapter can capture an execution trace and step through it after the fact — forward and backward over the recorded run — so you can inspect how state reached a failure without repeatedly re-running to the same breakpoint.
Fixed
  • Identifiers with non-ASCII characters now match case-insensitively, as Apex does. A field, variable, or type name containing non-ASCII letters (for example accented or non-Latin characters) could fail to resolve when referenced with different casing, because the case-folding fast path left such names unchanged. Case-insensitive resolution now covers the full Unicode range, matching the platform.
Performance
  • Large test suites run substantially faster. A round of database and interpreter work removed the main bottlenecks on big projects — the largest suites we track finish in roughly half the time. The improvements below apply automatically, with no configuration.
  • Hierarchy and list Custom Settings resolve from cache instead of the database. Repeated getInstance(), getOrgDefaults(), and getValues() lookups — which fire constantly in framework-heavy code — are now served from an in-memory snapshot taken once per run, including the very common case where the setting has no matching row for a user or profile. On settings-heavy suites this removes the bulk of query traffic. Tests that modify a setting still see their own writes.
  • Parallel runs no longer stall behind on-the-fly schema changes. When a record used a field the local schema hadn't materialized yet, nimbus added the column inside the test's own transaction — taking a lock that blocked every other worker touching that table until the test finished. Those columns are now provisioned up front, so parallel workers stop serializing on the first insert of an affected object.
  • Roll-up summary recalculation issues far fewer database queries. When a save recalculates several roll-up summary fields on the same parent record, nimbus now computes them in one combined query per parent instead of one query per field, cutting round-trips on trigger-heavy save paths. Results are unchanged.
  • Lower memory churn in the interpreter. Case-insensitive name resolution — which runs on nearly every field access, method call, and variable lookup — now reuses cached lowercased identifiers instead of recomputing them, cutting total allocations on a large run by roughly a quarter and easing garbage-collection pressure.
June 30, 2026
1.1.12
Fixed
  • Stopping the daemon no longer breaks a test run that's already in progress. If a separate nimbus test/nimbus exec was using the database, stopping the daemon — by command or signal — used to clear the tables and shut down Postgres underneath it, failing every remaining test with "Failed to begin transaction". The daemon now detects an in-flight run and leaves the database up for it, completing its own shutdown without disturbing the run. (#10)
  • Abandoned local Postgres instances are now cleaned up automatically. A daemon killed with SIGKILL, a crashed run, or a daemon stopped mid-run could leave a background Postgres process with no owner — invisible to nimbus daemon list and, if you never returned to that project, lingering until reboot. nimbus now reaps these on its next run in any project: an instance with no live daemon and no active connections is stopped and its data removed. Instances that are in use or daemon-managed are never touched. (#10)
  • An unclean stop no longer hangs the next start. A crash mid-recovery could leave the data directory in a state that stalled the next Postgres startup; that case is now detected and the cluster rebuilt deterministically on start. (#10)
June 29, 2026
1.1.11
Fixed
  • Calling a method on a static Map field whose name matches a system class now works. A field named CACHE collides with the Platform Cache class, and CACHE.put('k', 'v') failed with "Unknown method put on Type CACHE" — while .get() quietly returned null. The bare field name resolved to the system class instead of the field. A static field now shadows a same-named system class, just as instance fields and local variables already do, so the idiomatic static-Map cache populated via .put() (often in a static {} block) holds its entries. (#38)
  • Inserting a record with a Text formula field that evaluates to a number no longer fails. A Text-typed formula field is stored as text, but when its formula reduced to a number (for example arithmetic with blanks treated as zero), the insert aborted with "unable to encode 0 into text". A Text formula now yields text — matching the platform, where a Text formula field always returns a string — so the value is stored as "0" and records whose @testSetup touched such a field save cleanly. (#39)
June 28, 2026
1.1.10
Fixed
  • nimbus sync --include-setup-data no longer aborts the whole pull when one setup object can't be queried. An org without Experience Cloud has no queryable Network, so SELECT Name, Status FROM Network errored and that single failure stopped the entire pull — no Group, queue, or UserRole entries were written. Each object type is now pulled independently: a type the org can't query is skipped with a warning and the rest still land. The pull only fails outright when no setup object can be queried at all (a bad org alias or no connection). (#33)
  • The setup-data seeders now appear in nimbus config properties and nimbus config show. nimbus.seed.group.*, seed.queue.*, seed.role.*, seed.network.*, and seed.user.* were honored at runtime but absent from the config listings, so there was no way to confirm a hand-authored key was recognized. They are now documented alongside the other seed keys. (#33)
June 26, 2026
1.1.9
Added
  • Seed Networks (Experience Cloud sites) for local runs. New nimbus.seed.network.<Name>=<Status> key. Code that gates on a community — [SELECT Id FROM Network WHERE Name = 'Member Hub'] — resolves the seeded row instead of failing on an empty single-row query. Status defaults to Live. Querying Network with no rows now returns 0 rather than erroring on a missing object. (#33)
  • Seed named Users by Name. New nimbus.seed.user.<Name>=<Username> key. Factories that look up integration or system users by name — [SELECT Id FROM User WHERE Name = 'System Salesforce'] — now resolve. The Name is split into FirstName/LastName so the generated Name field reconstructs exactly; the Username is optional. (#33)
  • nimbus sync --include-setup-data now also pulls Networks. Alongside Groups, queue routing, and UserRoles, Network rows are read from the org and written as nimbus.seed.network.* entries. Named Users are left as an explicit seed list rather than bulk-synced, since a large org has thousands. (#33)
Fixed
  • Record-triggered flows no longer blank Date, DateTime, and Time fields. A flow that updated its own triggering record (Update Triggering Record, or any update of $Record) silently nulled every temporal field on the saved record, on both insert and update — String and reference fields survived, which made it look field-type-specific. Those fields are now preserved when $Record is built for the flow. (#35, #36)
  • Unfiltered SELECT COUNT() FROM Profile now counts the backing rows. The aggregate returned 0 while the same query with WHERE Name = '…' returned 1; the unfiltered count path didn't enumerate the Profile pseudo-table's rows. Filtered and unfiltered counts are consistent now. (#37)
  • More standard objects are provisioned for queries. ProcessInstanceWorkitem, ProcessInstanceStep, UserLogin, RecentlyViewed, and NavigationLinkSet no longer fail with "relation does not exist", and ContentFolderItem.IsFolder resolves instead of "No such column". (#32)
June 26, 2026
1.1.8
Added
  • Seed UserRoles and Queues for local runs. Two new keys join the existing group/site/label seeders: nimbus.seed.role.<DeveloperName>=<Name> and nimbus.seed.queue.<DeveloperName>=<SObject1>,<SObject2>. A queue seed inserts the queue plus its routing for each listed SObject, so case and lead assignment automation resolves owner candidates; role seeds make WHERE DeveloperName = … lookups against UserRole resolve. These are setup objects that test factories assume exist but repos rarely check into metadata. (#33)
  • nimbus sync --include-setup-data pulls setup objects from the connected org. Groups, queue routing, and UserRoles are read from the org and written back as nimbus.seed.{group,role,queue}.* entries in nimbus.properties, under a delimited block. Hand-authored seed lines outside the block are preserved across re-runs, so you can reproduce org-resident setup data locally without authoring every entry by hand. (#33)
Fixed
  • Configured governor limits are now honored by the Limits API and by nimbus exec enforcement. nimbus.governor.soql-queries and nimbus.governor.dml-statements were accepted and shown by nimbus config show, but Limits.getLimitQueries() / Limits.getLimitDmlStatements() always returned the standard 100 / 150 regardless of the override, and nimbus exec kept enforcing at the defaults. Both now reflect the configured values, so projects on orgs with Salesforce-raised limits can match their runtime ceiling locally. (Enforcement under nimbus test already honored the override.) (#34)
  • SOQL query limit now varies by execution context. Asynchronous contexts (Queueable, @future, Batch) get the platform's 200-query ceiling instead of the synchronous 100, in both enforcement and the Limits class. Previously async code was capped at 100, so async tests that legitimately exceeded 100 queries failed locally despite passing in the org. An explicit nimbus.governor.soql-queries override stays authoritative across both contexts. (#34)
  • Group seeds are now resolvable by DeveloperName, not just Name. Factories that looked up a seeded group by its DeveloperName previously came up empty because only Name was written. Both columns now carry the seed key.
  • Warm-daemon runs reseed after edits to seed config. The daemon's seed cache fingerprint omitted group, label, role, and queue seeds, so editing those entries in nimbus.properties silently had no effect until a cold start. Changes are now picked up on the next run.
June 25, 2026
1.1.7
Performance
  • --fetch-missing is no longer ~20x slower. The retriever was issuing one sf sobject describe per missing object — each its own process spawn + org round-trip — serially. It now runs them across up to 8 concurrent goroutines. For N missing objects the wall time drops from N×t to roughly ceil(N/8)×t. (#8)
Fixed
  • Pool stalls on large projects (~1,800+ tables). The connection pool formula (workers + 5, cap 100) was too tight when per-worker-schema cloning and test execution overlapped — setup transactions competed with test transactions and the pool ran dry. Changed to workers × 2 + 10, cap 200, with a DropWorkerSchema retry loop for transient deadlocks matching the existing CreateWorkerSchema retry. (#9)
  • Flow sub-executor was missing ctx and metadataProvider propagation. executeSubflow created its sub-executor without forwarding the parent executor's context and metadata provider, so a subflow that queried custom metadata or used ctx-dependent features (timeouts, cancellation) could fail or behave differently from the calling flow.
  • Standard objects without an embedded describe were missing their DB tables. The schema provisioner creates tables for standard SObjects from bundled JSON files, but some objects declare no embedded describe at all (only custom-field metadata). Those objects now get their tables created anyway from the standard-schema scan, so SELECT from them works instead of reporting a missing-relation error.
June 24, 2026
1.1.6
Fixed
  • Cross-object formula references on the parent evaluated to blank. A formula field on a child object that referenced a formula field on the parent — Parent__r.ParentCode__c where ParentCode__c is itself a formula — returned null because the resolution logic stopped at the parent's stored columns and never resolved its computed fields. The cross-object resolver now detects when the requested field is absent from the parent's DB row and recursively resolves it via resolveFormulaFields on the parent SObject. The resolved value is cached so subsequent lookups avoid re-resolution.
June 23, 2026
1.1.5
Fixed
  • DML after a prior transaction rollback no longer crashes Nimbus with a SIGSEGV. On the platform, a rolled-back transaction returns a catchable exception on any subsequent DML attempt. Under Nimbus, a record-triggered Flow that performed DML after the underlying Postgres transaction was already closed (via a prior deadlock recovery or explicit rollback) dereferenced a nil transaction handle, crashing the process with no test-framework recovery path. DML methods now check for a valid transaction before any operation, so a rollback mid-test produces a catchable error instead of a segfault.
June 23, 2026
1.1.4
Fixed
  • Record-triggered flows now count toward DML governor limits. Every DML operation on the real platform counts against the 150-DML transaction limit. Nimbus's flow executor was calling the DML engine through a path that bypassed the DML counter, so flow-heavy tests could exceed limits without detection and the TUI write counter under-reported actual DML volume. Flow-initiated inserts, updates, and deletes now count correctly.
  • A before-save flow with a Loop + Create/Update Records no longer re-dispatches the trigger chain indefinitely. On the platform, a before-save flow that creates records on the same object type converges — each DML fire is bounded. Under Nimbus, a Loop element iterating a collection with a Create/Update Records inside could re-enter the before-save trigger chain thousands of times, with each iteration adding ~5 SOQL, culminating in System.LimitException: Too many SOQL queries or a 25 s timeout. The trigger re-entry is now capped at one dispatch per flow per (object, trigger event) pair.
June 22, 2026
1.1.3
Fixed
  • Building a Map<Id, SObject> from records without Ids throws, matching the platform. new Map<Id, SObject>(records) where a record has no Id — an unsaved list, or new Map<Id, X>(Trigger.new) in a before-insert — now throws System.ListException: Row with null Id at index: N (the first offending row) instead of silently producing an empty or partial map. Maps built from queried records, and the copy constructor, are unaffected.
June 22, 2026
1.1.2
Added
  • Schema.describeTabs() returns your project's tabs. The returned tab set is now populated from the *.tab-meta.xml (CustomTab) metadata in your project — source-driven, like the rest of the schema. An object tab resolves its getSobjectName() and label from the object; web, Visualforce, and Lightning tabs carry their own label. Code that resolves a tab or icon for an SObject now finds it instead of getting an empty list.
June 22, 2026
1.1.1
Fixed
  • A custom exception's own message field is distinct from getMessage(). When a class that extends Exception declares its own public String message field, reading the field now returns the assigned value while getMessage() returns the exception's built-in message (defaulting to Script-thrown exception when none was set) — matching the platform, where the two are separate slots. Previously both resolved to the first constructor argument.
  • Type.newInstance() on an inner class of an @IsTest class upcasts correctly. Instantiating a test-local subclass reflectively — the common pattern of registering SomeType.class in a Map<…, Type> and later doing (Base) registry.get(key).newInstance() with a mock subtype defined inside the test class — now resolves the inheritance instead of throwing Invalid conversion from runtime type.
  • Schema.describeTabs() is implemented. It returns a non-empty List<Schema.DescribeTabSetResult> (matching the platform guarantee that the call is never empty) instead of aborting execution with a malformed error. Tab/app metadata isn't synced, so the set carries no per-object tabs.
  • Standard OpportunityStage reference data is seeded. The standard sales-process stages (Prospecting … Closed Won / Closed Lost, with IsClosed / IsWon / DefaultProbability / ForecastCategory) are now present, so common @testSetup idioms that pick a valid stage from org config no longer return zero rows. Orgs with a customised stage picklist can override via nimbus.seed.record.
June 21, 2026
1.1.0
Added
  • Code generation in the editor. Yellow-bulb refactors on a class declaration: generate a constructor from the class's fields, generate getters and setters, implement the methods of every interface in the implements clause (including Queueable, Schedulable, Comparable, and Database.Batchable), and override a superclass's virtual/abstract methods. Served by the language server, so they appear in both the JetBrains plugin and the VS Code extension.
  • Relationship-aware SOQL and SOSL completion. Completion inside a query now follows relationships — typing Owner. or Account.Parent. resolves the chain and offers the related object's fields — and child-relationship subqueries ((SELECT … FROM Contacts)) complete the child object's columns. SOSL RETURNING Account(…) completes that object's fields too. Standard objects work with no sync. Relationship fields are also validated, so a typo like Owner.Namee is flagged once the chain resolves to a synced object.
  • Apex and SOQL live templates. A bundled set of about twenty templates — sysdSystem.debug(), tm → an @IsTest method, soqlf → a bulk-safe SOQL for-loop, bulkt → a 200-record test-data block, and more — ship in both the JetBrains plugin (native live templates) and the VS Code extension (snippets).
  • Coverage delta in the gutter. Snapshot a coverage run as a baseline ("Set Coverage Baseline"), then "Toggle Coverage Delta" to overlay what changed since — green for newly-covered lines, orange for regressions — in both editors.
  • Three new CLI commands. nimbus new <kind> <Name> scaffolds a class, test, trigger, trigger handler, batch, queueable, or schedulable (with its -meta.xml) into your default package. nimbus fixture <SObject>… generates a TestDataFactory with required fields filled in from the schema. nimbus coverage diff <base.json> <pr.json> reports the coverage delta — overall, per file, and the exact newly-uncovered lines — with --fail-on-drop for CI gates.
  • Parallel mutation testing. nimbus mutate takes a -p/--parallel flag and runs mutants across multiple workers, the way nimbus test already parallelizes test runs (Pro).
  • More signals for AI agents over MCP. The MCP server adds get_governor_usage (per-test SOQL/DML/CPU/heap from the last run) and run_mutation_tests (mutation score plus surviving mutants), so an agent can spot un-bulkified code and weak assertions without leaving its loop.
Fixed
  • Summer ’26 (API v67) semantics. Classes declared on API version 67.0 or later now enforce two Summer ’26 changes: an unannotated class runs as with sharing instead of system mode, and a SOQL query using WITH SECURITY_ENFORCED throws a System.QueryException (the clause was removed). Classes on earlier API versions are unchanged.
  • Child-relationship subqueries no longer report a false error. A parent query with a subquery — SELECT Id, (SELECT Id FROM Contacts) FROM Account — previously flagged the relationship name (Contacts) as an unknown SObject. It's now recognized as a child relationship.
June 20, 2026
1.0.9
Added
  • Code coverage in the editor, with a per-class Coverage button. A new Coverage view shows the line coverage for the run you just did — an overall score plus a per-file breakdown sorted most- to least-covered, labeled with the test it came from. A 🪄 Coverage action now sits at the top of every test class (alongside each test method), so you can collect coverage for a whole class in one click. The class-level button is served by the language server, so it appears in both the JetBrains plugin and the VS Code extension.
  • A tabbed execution-trace viewer. Traces open with Execution Log, Call Tree, Timeline (a flame graph), and Flow (a call-flow graph) tabs, plus an inspector for the selected span — its attributes, events, and the variable snapshots captured inside it.
  • Detect the local runtime from Apex via Organization.InstanceName. Querying InstanceName on Organization returns Nimbus by default, so code can branch on whether it's running locally ([SELECT InstanceName FROM Organization].InstanceName == 'Nimbus'). Real Salesforce instance names are always uppercase, so there's no collision; override it per project with nimbus.org.instance-name.
Fixed
  • null instanceof T follows the platform's API-version rule. It returns true for code on API 31.0 or earlier and false from 32.0 (Spring ’15), governed by the class that contains the expression — so frameworks that rely on the legacy behavior (for example apex-lambda’s comparers, where compare(null, null) must report equal) behave correctly.
  • Typed Decimal collections coerce integer literals. new Set<Decimal>{60, 150} now stores Decimal values rather than Integer, so membership checks against Decimal field values match instead of silently missing.
  • Collection-type instanceof and casts match the platform. A List<SObject> satisfies instanceof against any concrete SObject subtype, and an explicitly cast collection argument — Collection.of((List<Account>) Trigger.new) — resolves List/SetIterable<T> overloads instead of failing with “no matching overload.”
  • SOQL binds that are an inline cast translate correctly. WHERE Id IN :(Set<Id>) raw previously produced a database syntax error; the cast operand is now captured correctly.
  • System.-qualified valueOf(…) works. System.Id.valueOf(…), System.String.valueOf(…), and the other qualified forms previously returned null; they now reach the same handlers as the unqualified forms.
  • JSON.deserialize rebuilds compound address fields. Deserializing a record with BillingAddress/ShippingAddress now materializes a read-only System.Address, so record.BillingAddress.getCity() works instead of failing on a raw map.
  • Coverage is collected on every run, not just the first. When the background daemon was warm, only the first coverage run in a session reported line data — every run after it came back empty until the daemon restarted. Coverage now loads the program sources it needs on each run, so repeated coverage runs against different classes always report correctly.
  • Editor-driven runs honor managed-package stub namespaces. Tests launched from the editor (rather than the CLI) now apply the nimbus.stubs.namespaces setting from nimbus.properties, so references to auto-stubbed managed packages resolve the same way they do on the command line.
June 19, 2026
1.0.8
Added
  • A JetBrains / IntelliJ plugin. Nimbus now runs inside IntelliJ IDEA and the other JetBrains IDEs, alongside the VS Code extension. Run and debug Apex tests with breakpoints, step-through, and variable inspection; see results in the IDE's test runner; collect coverage; and get Apex completion, hover, navigation, diagnostics, code lenses, and inlay hints from the language server. A Nimbus tool window adds test history, a schema explorer, and governor-limit usage, and a watch mode re-runs tests as you save. Requires the nimbus CLI on your PATH.
June 18, 2026
1.0.7
Performance
  • Sync reads project metadata concurrently. Building on v1.0.6, the three independent passes a sync makes over your metadata — bundled standard schemas, custom fields on standard objects, and custom objects — now run at the same time instead of one after another. On large orgs the heavy passes read separate sets of files, so overlapping their disk I/O shortens the scan further, most noticeably on slower or contended filesystems. The result is unchanged; only the time to produce it.
June 18, 2026
1.0.6
Added
  • Object-scoped sync: nimbus sync -s Account,Contact,My_Object__c. Re-sync just the objects you name instead of the whole org. When you're iterating on a handful of objects, there's no longer any reason to wait on a full-org sync — other tables are left untouched. (--sobjects is the long form.)
  • Sync is now incremental by default. A re-sync creates missing tables and adds missing columns without dropping anything, so running nimbus sync when nothing has changed is now near-instant rather than a full rebuild. Pass --rebuild to force the old drop-and-recreate behavior — the only mode that applies column type changes (and it wipes table data).
Performance
  • The first sync on a large org is dramatically faster. On orgs with hundreds of objects and heavy custom-field counts, the initial sync could take many minutes. Two changes cut that down: object and field metadata is now parsed in parallel across CPU cores rather than one file at a time, and table creation runs as a single batched operation instead of committing each statement separately — which is where most of the time went on slower or contended disks. Redundant per-table indexing was also removed. The schema produced is identical; only the time to build it changed.
June 18, 2026
1.0.5
Fixed
  • Compound address fields now populate across parent relationships. v1.0.4 reconstructed a System.Address from the components of a directly queried record; this extends it to relationship-traversed compounds. SELECT Account.BillingAddress FROM Contact followed by record.Account.BillingAddress.getCity() — or the equivalent Parent__r.Address through a custom lookup — now hydrates the parent record with the address components and rebuilds the compound on access, matching the platform.
June 18, 2026
1.0.4
Added
  • String.fromCharArray(List<Integer>) is now supported. Building a string from a list of character codes — for example String.fromCharArray(new List<Integer>{160}) to produce a non-breaking space — previously failed with an "Unknown method" error. It now returns the expected string, so code that strips or inserts specific characters by code point runs as it does on the platform.
Fixed
  • Compound address fields are now populated on queried records. Selecting a compound address field — Address on Lead/Contact, BillingAddress / ShippingAddress on Account, and the equivalents — and then reading it back (record.Address.getCity(), System.debug(record.Address)) returned null. Nimbus now reconstructs the System.Address from the underlying street/city/state/postal/country components for records returned by SOQL. Fresh in-memory records still return null for the compound field, matching the platform (the value only materializes on query).
  • Static methods inherited from a parent class now resolve. Calling an inherited static method without a class qualifier — directly inside a subclass method, or from a static field initializer such as static final String x = baseMethod(); — failed with "Unknown function". Nimbus now walks the superclass chain when resolving an unqualified static call, so classes that extend an abstract base behave as they do on the platform.
  • Regex patterns that escape ordinary characters now compile. Apex (Java) regex treats a backslash before any character as that literal character, so patterns that escape punctuation or non-ASCII characters are valid. Nimbus rejected some of these with an "invalid escape sequence" error because its primary regex engine is stricter than Java's. Such patterns now fall back to a Java-compatible engine and compile and match correctly across Pattern / Matcher and String regex methods.
June 17, 2026
1.0.3
Added
  • Default-value formulas are now evaluated on insert. A field whose default value is a formula — a Time field that defaults to TIMEVALUE(...), a Date that defaults to TODAY() + 1 — now has that value computed and filled in when you insert a record without setting the field, matching the platform. Defaults are applied before before-insert triggers, so triggers and any value you set explicitly still take precedence. Literal defaults (text, number, checkbox) are unchanged.
June 17, 2026
1.0.2
Fixed
  • A field with a formula default value no longer breaks database setup. When a custom field's default value was a formula (e.g. a Time or Date field defaulting to TIMEVALUE(...) or TODAY()), the formula text was written into the table schema as a literal default — which PostgreSQL rejected, and the failed table creation aborted the whole run, so no tests could execute. These defaults are now handled correctly. (v1.0.3 goes further and evaluates them on insert.)
June 16, 2026
1.0.1
Added
  • nimbus mutate now mutates the class under test when you point it at a test class. Running mutation testing against a test class (e.g. nimbus mutate FooServiceTest) previously found zero mutants — test code is never mutated — and dead-ended at a misleading 0.0% score. Nimbus now derives the class under test from the conventional Test prefix/suffix, mutates that, and scopes each mutant run to the named test suite. Behavior is unchanged when a production class is already in the selection.
June 16, 2026
1.0.0
Released
  • Nimbus 1.0. Local Apex execution, SOQL, DML, triggers, and Flows against an embedded PostgreSQL — no scratch org, no Docker, no JVM. The runtime, coverage, debugger, parallel execution, mutation testing, LSP, and VS Code integration are stable and run thousands of tests daily across real Salesforce codebases.
  • Pro is free. Every Pro feature is available at no cost with a Pro license — no card required. Claim one from the portal and run nimbus login to sign in your CLI.
June 15, 2026
0.3.7
Changed
  • XML coverage and test-results output is now included with Pro. Cobertura coverage (--coverage-report *.xml) and JUnit results (--results-xml) — previously a separate tier — are now available on any Pro license, including the CI license. CI already requires Pro, so your pipeline can produce SonarQube/Codecov/GitHub-quality-gate reports with no extra entitlement.
June 15, 2026
0.3.6
Fixed
  • CI license keys are now accepted regardless of how the license is issued. Some valid, active Pro/Team keys were rejected on CI runners with an authentication-policy error — so nimbus test failed even with a correctly configured NIMBUS_LICENSE_KEY secret. License validation now falls back to a key-based check that works for every license type, and trusts the licensing server's current expiry rather than the date embedded in the key at issue time (which can lag a renewal).
  • License errors in CI report the real reason. A key the server actively rejects (expired, revoked, or refused by policy) was previously reported as "please connect to the internet to activate your license", even on an online runner. Definitive rejections are now surfaced with their actual cause, instead of being masked as a network outage or carried by the offline grace period.
June 13, 2026
0.3.5
Fixed
  • Platform events now deliver with faithful transaction semantics. Each delivery generation runs as its own transaction with fresh governor limits — exactly how the platform bounds each subscriber transaction — so a subscriber that re-publishes is bounded by the SOQL/DML caps instead of delivering inline forever (an exception-logger that published on failure could previously loop until it exhausted limits). And a Publish-After-Commit event published in a transaction that then fails is now discarded rather than delivered: a rolled-back transaction publishes nothing, just like the platform.
  • Governor limit configuration is now honored. nimbus.governor.soql-queries and nimbus.governor.dml-statements in nimbus.properties are now applied at runtime (an unset value falls back to the platform default of 100 / 150). Previously only nimbus.governor.mode took effect and the numeric caps were ignored.
  • Record-triggered flows with only an async scheduled path no longer block DML. A flow whose <start> has only an AsyncAfterCommit scheduled path (no synchronous connector) used to fail to parse and fail every DML on the triggering object. It now degrades to a no-op for synchronous execution — the async path wouldn't run during a synchronous test anyway.
  • The standard emailSimple (Send Email) flow action is now a no-op instead of an error. It was being resolved as an Apex invocable and failing with no @InvocableMethod found in class: emailSimple, which failed every DML that triggered the flow. Standard side-effect actions (emailSimple, emailAlert, chatterPost) are now skipped — they have no record state a test asserts on, and the platform doesn't send email during tests.
  • nimbus exec now evaluates validation rules, like nimbus test. An insert that violated an active validation rule used to succeed under exec but fail under test; both paths are now consistent.
  • nimbus doctor detects dotted managed-package references. It now flags usages like dlrs.RollupService.triggerHandler() (previously only the ns__Object / field form was matched), while still ignoring ordinary SObject field access such as acc.Name — so it can nudge you to stub a managed package before tests fail at runtime.
  • State & Country picklists: the country text field is derived from the country code. With State and Country picklists enabled, setting only BillingCountryCode = 'NL' now populates BillingCountry = 'Netherlands' before triggers and validation run, using the field's own picklist mapping — so country validation rules stop misfiring on records that are valid in the org. (Country only for now; state codes aren't globally unique and need country-scoped resolution.)
Added
  • nimbus daemon list and nimbus daemon stop --all. Daemons are per-project, so stray daemons from a previous session (or an MCP client on a different org) used to linger invisibly and daemon stop only stopped the current project's. daemon list now shows every running daemon (pid, org, project) and daemon stop --all stops them all in one command.
June 12, 2026
0.3.4
Fixed
  • USER_MODE queries inside System.runAs now honor the System Administrator profile. When a runAs user had any permission set assigned, AccessLevel.USER_MODE access was derived from those permission sets alone — the user's profile contributed nothing. A System Administrator has implicit View All / Modify All plus full field-level security, and permission sets only add to that, so a query for a field the profile already grants (a standard field like Account.Industry, or a custom field not named in the perm set) wrongly threw System.QueryException: No such column. Sysadmin runAs users now get full access regardless of assignments; Standard User and restricted-profile semantics are unchanged.
  • USER_MODE field-level security now unwraps SOQL function calls in the SELECT list. A select-list entry like toLabel(My_Picklist__c), FORMAT(Amount), or convertCurrency(Amount) was checked for FLS as if the whole expression were a column name, so it failed with No such column 'Tolabel(my_picklist__c)' even when the underlying field was perfectly readable. The check now resolves the inner field before verifying access, matching the platform.
June 12, 2026
0.3.3
Fixed
  • Newly created test classes are discovered without a restart. Long-running Nimbus processes — the background daemon and the MCP server that AI coding agents drive — captured the set of test classes when they started and never refreshed it. A test class you (or an agent) added mid-session, or one a git branch switch brought in, stayed invisible until the process was restarted. The runner now reconciles its in-memory view against the filesystem before each run, the file watcher waits for a freshly written class to finish flushing before parsing it, and a bulk change like a branch switch triggers a full re-scan — so the set of tests that runs is always the set that's on disk.
  • Child relationship names are no longer mangled when a lookup field's API name looks like a namespace. A custom lookup or master-detail field whose API name is a single lowercase segment — e.g. a field named region__c with relationship name Stores — had its parent's child relationship derived as region__Stores__r instead of Stores__r, because the leading region was misread as a managed-package namespace. Every SOQL subquery against the real relationship silently returned no rows, and getChildRelationships() reported the wrong name. A genuine namespace prefix carries two underscore pairs (ns__Field__c), so a single-underscore field name is now treated as un-namespaced, matching the platform.
May 25, 2026
0.3.2
Added
  • The remaining ConnectApi enum gaps are closed. 37 enums across the D/L/O/P/Q/S/W range — SObjectFieldDataType, SavedPaymentMethodStatus / SavedPaymentMethodType / SavedPaymentMethodUsageType, WorkflowProcessStatus, StreamingAppDataConnectorSubTypeEnum, DelimiterTypeEnum, SortOrder, and 30 more. Code that references these types at compile time — declarations, String.valueOf(MyEnum.MEMBER), equality checks, switch over enum values — now resolves locally instead of erroring on an unknown identifier.
  • Corrected member list on ConnectApi.MlRetrieverCitationConfigurationModeEnum. An earlier draft carried the canonicals from a sibling enum (DISABLED / ENABLED); the spec values are CUSTOM / DEFAULT. Customer code that switches on this enum will now see the right shape.
May 17, 2026
0.3.1
Added
  • Slack namespace support. Apex code that builds Slack Block Kit payloads, fires Slack.PostMessageAction, handles slash commands and interactive components, or reads Slack.UserInfo / Slack.ConversationInfo describe shapes now runs end-to-end against Nimbus's local stubs. Slack integrations no longer need a connected app or a live workspace to test.
  • ConnectApi surface broadened across 13 sub-clusters. Managed Content (variants, versions, status enums), Chatter Messages, Files (preview, sharing, delimiter, import status), Errors, Mentions / Communities / Records, Topics, Record Fields, Datacloud, Action Links, Wishlist, Recommendations, Chatter Groups, and Chatter Users. Tests for Connected Apps and Experience Cloud surfaces that touch the ConnectApi REST layer now have substantially more of the namespace available locally.
  • Closed remaining System.* gaps. Type metadata, lightweight introspection, and runtime info round out the core platform surface so it's fully resolvable without leaning on the generic fallback path.
Changed
  • SObject field keys are now lowercased everywhere they're stored. Apex treats field names case-insensitively at the call site (acc.Name and acc.NAME are the same slot), but pre-seeded shells inside Nimbus had been storing some fields in their declared case — so a read after an assignment occasionally missed because the two paths normalized differently. All field storage now goes through a single lowercased key, matching what Apex does.
  • Fresh SObject and EnumValue slots default to their platform-correct zero values. Reference and Datetime fields default to null, Boolean to false, and Integer / Long to 0 — so customer code that guards on obj.SomeField != null, !obj.SomeBool, or obj.Count == 0 short-circuits the way it does in the platform instead of seeing a sentinel value from an uninitialized slot.
May 12, 2026
0.3.0
Added
  • nimbus app — local dev server for Salesforce Multi-Framework React+Vite bundles. Replaces sf ui-bundle dev for offline development. Mounts your ui-bundles/<name> source at http://localhost:<port> with Vite's HMR, and serves the Salesforce API surface your bundle calls into locally against Nimbus's embedded Postgres — no scratch org, no proxy, no internet round-trip. Sub-commands: list, schema, build, test, preview; --all serves every bundle in the project simultaneously.
  • The local API surface is version-agnostic — any /services/data/vXX.Y/ path works. Implemented: GraphQL UIAPI (queries, mutations, introspection, aggregates, cursors), UI API REST (object-info, records CRUD, picklist-values, list-ui, MRU, related-list, layout, record-defaults, record actions, duplicates), Composite API (composite / batch / tree / sobjects), Bulk API 2.0 (ingest + query lifecycles), Search / SOSL, Limits, OIDC userinfo, Connect / Chatter (users/me, feeds, communities, CMS channels), Apex Invocable Actions, and ContentVersion multipart upload. Whatever your bundle hits, it lands on the same local data your tests use.
  • The nimbus serve live dashboard at /__nimbus/dashboard is reused by nimbus app. Inspect the same database, replay the same requests — one mental model across both commands.
Fixed
  • Long-running nimbus serve sessions no longer surface driver: bad connection errors after idle periods. The server was opening a fresh per-request transaction against the embedded Postgres on every call, which the pool would eventually time out and close mid-request. The server now reuses the live interpreter's connection pool — the same path the test runner already used — so connections survive idle stretches and reconnect cleanly when the pool prunes a stale handle.
May 11, 2026
0.2.4
Changed
  • Platform-fidelity compat warnings are now scoped to the code your test actually exercises. Previously, nimbus test emitted compile-time-rule diagnostics for every class in the project at registry-load time — including classes the test never touched — and under the default nimbus.compat.mode=strict would fail the run because of violations in unrelated code. Diagnostics are now deferred and surface only when the interpreter actually invokes the offending class (static call, instance construction, or test-method entry). The strict-mode exit gate fires for the same scope. Project-wide compat audits stay in nimbus validate for explicit "scan the whole repo" workflows.
May 10, 2026
0.2.3
Changed
  • Code coverage and governor limit enforcement are now Free-tier features. nimbus test --coverage with console, JSON, and HTML output runs on every install — no license required. Governor limit enforcement (SOQL/DML/query-row counters, strict/warn/off modes) was already part of the core runtime and stays free. The Pro tier focuses on developer-productivity features (background daemon, parallel execution, watch mode, debugger, mutation testing, VSCode coverage gutters); JUnit XML and Cobertura XML output remain Team-tier for CI integration. The principle: Free lets you trust the result, paid tiers make you faster.
May 10, 2026
0.2.2
Added
  • Nimbus writes manifest/nimbus-missing.xml whenever a test run surfaces missing metadata. The file is a standard SFDX manifest you can hand to sf project retrieve start --manifest manifest/nimbus-missing.xml -o <alias> to fetch every missing item in one command. The filename is distinct from package.xml so it never clobbers a user-managed deployment manifest. --fetch-missing now drives a single sf invocation internally — replacing up to four per-category calls with one — so retrievals are faster and immune to argv length limits on projects with many missing items.
  • Custom Metadata Types are now detected as missing when the type schema isn't deployed locally. Previously, a SOQL query against a __mdt table that wasn't in the project source returned an empty list silently — the test would pass on data that didn't actually exist. The detector now surfaces the missing type in the run summary and (with --fetch-missing) retrieves both the source XML and the runtime JSON describe in one pass, so a re-run sees the type and its records.
Fixed
  • --fetch-missing no longer redundantly re-fetches items the org doesn't have. The previous retry loop attempted up to three retrieve passes for the same set of names; if the org didn't have them on the first attempt, the second and third were guaranteed to fail too — burning ~10–30s per pass on a real sf call. Two root causes: sf project retrieve can return success with empty result arrays (when none of the requested items exist) or with entries marked state: "Failed" in the response — both shapes were being miscounted as successful retrievals. The retriever now parses sf's response correctly, surfaces the per-file failure messages to the user, and the runner remembers which entries it has already attempted so the same name is never re-fetched within one run.
  • The Custom Metadata Type manifest mapping would have been rejected by sf. CMDT entries were emitted under <name>CustomMetadata</name> with bare type names — but that manifest type expects record-level members in Type.RecordName form. Type schemas live under <name>CustomObject</name> in package.xml, with the __mdt suffix on the member name as the signal to sf. The mapping is now correct, so the auto-generated manifest round-trips cleanly through sf project retrieve.
May 9, 2026
0.2.1
Fixed
  • A static-property getter on one class can now read another class's same-named static property. The recursion guard that stops X { get { … X … } } from looping was keyed on the field name only — so when class A's getter for FOO read class B's FOO, B's getter was short-circuited and returned its raw (uninitialized) backing storage instead of running. Nebula Logger v4.18's new declarative ignored-class config (LoggerStackTrace.IGNORED_APEX_ORIGINS chains into LoggerParameter.IGNORED_APEX_ORIGINS) was the visible breaking case — the ignored-class list came back empty regardless of the configured custom metadata. The marker is now scoped per (class, field), so cross-class reads land on the real getter.
  • SOSL RETURNING blocks now honor their WHERE / ORDER BY / LIMIT filters when running against Test.setFixedSearchResults. The fixed-results stub previously returned the raw Id list verbatim, ignoring whatever filter the caller had appended inside the RETURNING parens. Strict-search patterns (FIND … RETURNING LogEntry__c (Id WHERE RecordId__c = :recordId)) couldn't exclude records that didn't match. The stub now translates the RETURNING spec into an equivalent SOQL query scoped to the fixed Ids and runs it through the normal SOQL evaluator, so bind variables and platform filter semantics apply.
May 9, 2026
0.2.0
Added
  • nimbus mcp exposes Nimbus to AI coding agents. A Model Context Protocol server that lets Cursor, Claude Code, and other MCP-aware agents run tests, list test classes, fetch coverage, and surface failures the same way you would from the CLI — so agents can iterate on Apex code without leaving the editor.
  • nimbus skills manages installable agent skills. Skills are bundled task playbooks (list, add, remove, path) that an agent loads on demand. Ships with the apex-coverage-uplift skill, which reads Nimbus's per-line coverage and drives an agent to write targeted tests for the highest-leverage gaps rather than synthetic call-only tests.
  • The MCP list_test_classes tool now returns file paths alongside class names. Saves a round-trip when an agent needs to open the source.
Fixed
  • nimbus upgrade no longer clobbers Homebrew and Scoop installs. An in-place binary swap was wrong for package-manager installs — brew and scoop track the installed version in their own metadata, and any subsequent brew or scoop command would revert the swap, so the user appeared to upgrade and then snapped back to the old version. Nimbus now detects the install source from the resolved binary path and routes you to brew upgrade nimbus or scoop update nimbus. The passive "new release available" notice and the VS Code extension's update prompt use the same detection.
  • After a successful upgrade, Nimbus warns about stale duplicates on PATH. If a second nimbus binary lives elsewhere on PATH (e.g. a leftover ~/go/bin/nimbus or ~/.local/bin/nimbus from a prior install method), nimbus upgrade now lists those paths so a fresh shell doesn't silently fall back to an older copy.
May 8, 2026
0.1.61
Changed
  • (Id) "15char" casts no longer expand to 18-char form. The expansion was added to support a single use case (case-only-different 15-char Ids producing distinct 18-char forms for unique-constraint purposes) but breaks every Map<Id, V> and Set<Id> keyed off raw 15-char record Ids — the cast site is too far upstream to keep collections in agreement, and a half-dozen frameworks (apex-test-kit's mock relationships, apex-fp's pluckIds/groupBy, forceea's data factory) regressed once a cast Id no longer matched its raw counterpart. The unique-constraint path needs to be solved at the SOQL/DML layer instead.
May 8, 2026
0.1.60
Fixed
  • SObjectType.getDescribe().getName() now returns the proper API casing regardless of how the type was reached. Previously, calling Account.SObjectType.getDescribe().getName() returned "Account" while account.getSObjectType().getDescribe().getName() on a queried record returned "account", breaking frameworks (fflib UoW) that key a Map<String,V> by the result and look it up the same way. The dispatcher now normalizes the lookup name through the schema provider so both paths agree.
  • Typed Map<Id, V> map literals lowercase their keys. Apex Id values are case-insensitive, but the literal initializer was using the raw key string. A subsequent .get(otherId) would compute the lowercased key and miss, returning null even when both Ids referred to the same record. Map literal evaluation now mirrors what put() does for Map<Id, V>.
Changed
  • Internal: platform-special maps now self-identify as case-insensitive. The schema describes (fields.getMap(), getGlobalDescribe(), RecordTypeInfo maps), OrgLimits.getMap(), HTTP headers, QueryException.getInaccessibleFields(), and SObject.getPopulatedFieldsAsMap() now carry an explicit case-insensitive flag. No behaviour change for users — regular Map<String, V> still defaults to case-insensitive lookup as before — but it sets up the correct foundation for the planned switch to spec-correct case-sensitive defaults.
May 8, 2026
0.1.59
Fixed
  • JSON.serialize(Map<String, V>) emits keys in reverse insertion order, matching Salesforce. Verified live across maps of size 1, 3, 5, 10, and 20: SF's JSON.serialize iterates string-keyed maps in the exact reverse of put()-order. Nimbus was emitting forward insertion order, which broke any test asserting on full-string JSON output (DLRS's RollupEditorController.testValidateRollupConfig). The fix also covers nested maps inside lists, SObjects, and custom objects — those previously fell through to Go's encoding/json map encoder, which alphabetizes keys, so they got the wrong order even when the top-level value was already correct.
May 8, 2026
0.1.58
Fixed
  • COUNT-only aggregate queries track one row per result group. Salesforce's Summer '18 governor accounting tracks one query row per group for COUNT() / COUNT(field) aggregates regardless of how many underlying rows contributed; SUM / AVG / MIN / MAX still bill per underlying row. Nimbus was applying the underlying-row count uniformly, over-counting tests that asserted on the platform's post-Summer-'18 COUNT semantics. The dispatcher now detects COUNT-only aggregates and uses the result-row count.
May 8, 2026
0.1.57
Fixed
  • Child subqueries resolve multi-level relationship paths. A subquery that selects a grandparent field ((SELECT Id, PricebookEntry.Product2.Name FROM OpportunityLineItems)) was projecting only the first-level FK column and dropping the grandparent reference, leaving the in-memory relationship null and causing NullPointerException on access. The dispatcher now recurses through dotted segments — fetches each level's parent rows in turn and chains them onto the SObject so oli.PricebookEntry.Product2.Name resolves correctly.
  • Subquery ORDER BY drops cross-relationship terms instead of failing the query. A subquery sorted by a grandparent field (ORDER BY PricebookEntry.Product2.Name) couldn't resolve against the flat single-table SQL nimbus emits and failed with missing FROM-clause entry — the subquery returned zero rows even though the FK filter would have matched several. The dispatcher now drops dotted ORDER BY terms and falls back to the default Id ordering when none remain, so the rows still surface (in insert order rather than the requested cross-object order).
May 8, 2026
0.1.56
Fixed
  • TextArea / LongTextArea fields trim trailing whitespace on direct field assignment. The platform strips trailing whitespace when saving values to TextArea / LongTextArea / Rich Text fields. Patterns like logEntry.ErrorMessage__c += someMsg + '\n'; (DLRS's rollup-error logger) accumulate values terminated with newlines, and the matching equality assertions exclude the trailing newline. Nimbus was preserving the value verbatim through the in-memory obj.field = … path, so the assertion failed by exactly one character.
May 8, 2026
0.1.55
Fixed
  • Database.update with allOrNone=false now commits successful records when others in the batch fail. Salesforce processes per-record failures independently — a single validation-rule failure on row 2 doesn't block rows 0 and 1 from committing. Nimbus was running the batch as a single SQL update, so any failure rolled back every record. The partial-success path now retries record-by-record after a group throws so each row's outcome is recorded individually.
  • Database.Error.getMessage() on a partial-success failure now returns just the validation-rule message. The DML exception nimbus throws on validation failure carries the platform-style framing (Update failed. First exception on row 0; first error: STATUS_CODE, message: [field]). Surfacing that whole string through Database.Error.getMessage() broke any framework that concatenates getMessage() + getStatusCode() + getFields() (DLRS's rollup-error logger does this). The dispatcher now parses the platform-format message and exposes the trio (statusCode, validationMessage, fields) on the underlying Database.Error object.
May 8, 2026
0.1.54
Fixed
  • Private OWD sharing now filters records the running user can't see. Custom objects with <sharingModel>Private</sharingModel> in their .object-meta.xml are recorded on the schema, and SOQL run from a with sharing class under System.runAs(testUser) filters records by ownership: rows owned by the running user, plus rows where a matching row in the object's __Share table grants the user explicit access. Earlier nimbus passed every record through regardless of OWD; rollup tests asserting that User B sees only their own children failed because the engine summed everyone's children.
  • Aggregate queries (GROUP BY) now have the OwnerId predicate injected before grouping. The previous WITH SHARING filter skipped queries with GROUP BY entirely on the assumption that the OwnerId column wasn't projected; in practice the predicate belongs in the WHERE so it filters rows before they reach the aggregate. The injection now finds the GROUP BY position and inserts the predicate ahead of it.
May 8, 2026
0.1.53
Fixed
  • StandardController.getId() now returns the 15-character form of the record Id. Visualforce reads the id from the URL id query parameter, which is the case-significant 15-char form. Code that builds a value from standardController.getId() and asserts equality against String.valueOf(record.Id).substring(0, 15) (DLRS's scheduled-rollup cron name pattern) was reading 18-char from nimbus and failing the comparison. The constructor now truncates to 15-char on retain.
  • String equality treats matching 15-char and 18-char Salesforce Ids as equivalent. When an Id-typed field (18-char) is compared to a string sourced from the 15-char form, Apex code historically expects the comparison to succeed since both name the same record. Nimbus was doing strict string equality. valuesEqual now falls back to a 15-char prefix check when both operands are Id-shaped strings of length 15 or 18.
May 8, 2026
0.1.52
Fixed
  • WITH SHARING SOQL no longer injects an OwnerId filter on setup objects. Setup / metadata objects (ApexTrigger, ApexClass, Profile, PermissionSet, __mdt, etc.) have no record-level sharing — they're universally readable subject to object-level CRUD, which is checked separately. Nimbus was treating them like any other queried object, appending OwnerId = '' to the WHERE clause, which either filtered every row out (the records aren't owned by the running user) or failed against tables with no OwnerId column. A trigger-deployed validation rule that queries ApexTrigger from inside a non-admin System.runAs block (DLRS's rollup-summary insert validates that the configured rollup target trigger is deployed) silently came back with zero rows. The dispatcher now skips the OwnerId injection for setup/metadata objects.
May 8, 2026
0.1.51
Fixed
  • SOQL row-count tracking on aggregate queries now matches Salesforce. Apex's Limits.getQueryRows() reports the number of underlying rows that an aggregate (SELECT … GROUP BY or COUNT(field)) consumed; PostgreSQL returns one row per group, so nimbus was reporting the count of grouped result rows. Tests that asserted on the platform's underlying-row accounting (rollup-style verifiers that expect 9 child rows for 3 groups of 3) under-counted by the difference. The dispatcher now runs a SELECT count(*) sibling against the same FROM/WHERE and uses that for the SOQL row tracker.
  • Database.update against soft-deleted records now raises ENTITY_IS_DELETED before validation rules fire. A trigger pattern that deletes a parent record then runs a rollup recalculation needs the update against the now-deleted parent to surface as a deleted-row error so the rollup framework can swallow it. Nimbus was running the update through, firing the validation rule on the recalculated value, and producing a FIELD_CUSTOM_VALIDATION_EXCEPTION instead. The dispatcher now pre-checks soft-delete state and throws System.DmlException with statuscode ENTITY_IS_DELETED before the update reaches the validation pipeline.
  • System.schedule duplicate-name error now matches platform wording. Apex throws System.AsyncException with the message The Apex job named "X" is already scheduled for execution. when scheduling a job whose name already exists. Nimbus was throwing the wrong wording (Duplicate CronTrigger name: X), so any test asserting the exception message via String.equals failed.
May 8, 2026
0.1.50
Fixed
  • SOQL "No such column" errors now match Salesforce wording. When a query referenced a non-existent field, callers caught a System.QueryException whose message read like SOQL execution error: query failed: ERROR: column foo.stage does not exist (SQLSTATE 42703) — fine for debugging, but anything that asserted on the message with String.contains('No such column') failed. The dispatcher now reformats column-not-exist errors to the platform string: No such column 'Stage' on entity 'Foo'. If you are attempting to use a custom field, be sure to append the '__c' …, with original casing recovered from the SOQL source.
  • Custom-object describe field maps now include the IsDeleted system field. Code that walked SObjectType.<X>.getDescribe().fields.getMap() looking for standard audit fields on a custom object came back empty for isdeleted — fflib's fflib_SObjectDescribe wrapper exposes the same map and DLRS's rollup-criteria validation rejected any criteria field that referenced IsDeleted with "Field isdeleted does not exist on the child object." The standard-fields injector now appends IsDeleted alongside CreatedById, OwnerId, SystemModstamp, etc.
  • AFTER UNDELETE triggers now receive the full restored record, not just the Id stub. When undelete was called via Id (Database.undelete(opps[0].Id)) the trigger context was populated with a stub SObject whose Fields map only contained id. Triggers that read foreign keys or aggregated values (rollup recalculators are the obvious case) silently produced wrong results because the lookup field they needed was missing. The handler now re-queries each undeleted record after clearing isdeleted and merges the row into the trigger payload before firing.
  • The (Id) cast on a 15-character string now expands to the 18-character form. Real Apex normalises a 15-character Id to its 18-character canonical form when cast to Id, encoding the original case into the three-character checksum so two 15-char strings that differ only by case map to two distinct 18-char Ids. Nimbus left the value unchanged, so (String)((Id) "Aaa00000000") and (String)((Id) "aAA00000000") produced identical strings — patterns relying on case-insensitive unique constraints over a casted Id incorrectly tripped the constraint.
May 8, 2026
0.1.49
Fixed
  • Database.getQueryLocator now raises System.QueryException on parse errors. The locator was eagerly executing the query but silently dropping any error result, so callers using it as a "validate this string" probe (DLRS's RollupSummaries.onValidate, fflib's QueryFactory validation paths) couldn't catch malformed SOQL via try/catch. The dispatcher now propagates the underlying ThrowValue.
  • String.format now follows Java MessageFormat quoting rules. A single quote starts/ends a quoted region (placeholders inside aren't expanded), and '' represents a literal single quote both inside and outside that region. Templates like 'aaa\'\'{0}\'\'bbb' now correctly produce aaa'x'bbb instead of aaa''x''bbb — matches platform behaviour and unblocks any test that does startsWith on a formatted error string.
May 8, 2026
0.1.48
Fixed
  • SOQL ORDER BY now defaults to NULLS FIRST for both ASC and DESC. Salesforce SOQL treats null as the smallest value regardless of direction; PostgreSQL's default is the opposite (ASC → NULLS LAST, DESC → NULLS FIRST). Tests that omit the explicit NULLS FIRST / NULLS LAST clause and depend on the platform's ordering — common in concatenation rollups — were getting differently-ordered result sets and asserting the wrong concatenated string. The translator now emits the SOQL-default NULLS FIRST when the caller didn't specify one.
  • Custom Metadata Type access checks now grant universal read. X__mdt objects are universally readable on the platform — every profile, including Read Only and Standard User, can read MDT records and fields. fflib's assertIsAccessible() wrapper around an MDT query (used in DLRS's LookupRollupSummary2__mdt selectors) was throwing under any System.runAs(readOnlyUser) context. CheckObjectPermission and CheckFieldPermission now short-circuit to true for read on __mdt suffixed types.
  • Database.undelete now accepts Id and List<Id>. The handler only accepted SObject / List<SObject>; calls like Database.undelete(opps[0].Id) erroried with Cannot undelete *interpreter.StringValue. The dispatcher now resolves the SObject type from the Id's key prefix and synthesises a stub SObject so the rest of the path (savepoint, isdeleted clear, AFTER UNDELETE trigger) is unchanged.
May 8, 2026
0.1.47
Fixed
  • Describe-style methods on Schema.SObjectField now resolve through the field-describe lookup. The token returned by SObjectType.<Sobj>.fields.<Field> is a Schema.SObjectField per platform semantics; Apex routinely accepts the same value where a Schema.DescribeFieldResult is expected (Schema.DescribeFieldResult dfr = Account.fields.Description; followed by dfr.getType()). Nimbus emitted the SObjectField but only routed describe methods (getType, getName, getSObjectType, getLength, isAccessible, …) for the DescribeFieldResult class — calling them on an SObjectField returned null, breaking common rollup-validation patterns. The dispatcher now forwards both classes through the same describe-style lookup so both usage shapes resolve identically.
May 8, 2026
0.1.46
Fixed
  • Test class static {} blocks no longer eagerly execute during class-graph load. When loading the dependency graph for any entrypoint (anonymous Apex via nimbus exec or a focused nimbus test SomeTest), every transitively-pulled-in class's static initializer ran up front. For test classes that's wrong: their static {} blocks routinely insert seed records (Accounts, Contacts, custom-setting rows) that real Salesforce would isolate to that class's test transaction. Running them eagerly landed the inserts in the shared database with no rollback boundary, causing later [SELECT … LIMIT 1] queries to return rows from another test's fixture. The runner now skips static {} blocks for @isTest-annotated programs during eager init; field initializers still run so static fields hold their declared defaults, and the blocks fire lazily on first reference (inside the active test's transaction, where rollback applies).
May 7, 2026
0.1.45
Added
  • Bundled Calendar standard-object describe. SOQL referencing Calendar (commonly via WHERE OwnerId IN (SELECT Id FROM Calendar WHERE Type = 'Public') on Event-related controllers) was failing with "relation calendar does not exist" because the platform's standard Calendar object wasn't in nimbus's embedded describe set. Run nimbus sync to materialise the table in projects updating from earlier versions.
May 7, 2026
0.1.44
Fixed
  • SOQL field NOT IN (…) now matches Salesforce's null-tolerant semantics. The platform treats NULL NOT IN (…) as TRUE — so a row with a null lookup-traversal value (e.g. Lead.ConvertedContact.Status__c when the lead isn't converted) still satisfies the predicate. Standard SQL and PostgreSQL return NULL for the same comparison, which evaluates as false in WHERE, silently dropping the row. The translator now wraps every <lhs> NOT IN (literals) as (<lhs> NOT IN (literals) OR <lhs> IS NULL) so the LEFT-JOINed null falls back to TRUE. Subqueries on the right (NOT IN (SELECT …)) keep their existing semantics.
  • SOQL format(field) [alias] projections now resolve to the underlying column. The translator was leaving the format(...) wrapper untouched, so the SELECT list quoted it as a literal column name ("task"."format(status) statusvalue") and Postgres rejected the query. format() on the platform is a locale-formatting projection for Date/Datetime/Number/Currency fields; nimbus emits the raw column under the requested alias so downstream .get('alias') reads return a sensible value.
  • Bare keyword IS is no longer mis-quoted. The simple-identifier quoting pass treated any uppercase-starting token as a column reference; IS NULL emitted by the new NOT IN wrap (and any user-written same-case form) had its IS stamped as "<table>"."is", breaking the surrounding clause. IS is now in the keyword skip list alongside AND / OR / NOT / IN.
May 7, 2026
0.1.43
Fixed
  • SOQL self-qualified WHERE references no longer produce db.schema.table.column. A WHERE clause like SELECT … FROM Event WHERE Event.StartDateTime != null sent the relationship resolver into the catch-all path because Event isn't in the join map (it IS the main table). The simple-identifier quoting pass then stamped both segments independently and produced "event"."event"."event"."startdatetime", which Postgres rejected as a cross-database reference. The relationship resolver now treats a parent prefix matching the main table as a self-reference and emits a single "event"."startdatetime".
  • SOQL multipicklist INCLUDES/EXCLUDES now translate to PostgreSQL = ANY(ARRAY[…]) / <> ALL(ARRAY[…]). The translation was wired into a legacy WHERE path that was never called from the active translator, so the operator passed through to PostgreSQL untouched. The downstream identifier-quoting pass then matched the bare keyword as a column name and stamped "table"."includes", surfacing as a syntax error pointing at the table identifier. The translation now runs in the live WHERE pipeline, and ARRAY/INCLUDES/EXCLUDES are added to the keyword skip list so subsequent passes don't mis-quote them.
May 7, 2026
0.1.42
Fixed
  • System.SelectOption.getLabel() now returns the stored label. The generic describe-class getLabel() dispatcher returned the receiver's ClassName as a fallback when none of its enumerated classes matched, which shadowed the dedicated SelectOption dispatcher further down — calls of opt.getLabel() on a Visualforce SelectOption were resolving to the literal string System.SelectOption. Visualforce extensions that match CSV header components against field labels via opt.getLabel() failed to map any field; the import flow then quietly skipped its main insert. Fixed by handling SelectOption explicitly in the getLabel case.
May 7, 2026
0.1.41
Fixed
  • Malformed JSON now raises System.JSONException from the parser instead of looping forever. JSON.createParser(s) on a malformed string was returning a parser with zero tokens; consumer code shaped like while (parser.nextToken() != JSONToken.END_OBJECT) { … } would then loop because nextToken() kept returning null and null != END_OBJECT stays true. The parser now captures the underlying tokenize error at createParser time and replays it as a System.JSONException on the first token-reading call (nextToken, getCurrentToken, getText, get*Value, skipChildren) — matching real Salesforce, where createParser is lazy and the failure surfaces on consumption. After the throw the parser transitions to an exhausted state so a subsequent consumeObject call (often called outside the try/catch) doesn't immediately re-throw.
  • while and do-while conditions now propagate ThrowValue. The condition expression was only checked for ErrorValue; a method call inside the condition (e.g. parser.nextToken() raising JSONException) had its ThrowValue fall through to IsTruthy, which returns true for unknown types — running the loop body and re-evaluating the condition forever. Both loops now treat the condition the same way the body already did: throws and errors return immediately so the surrounding try/catch can handle them.
May 7, 2026
0.1.40
Fixed
  • Assigning a SOQL list result to an instance SObject field now auto-unwraps to the first element. Apex semantics: this.profile = [SELECT … FROM Foo__c WHERE Id = :id] assigns a single SObject (the first row) when the field is declared as a singleton, throwing if the result has > 1 row. The local-variable assignment path already coerced via coerceSoqlListToSObject; the instance-field path was storing the raw List<Foo__c> instead, so a downstream this.profile.Bar__c = X later in the same method failed with "UNIMPLEMENTED_ASSIGNMENT" because the receiver wasn't an SObject. The field-assignment path now applies the same coercion.
  • Auto-property field reads no longer break member assignments. Reading an auto-property field (public Foo__c profile { get; set; }) returns the inner value wrapped in a FieldReferenceValue; the profile.Bar__c = X assignment path was matching against the wrapper rather than the underlying SObject and falling through to the catch-all "UNIMPLEMENTED_ASSIGNMENT". The wrapper is now unwrapped before the type-switch.
May 7, 2026
0.1.39
Fixed
  • Datetime ± Integer arithmetic now returns a Datetime offset by N days. system.now() - 10 previously errored with "Unknown operator". Apex semantics: integer addition/subtraction on a Datetime advances/regresses the value by that number of days (matching Date). Both Datetime + Integer and Datetime - Integer are now handled, preserving the operand's IsGmt flag so subsequent comparisons stay on the correct reference frame.
  • Limits.getFindSimilarCalls() / getLimitFindSimilarCalls() are now defined. The legacy duplicate-management counter was missing; codebases that defensively read every governor counter via reflection or that simply log the full set hit "Unknown method" when they touched it. Stubbed to 0 / 3 to match the platform shape.
May 7, 2026
0.1.38
Fixed
  • Inline // … comments inside SOQL brackets no longer truncate the query. The translator was stripping Apex line comments AFTER collapsing whitespace; once newlines became single spaces, the regex //[^\n]* happily ate everything from the first // through to the closing bracket. Multi-line SOQL with field-trailing annotations dropped most of the SELECT list and arrived at the DB as a truncated query. Comment stripping now runs first.
  • Database.executeBatch now writes the AsyncApexJob row shape Salesforce actually produces. Real SF emits one parent BatchApex row at executeBatch time plus one BatchApexWorker row per chunk processed; nimbus was emitting only a single row with one or the other JobType. The parent row is now inserted on enqueue and worker rows per scope iteration in executeBatchLifecycle, so empty-input batches produce one row (parent only) and chunked batches produce one + N.
  • Lookup FKs are restored on undelete. The 0.1.36 fix that "preserved FKs across soft delete" was wrong about platform behaviour — Salesforce DOES null SetNull child FKs at soft-delete time (visible to SOQL between delete and undelete) and then restores them when the parent is undeleted from the recycle bin. Soft delete now snapshots and nulls the FKs; undelete repoints them at the now-restored parent.
May 7, 2026
0.1.37
Fixed
  • Invalid relationship paths in SOQL once again raise System.QueryException. The 0.1.36 chain-break that suppressed broken JOINs for the parent-to-child WHERE pattern (WHERE Contacts.Individual.Id = …) was too eager: it also swallowed genuinely invalid field paths like UserLicense.InvalidPath.Name, where the first segment resolves but a later one doesn't. The translator now silent-breaks only when the depth-0 segment matches a known child-relationship name (handled later as a semi-join); every other unresolved segment falls through to the broken-JOIN path, so the downstream SQL error is wrapped to System.QueryException at the SOQL boundary.
May 7, 2026
0.1.36
Fixed
  • Salesforce-managed implicit defaults now apply on insert (Account/Contact/Lead.CleanStatusPending). The platform stamps these on insert even though the field describe lists no defaultValue; nimbus stored them as null, which broke any rollup or evaluator that COUNTs over CleanStatus through a one-to-many path. Verified by running anonymous Apex against a real org: insert new Account(Name='X') followed by SELECT CleanStatus returns Pending. Structured as a per-SObject map so future implicit defaults drop in without touching call sites.
  • SOQL DAY_ONLY() now uses GMT semantics, matching the platform. Salesforce extracts the date from the stored UTC instant; nimbus was using the raw locally-stored components, so a CaptureDate at local 23:59:59 was incorrectly matched as today by DAY_ONLY = TODAY. The translator now wraps the column with AT TIME ZONE before CAST AS DATE, lifting the stored components into a real UTC instant first.
  • Datetime comparisons now resolve mixed-IsGmt operands to a true UTC instant. Datetime.newInstanceGmt(d, t) and Datetime.newInstance(d, t) produce instants offset by the user's timezone; nimbus stored both as raw components, so a GMT-built reference compared against a local-flavoured stored value with raw component equality got the wrong answer for boundary records across midnight. When the operands disagree on IsGmt, each side now resolves to a UTC instant; same-IsGmt comparisons keep the historical component path, so codebases that store and compare exclusively non-GMT values are unaffected. addDays/addMonths/etc. now preserve the IsGmt flag, and Datetime.valueOf(stringWithTzMarker) returns an IsGmt=true result so downstream comparisons stay on the right reference frame.
  • System.Assert.areEqual Datetime semantics now match Datetime.equals (second-precision). The Assert path fell through to a string-form fallback that included milliseconds, so a Datetime that round-tripped through SObject field assignment (which truncates millis to match Salesforce's stored precision) compared unequal to its pre-store twin under Assert.areEqual but equal under System.assertEquals. Apex's documented Datetime.equals() ignores millis; both paths now agree.
  • Lookup foreign keys are preserved across soft delete (recycle bin). Salesforce nullifies SetNull child references only on hard delete; soft delete (default delete records;) keeps them so a subsequent undelete restores the chain end-to-end. Nimbus was nullifying immediately, so delete parentApps; undelete parentApps; left children with permanently null lookups. Cascade-delete (master-detail) is preserved unchanged — those still cascade into the recycle bin and restore on undelete.
  • Set<T> instanceof Set<Object> now respects Apex's generic-parameter invariance. Set<String> is not a Set<Object> for instanceof purposes (it is for assignment, but those are different rules). The earlier permissive behaviour conflated the two and miscategorised typed-instanceof chains that branch on Set<Object> vs Set<SObject>. The legitimate covariance case (Set<Account> instanceof Set<SObject>) is unchanged.
  • Switch when literals now apply leading sign tokens. The Apex grammar permits when -7 as a single literal with a SUB child; the tree walker was reading only the integer part, so when 4, 5, 6, -6, -7, -8 silently collapsed to when 4, 5, 6, 6, 7, 8. Codebases that map negative offsets through Math.mod got off-by-one fiscal-month routing.
  • Async work queued without a matching Test.stopTest is now discarded between test methods. Real Salesforce tears down each test method's queued Queueables/Schedulables/Future jobs/Platform Events at method boundary; nimbus was carrying them on the shared interpreter, so a stale Schedulable from test N could fire during test N+1's Test.stopTest drain — running with whatever static state happened to be in scope. The runner now resets these queues alongside ClearAllStaticVars at the start of every test method.
  • SOQL COUNT() with LIMIT N now caps the returned count at N. Salesforce semantics: SELECT COUNT() FROM X LIMIT 1 returns 0 or 1, never the full row count. PostgreSQL applies LIMIT to the result set (always one row for a count-only query), so the standalone clause was a no-op. The translator now folds the limit into the projection via LEAST(COUNT(*), N) when the SELECT is a single count and there's no GROUP BY.
  • Polymorphic Type filters now recognise JOIN-aliased relationship targets. Grandparent rollups can generate queries like LEFT JOIN account AS what ON … WHERE what.type = 'Account' — the join alias is named after the relationship (what), not the table (account). The earlier polymorphic-Type-tautology pass only stripped the redundant filter when the alias name matched the literal directly. It now also accepts matches where the alias resolves through the joins map to a table matching the SObject literal.
Added
  • Parent-to-child WHERE predicates are rewritten as semi-joins. Some real-world rollup paths emit SOQL like WHERE Contacts.Individual.Id = :recordIds — strict bare SOQL doesn't permit child-relationship traversal in WHERE, but the platform appears to silently rewrite to the equivalent EXISTS shape. The translator now does the same up front: id IN (SELECT AccountId FROM Contact WHERE Individual.Id = :recordIds). Operators supported: =, !=, <>, IN, NOT IN.
May 1, 2026
0.1.35
Added
  • toLabel(picklist) in SOQL now returns the picklist's display label. Salesforce's toLabel(Field__c) returns the human-readable label (e.g. Long Range) rather than the stored API name (Long_Range). The translator was stripping the wrapper and projecting the raw column, so tests asserting against labels passed in the org but failed locally. Per-value labels are now captured in FieldSchema, propagated through the SFDX scanner, embedded JSON describe, and source cache; the translator emits a CASE expression mapping API names to labels, aliased to the underlying column. Falls back to the prior raw-value behavior when labels are unknown (global value sets, missing schema), so queries that already worked still do.
May 1, 2026
0.1.34
Fixed
  • Unlinked __r placeholders now compare equal to null. The Apex pattern if (rec.Rel__r != null) { …rec.Rel__r.field… } was failing locally because the interpreter returned an unlinked SObject placeholder for __r reads on records with a null foreign key — so the guard fell through and the chained access blew up. Equality (== / !=) now treats these placeholders as null while preserving safe field reads on the same value, matching real Salesforce semantics.
May 1, 2026
0.1.33
Fixed
  • Test.loadData now coerces CSV cells to typed Apex values. Every cell was previously stored as a String regardless of the SObject's field schema — so test code that did acc.AnnualRevenue.intValue(), lead.IsConverted == true, or opp.CloseDate.year() on loaded records blew up because the value was a String, not a Decimal/Boolean/Date. The CSV reader now consults FieldSchema.Type per column: numeric fields parse to Integer/Decimal, checkboxes to Boolean, date/datetime via the same flexible parser the rest of the interpreter uses, and blank cells become null (matching Apex's blank-cell semantics). Reference fields and unknown columns fall back to String.
May 1, 2026
0.1.32
Fixed
  • Auto-generated stubs are now folded into the AST cache and the daemon's program map. nimbus stubs auto and nimbus test --write-stubs previously wrote new stubs/<Name>.cls files to disk but left them outside the runner's in-memory state, the consolidated AST cache, and any running daemon — so the next test run paid a re-parse cost, and a daemon serving LSP/IDE features stayed blind to the new classes until restart. The writer now hands new files back to the runner: programs map and class registry are mutated in place, .nimbus/cache is updated, and the daemon (if attached) is invalidated explicitly. The daemon also watches stubs/ on its filesystem watcher and pre-parses it at startup, so file events from any source — CLI, LSP, hand edits — flow through without a restart.
April 30, 2026
0.1.31
Fixed
  • nimbus mutate now loads nimbus.properties for the baseline pass. The mutate command built its test runner from CLI flags only, skipping the project-config block that nimbus test uses — so seeded labels, mock profiles, stub namespaces, org config, and a dozen other fields were silently zero-valued during the baseline. Projects that override custom labels via nimbus.seed.label.* (or rely on any other nimbus.properties seeding) saw baseline has 1 failing tests — fix them before mutation testing on tests that pass cleanly under nimbus test. Mutate now mirrors the test runner's config loading so the baseline matches.
April 30, 2026
0.1.30
Changed
  • Free tier now re-syncs org schema before every nimbus test. A green test built on stale schema is the worst kind of false confidence — pass locally, fail on deploy when a custom field added in the org wasn't in the local DB. Free now pays the sync cost up front so the result is honest. Pro keeps the existing first-run-only behavior because the daemon owns sync lifecycle and keeps state warm; trigger an explicit refresh with nimbus sync when needed. The setup output prints a one-line hint so the speed cost lands paired with the upgrade path.
Fixed
  • nimbus upgrade works on Windows. The self-update path assumed every release artifact was a .tar.gz with a bare nimbus binary inside; on Windows the asset is a .zip containing nimbus.exe. Upgrade now extracts both archive types and matches the OS-correct binary name, so nimbus upgrade from a Windows shell completes instead of erroring with binary not found in archive.
April 29, 2026
0.1.29
Performance
  • Windows now gets the same PostgreSQL tuning macOS and Linux do. An upstream quoting bug in the embedded-postgres library prevented our perf flags (shared_buffers=512MB, jit=off, fsync=off, autovacuum=off, etc.) from reaching pg_ctl on Windows — postgres booted with conservative defaults instead, leaving warm test runs 2-5× slower than the Unix path. We now apply the same parameters via ALTER SYSTEM + a quick restart, so Windows users get near-parity warm-run performance with Mac and Linux. CI on windows-latest verifies the tuning actually lands via SHOW assertions on every PR.
April 29, 2026
0.1.28
Fixed
  • Embedded PostgreSQL now works on native Windows. First-run initdb previously aborted at the post-bootstrap stage on Windows because of a path-layout mismatch between the bundled binary and the extension DLLs. The extractor now lifts dict_snowball.dll, plpgsql.dll, and friends to the directory the Windows binary expects, and we work around an upstream quoting bug that caused pg_ctl to reject our perf-tuning flags. No first-launch download required — the bundle ships in the binary and just works.
  • Daemon, named-pipe IPC, and AST/label caches verified end-to-end on Windows. Three new integration tests — embedded postgres lifecycle, daemon transport, and Pro-tier cache machinery — run on every PR against a real windows-latest runner, so the Pro experience won't silently regress on Windows.
April 29, 2026
0.1.27
Added
  • Native Windows support. Nimbus now runs on Windows without WSL. The daemon uses Windows named pipes for IPC, detaches cleanly from the parent shell without flashing a console, and ships as a real nimbus-windows-amd64.exe binary alongside the macOS and Linux builds.
  • Cross-platform CI. Every PR now builds, vets, and tests on macOS, Ubuntu, and Windows runners — so the Windows experience won't silently regress.
April 29, 2026
0.1.26
Fixed
  • Fewer background network requests from licensed CLIs. The activity heartbeat is now throttled to once every 24 hours instead of firing on every invocation. The portal's last used timestamp now updates daily.
April 29, 2026
0.1.25
Fixed
  • Sibling test methods no longer interfere with record-triggered flows. A flow that fired during one test method could be silently skipped on the same record in a later test in the same class, even with a fresh transaction. Affected classes that update shared @testSetup data across multiple methods. Sibling tests now see fresh flow state.
April 29, 2026
0.1.24
Performance
  • Test suites run ~60% faster. No flag, no config change. Same workload, less wall time.
Fixed
  • Spring '26 platform-event and scheduled-job fields added to the standard schema (EventBusSubscriber.LastProcessed / LastPublished, CronTrigger.JobType / CronJobDetailName). Resolves intermittent failures on parallel runs.
April 27, 2026
0.1.23
Added
  • nimbus upgrade — self-update from the CLI. Pulls the latest release tarball from GitHub and replaces the running binary in place. Mirrors the curl install script — same source, same naming. Aliased as nimbus update and nimbus self-update. Use --check to compare versions without installing. (This should have shipped in 0.1.20; better late than never.)
  • nimbus login — browser-based machine activation. Sign in via your browser and the CLI registers itself; no key paste, no config files. Companion commands: nimbus logout frees a seat, nimbus whoami shows account + tier. Existing installs keep working untouched — same 7-day offline grace.
  • Self-service portal at testnimbus.dev/portal. Switch plans with prorated billing, view invoices, deactivate machines, invite team members, manage account + data — all in one place.
  • Per-user machine quota. Each user is capped at 3 active machines, even on Team plans where the underlying pool is larger. Tagged automatically at login.
  • CLI heartbeat. The portal's last used timestamp now reflects actual usage rather than activation date. Async, fire-and-forget; never blocks tests. Skipped in CI.
  • Per-package stub layout. Managed-package stubs are organized one folder per package (stubs/Nebula/, stubs/fflib/, …) with optional objects/ subdir for namespaced custom objects. Walkthrough in berlinbrew-demo.
Changed
  • nimbus license activate / deactivate / status are removed in favour of nimbus login / logout / whoami. NIMBUS_LICENSE_KEY still works in CI and AI-agent environments. Existing installs keep working until reinstall.
Fixed
  • Re-activating a machine after a Nimbus reinstall (same fingerprint, fresh local state) now recovers the existing Keygen activation instead of erroring with machine already activated.
April 26, 2026
0.1.21
Added
  • apiVersion validator. A class compiled at apiVersion=52.0 can't use a v66 feature like Database.getCursor or Blob.toPdf — Salesforce eventually rejects the deploy, but the discovery loop is slow. Nimbus now catches it before the deploy. The check reads each *.cls-meta.xml for the declared apiVersion, looks up every referenced symbol against a per-version catalog, and emits a warning when the symbol's introduction version is newer than the class's. Surfaces in three places:
    nimbus validate — inline alongside parser errors.
    apex-version-lint — standalone CLI for CI gates. Add -strict to exit non-zero on any finding.
    VSCode — inline squiggles via the LSP. Editing the meta.xml to bump apiVersion clears the squiggle immediately (no need to re-save the .cls).
  • Catalog: v31.0 → v67.0. ~1000 symbols from every Apex release between Summer '14 and Summer '26, sourced from Salesforce release notes. Catalog policy is GA-only — Beta / Pilot symbols are listed at their GA version, not their first appearance. Per-overload arity tracking distinguishes Database.query(String) (baseline) from Database.query(String, AccessLevel) (v57) so the validator doesn't false-positive on the older overload.
  • Per-line suppression. // nimbus:ignore on the same line or the line directly above suppresses the apiVersion warning. Add a comma-separated rule list to suppress only specific rules. Bare // nimbus:ignore is a catch-all for everything Nimbus emits.
  • Type-position checking. The validator flags too-new types in declarative positions, not just call sites. A v52 class declaring System.Domain d as a parameter, local, field, cast target, or generic type argument now warns on the type itself — same diagnostic the call site would produce, attributed to the declaration line.
  • Missing-meta.xml warning. A .cls without a sibling *-meta.xml silently disables apiVersion checks. VSCode now flags the file at the top so the user knows the gate isn't running. A meta.xml that exists but omits <apiVersion> gets a softer hint (the file is still deployable with Salesforce's default).
  • ~150 new Apex symbols across v31 → v65. Backfills the long tail between the runtime's baseline and the recent (v66/v67) headline additions. Highlights:
    Canvas.Test.mockRenderContext + Canvas.RenderContext / Canvas.ApplicationContext getters (v31).
    System.SandboxContext getters (v36), Metadata.DeployContainer.addMetadata + DeployCallbackContext.getCallbackJobId (v40).
    ConnectApi.NextBestAction.executeStrategy (v45), Formula.recalculateFormulas returning one FormulaRecalcResult per record (v47).
    Auth.VerificationPolicy.HIGH_ASSURANCE + UserManagement verification methods (v49–v50), Messaging.CustomNotification setters + send() (v50).
    Database.LeadConvert person-account fields + Setting__mdt.getInstance(QualifiedAPIName) (v51), B2B Commerce ConnectApi.* stubs + Messaging.SingleEmailMessage.setRelatedToId (v52).
    functions.FunctionInvocation getters (v53), System.Domain getters + DomainParser.parse overloads (v54).
    Invocable.Action.createCustomAction(type, ns, name) + getName / getNamespace / isStandard / setInvocationParameter (v56).
    System.Label.translationExists + ConnectApi.NamedCredentials.create / get / delete (v58), System.Collator + DataWeave.Script.createScript(ns, name) (v59), EventBus.TriggerContext.getRetries / lastError / setResumeCheckpoint (v62), FormulaEval.FormulaInstance.getReferencedFields (v63).
Fixed
  • Database.getCursor now returns a Database.Cursor (the v66 paginated cursor) instead of falling through to Database.QueryLocator. Apex code following the official docs (Database.Cursor c = Database.getCursor(query); c.fetch(0, 10);) was crashing with Unknown method fetch on object Database.QueryLocator.
  • FormulaRecalcResult.isSuccess() returns the stored success flag instead of always false. Same fix for getSObject() and getErrors() — all three now read from the result actually populated by Formula.recalculateFormulas.
  • __mdt.getInstance(key) accepts the record Id and QualifiedAPIName forms added in Spring '21 (v51), not just the legacy DeveloperName. Setting__mdt.getInstance('MySetting.Alpha') now resolves; previously it returned null.
  • Auth.VerificationPolicy.valueOf("HIGH_ASSURANCE") now returns the enum value instead of throwing NullPointerException.
  • Metadata.DeployContainer.getMetadata() and similar season-specific accessors no longer get shadowed by a generic getter fallback that returned null. Affects every namespace stub class added in this release.
April 23, 2026
0.1.20
Added
  • The compatibility release. Broad interpreter, SOQL, and DML fidelity work to bring Nimbus closer to real Salesforce behavior across the board.
  • nimbus serve — local Salesforce Pub/Sub API (Pro). gRPC endpoint with full Avro encoding, auth headers, replay-id support, and keepalive. Point CometD- or EMP-style consumers at localhost and develop platform-event flows with no org. Backed by 26 new exception types, 7 enums, and 15+ stdlib classes. See the Local Server page for setup.
  • DAP debugger. Step, breakpoint, and inspect locals from any DAP-aware editor (VSCode, JetBrains, Neovim).
  • --assign-perms on nimbus test grants permission sets to the running user before execution.
  • --quiet flag, transparent parallel gating, and honest defaults — nimbus test no longer over-promises about parallelism when the workload can't take advantage of it.
  • Strict validator and governor defaults. Limits are enforced by default to match real Salesforce; opt out per-test with config keys when you need looseness.
  • 25+ new standard SObject schemas shipped in stubs/: Site, SiteDetail, ContactPointEmail, ContactPointAddress, ContactPointPhone, ContactPointConsent, OperatingHours, DandBCompany, CallCenter, Individual, OpportunityContactRole, EntityDefinition, BusinessHours, TaskStatus, CaseStatus, Event. Embedded standard SObjects in .nimbus/schemas/ are now auto-recognized.
  • Classic .object metadata loading: inline record types, picklist values, and child relationships from old-style metadata files. CumulusCI unpackaged/ directories are scanned for record types.
  • MDAPI files: .labels and .translation are loaded. The labels parser tolerates bare <labels> fragments that ship without the <?xml?> header or <CustomLabels> wrapper.
  • nimbus.seed.label.<FullName>=<value> overrides a Custom Label's resolved value at runtime. Use when a label's source-of-truth lives in the deployed org (e.g. an admin-set environment-specific profile Id that ships as PLACEHOLDER in the repo).
  • nimbus.seed.group.<Name>=<Type> seeds standing Public Groups into the database. For regional or team groups that admins create once in the org and user-trigger automation queries by name — not usually checked into repo metadata. Deterministic 00G-prefix Id from the name.
  • Test.loadData defaults OwnerId to the running user when the CSV omits it, mirroring what Apex DML does on insert.
  • New system APIs: System.Version, Test.testInstall / InstallContext (managed-package upgrade simulation), Approval.lock / unlock with real LockResult / UnlockResult, System.SelectOption VF class, Schema.DescribeFieldResult.getController() for dependent picklists, Schema.SObjectField.getPicklistValues(), Network stubs (Communities/Experience Cloud), Database.Cursor.fetch(position, count), Metadata.* deploy-container ops (stubs), Messaging.SendEmailResult.isSuccess(), ApexPages.StandardController stub.
  • VSCode 0.1.20: coverage gutter shows a left-border indicator; the extension warns when a newer Nimbus CLI is available.
Fixed
  • SOQL string equality, IN, and ORDER BY are now case-insensitive for string-typed fields, matching Salesforce behavior. Same for __mdt.getInstance() DeveloperName lookups.
  • RecordBeforeSave flows now fire BEFORE Apex before-triggers, matching Salesforce's documented order of execution. Previously reversed, which let a trigger overwrite a field on stale state before a downstream flow could read it (e.g. a trigger flipping Lead.Status back to Open before a flow filtered on Status = Unqualified). Flow $Record writes now also sync back onto the save unconditionally — nimbus.compat.flow-record-sync is removed and the Salesforce-truthful ordering (flow writes → before-triggers → validation rules) is the default. Previously gated flag-off suites may surface validation-rule failures that the old path was hiding; those are real bugs on the platform too.
  • EncodingUtil.base64Decode now strips whitespace from the input, matching Java's MIME base64 decoder (what Salesforce uses). Fixes round-trips through urlDecode (which turns + into space under form-encoding) for ContentVersion upload flows and similar.
  • Schema.getGlobalDescribe().get() is lenient about trailing whitespace in the lookup key, matching Salesforce. Fixes code that reads an SObject name from a Text custom field (not auto-trimmed) and passes it straight to getDescribe().
  • Flow-to-interpreter value conversion handles Date and Datetime. Previously they fell through to the default branch and silently became null, which dropped $Record.SomeDate__c = Now writes from RecordBeforeSave flows.
  • SOQL bind :sobj.Id falls back to the struct-level Id when the Fields map doesn't contain an "id" key. Some paths (notably Test.loadData) populate only the struct Id, so WHERE filters that bound :c.Id silently matched zero rows even when the record was present in the DB.
  • Custom-metadata-type relationships: __r returns null when the FK is null (instead of an empty SObject); EntityDefinition / FieldDefinition FKs resolve end-to-end; multi-line MDT SELECT parses cleanly; dynamic __mdt queries count toward Limits.getQueries(); SELECT COUNT() on __mdt returns Integer.
  • DML semantics: AFTER-INSERT / AFTER-DELETE addError() now rolls the save back; partial-success Database.delete / update respects per-record addError; partial-success unique-constraint violations become failed SaveResults; heterogeneous List<SObject> updates dispatch per-type; Database.merge(master, Id) properly deletes the duplicate; required-field validation on custom __c insert; restricted picklist values enforced.
  • Trigger correctness: trigger-throw DmlException preserves the original stack; trigger body pushes a stack frame so traces name the trigger; before-insert mutations to Name don't leak back to the caller's local; the Trigger.new copy barrier matches Salesforce.
  • Exception handling: stack trace preserved on re-throw of caught exceptions; NPE message format matches Salesforce's bare format; user toString returning Apex null stringifies as "null"; exception toString matches the Type:[]: msg shape.
  • Type system: Date / Datetime / Time casts throw TypeException for incompatible sources; Decimal preserves explicit scale from setScale; List and Set match instanceof Iterable<T>; super() chain terminates cleanly at built-in Exception; Type.forName for List / Set / Map and primitives; Set<T>.add / contains / remove honor user hashCode / equals; subclass static method doesn't shadow ancestor instance method; inner-class shadowing of standard SObjects is now scoped.
  • JSON: deserialize handles Base64 fields, numeric-offset datetime formats, single-key SObject wrappers, and enum strings, and resolves user-class field types; Map.serialize preserves insertion order and original-case keys.
  • Async / scheduling: System.schedule returns the CronTrigger Id (08e), not the AsyncApexJob Id, and rejects invalid day-of-month/week combos; SchedulableContext.getTriggerId wired up; EventBus.publish increments getDmlRows alongside getDmlStatements and stamps CreatedById / CreatedDate.
  • Profiles & permissions: Minimum-Access profile requires explicit FLS for edit/create; restricted profiles with default CRUD get synthesized grants; with-sharing DML honors record ownership; setup-object auto-grant skipped for minimum-access.
  • SOQL miscellany: child subquery LIMIT honored; subquery LIMIT :bindVar resolves at execute time; aggregate ORDER BY translates SOQL date functions (CALENDAR_YEAR, etc.); trailing AND / OR trimmed cleanly; LAST_N_* / NEXT_N_* / N_X_AGO use calendar boundaries instead of rolling windows; child subquery rows always include Id; parent-relationship SObjects carry QueriedFields from the SELECT, so untouched fields throw on access (matching real Salesforce); :true / :false / :null bind syntax resolves to literal constants.
  • Decimal.valueOf(Integer) returns the value instead of 0.
  • Schema casing: standard fields get English labels; custom SObject names preserve API casing; getPopulatedFieldsAsMap returns PascalCase keys.
April 17, 2026
0.1.19
Added
  • Nimbus Language Server (LSP 3.17) on stdio. nimbus lsp starts a server every LSP-aware editor can launch — VSCode, JetBrains, Neovim, Zed, Helix, Emacs. 20 advertised capabilities: schema-backed completion (71+ real Account fields from .nimbus/schemas/, no org round-trip), live SOQL column validation, hover with mutation-score and flaky-rate overlays, signature help, go-to-definition / typeDefinition / implementation / declaration, find-all-references, rename, workspace symbol search, document symbols, document highlight, folding and selection ranges, semantic tokens, call hierarchy, code actions (SOQL typo auto-fix, generate test stub with schema-aware required-field constructors, extract-to-@TestSetup, add-@IsTest), inlay hints (coverage hit counts, surviving mutants, flaky warnings), code lenses (Run / Debug / Coverage / View Trace / Mutate), opinionated conservative formatter, 12 Apex snippets with Tab-through placeholders. See the Language Server page for per-editor setup.
  • VSCode extension auto-launches the LSP when the extension activates; gated on nimbus.lsp.enabled (default on). Disables the extension's legacy code-lens provider when LSP is active so Run Test / Debug / Coverage buttons don't double up.
  • New CLI flags on nimbus test: --dry-run / -n, --sandbox, --feature MultiCurrency,PersonAccounts, --faketime 2030-01-15T12:00:00Z, --timeout N, --readonly, --profile ci. All seven also available as nimbus.properties keys under nimbus.org.* and nimbus.test.* with CLI winning when both are set. --profile is a persistent root flag — activates Quarkus-style %ci.nimbus.foo=bar profile overrides for every subcommand in the same invocation.
  • nimbus stub list / add / remove / path scaffolds plain-.cls stubs under stubs/ with --namespace and repeatable --method name:returnType options. Each generated stub exposes callCount and calls static counters so assertions are a one-liner.
  • nimbus config show now loads on demand when invoked outside a test run instead of erroring configuration not loaded.
  • TUI subcommands (nimbus history / analytics / schema / trace) fall back to JSON / text when stdout is piped, so CI pipelines no longer crash on could not open a new TTY.
Fixed
  • Decimal.valueOf(Integer) returns the value instead of 0. Three dispatch sites (type.go:118, methods.go:7110, methods.go:8287) now accept Integer, Decimal, String, Boolean, and Null operands. Integer.valueOf(Decimal) extended symmetrically.
  • Empty string literal no longer equals null in comparisons. String x = ''; x == null now correctly returns false per the Apex spec — matched against real Salesforce behaviour. The DML round-trip behaviour (text fields stored as NULL) is preserved separately via dmlValueToInterface.
  • Inline :new Set<Id>{a.Id, b.Id} / :new List<Integer>{1, 2} bind variables now parse and evaluate inside SOQL literals. The previous SQLSTATE 42601 leak on WHERE Id IN :new Set<Id>{...} is gone; the parser's bind-variable scanner and a new resolveInlineNewBinds pre-pass rewrite inline constructors into literal IN lists before translation.
  • <required>true</required> on custom __c fields is now enforced on DML insert. Skips fields with a DefaultValue, formula fields, and master-detail parents (handled elsewhere). Standard objects keep the existing business-field whitelist to avoid false positives on system fields like OwnerId.
  • SOQL errors no longer leak raw PostgreSQL SQLSTATE codes. The new apexifySOQLError helper maps syntax error at or near … to unexpected token: …, column does not exist to No such column on entity: …, and wraps everything in System.QueryException with a Salesforce-truthful message.
  • Test method name casing preserved in runner output and LSP responses. GetTestMethods and the runner's cached registry now use methodDef.Name instead of the lowercased map key, so testReverse stops showing as testreverse.
  • nimbus.test.parallel and profile overrides now actually work end-to-end. The --parallel flag defaulted to runtime.NumCPU(), which is non-zero, so every downstream caller thought it was explicitly set and the config value never got consulted. Fix: check cmd.Flags().Changed("parallel") before trusting the flag. Profile overrides (%ci.nimbus.test.parallel=1 + NIMBUS_PROFILE=ci) now visibly drop worker count.
  • UserInfo.getLocale() / getLanguage() / getDefaultCurrency() and Organization.IsSandbox now read from the configured nimbus.org.* keys instead of being hardcoded to en_US / USD / false. UserInfo.isMultiCurrencyOrganization() observes the MultiCurrency feature toggle.
  • Record-triggered flow execution: flows are now actually fired on DML (previously they were parsed and counted but never run). Test.enableRecordTriggeredFlows path also works. (Historical note: 0.1.19 initially gated before-save $Record writes behind nimbus.compat.flow-record-sync; that flag was removed in 0.1.20 and the Salesforce-truthful ordering is now unconditional.)
  • nimbus test and nimbus exec build every interpreter with the resolved OrgConfig installed, so currency / locale / sandbox / features / faketime / readonly propagate through triggers, async jobs, and @testSetup blocks uniformly.
April 16, 2026
0.1.18
Changed
  • VSCode extension icon: slashed-N brand mark (replaces the previous cloud silhouette) for better contrast on both light and dark Activity Bar themes.
April 16, 2026
0.1.17
Added
  • Daemon now picks up brand-new custom objects and custom fields live. Adding objects/Foo__c/fields/Bar__c.field-meta.xml (or a whole new Foo__c/ directory) while the daemon is running propagates to Postgres as CREATE TABLE / ALTER TABLE ADD COLUMN automatically — no nimbus sync required. Verified end-to-end with an editor-simulated live-add scenario.
  • Daemon watches sfdx-project.json. Adding a new package directory (e.g. force-app-secondary/) is picked up on save: the new dir joins the watch set, its Apex files are pre-parsed, and the metadata provider is rebuilt.
  • Daemon invalidates custom-metadata-type discovery on .md-meta.xml changes and on new __mdt object definitions. New MDT types no longer require a daemon restart.
  • nimbus test without the daemon now also reconciles the Postgres schema on every run via the same idempotent ApplySourceDelta. Users on the free tier get live field/object pickup too.
Fixed
  • File watcher now covers intermediate directories like force-app/main/default/objects/. Previously only terminal dirs with watched files were registered, so a newly-created child directory (objects/Foo__c/) fired no fsnotify event and the new object was invisible until daemon restart.
  • New directories created by atomic mkdir -p are now watched unconditionally. Empty fields/ subdirs no longer slip through the selective startup scan and miss subsequent field-meta.xml creations.
  • Source scan now records every parsed field, not just formulas/picklists/rollups/defaults. Newly-added regular Text/Number fields flow into the interpreter's SchemaProvider without a re-sync, so insert new Foo__c(NewField__c = …) stops erroring with "Field not found in schema".
  • Added a 150ms settle delay before the watcher re-scans on CREATE events. fsnotify delivers CREATE before the writer flushes, which was leaving parseCustomField with an empty file and dropping new fields from the scan.
  • DB-seeded cache hash now covers every seed-affecting CLI flag: --seed-records, --seed-sites, --seed-list-views, --org-defaults, --default-record-types, --seed-record-excludes. Changing any of them correctly forces a re-seed instead of silently reusing stale rows from the previous run.
April 16, 2026
0.1.16
Added
  • EventBus.publish now routes against the correct governor counter based on the event's publishBehavior metadata. PublishImmediately events increment Limits.getPublishImmediateDml(); PublishAfterCommit events and generic List<SObject> payloads increment Limits.getDmlStatements(). Verified on a real org with a cross-matrix test.
  • publishBehavior and eventType from platform-event object metadata are now parsed from object-meta.xml, round-tripped through .nimbus/schemas/*.json, and exposed via SchemaProvider.GetPublishBehavior. Requires a fresh nimbus sync.
  • Per-worker VariableTracker is now mutex-guarded. Timed-out test goroutines can no longer race the main goroutine reading GetExecutedLines, eliminating a class of "concurrent map iteration and map write" crashes near the end of long suites.
Fixed
  • Test.stopTest() now resets governor limits between queue-drain passes. A subscriber that re-publishes events when hitting the callout cap (the Onfido "retry-later" pattern) no longer loops forever — each pass models a separate async transaction with fresh limits, matching real Salesforce behaviour.
  • nimbus sync now invalidates the daemon's "DB already seeded" flag so the next test run re-seeds standard profiles and the mock user. Previously the daemon kept the flag true, the post-sync test suite skipped seeding, and every @testSetup that touched Profile/User failed with Attempt to de-reference a null object: field Name.
  • Site and SiteDetail seed rows from nimbus.seed.site.<Name>=<secureUrl> now land after ClearAllTables instead of before. The earlier race let TRUNCATE CASCADE wipe the sites a few ms after they were inserted.
  • Before-insert triggers that mutate Name on the trigger copy no longer leak the mutation back to the caller's local variable. The copy barrier now matches Salesforce: auto-populated Names still propagate, but trigger-renamed Names do not.
  • Child subquery handling now prefers the declared element type over the first-element type when dispatching EventBus.publish. A List<SObject> generic list stays on the regular DML counter even when filled with PublishImmediately events.
  • Identifier.Lower lazy cache is now stored via atomic.Pointer[string]. The previous plain-string assignment was not atomic (two-word header) and concurrent AST walks occasionally observed a torn value, crashing deep inside map access.
  • Attachment.Name is no longer hidden by objectsWithoutNameField. Attachment has Name (the file name) in real Salesforce; hiding it broke every test that set Name on Attachment in @testSetup.
  • stripInaccessible and SOQL user-mode access checks now treat Name as implicitly readable when the user has object-level read but no explicit FLS grant. The previous stricter rule over-stripped Name for standard users.
  • lookupRecordById now quotes table and column identifiers. Tables whose API name is a PostgreSQL reserved word (e.g. User) no longer cause SELECT * FROM user WHERE id = $1 to resolve user as CURRENT_USER and abort the transaction with "column id does not exist".
  • ALTER TABLE add-missing-column retry in Insert and BatchInsert is now wrapped in savepoints. Under parallel workers, a lock-timed-out ALTER used to leave the transaction aborted and cascade failures deep in the trigger chain.
  • Preserved static fields (externalStaticWrites) are now snapshotted to their initial values so cross-test reset restores the exact pre-test state instead of leaking mutations from previous tests.
April 12, 2026
0.1.15
Added
  • Dom.Document and Dom.XmlNode are now fully implemented with real XML parsing, serialization, attribute handling, and namespace support. Methods include load, getRootElement, createRootElement, toXmlString, and the full XmlNode API.
  • Compression.compress and Compression.decompress for Zlib blob encoding.
  • Flow.Interview.start() now actually executes the flow via the flow executor. Input variables (including Apex class instances) are preserved through the round-trip so getVariableValue() returns them correctly.
  • transient keyword support: fields and properties marked transient are now excluded from JSON.serialize and JSON.serializePretty output, matching Salesforce behaviour.
  • Assert.isInstanceOfType now checks implemented interfaces on ObjectValue, not just the inheritance chain. A class implementing System.Schedulable is now correctly recognised as an instance of it.
  • Automated Process user (autoproc@<OrgId>) is now seeded with no profile, matching Salesforce's system user.
  • OrgLimits.getMap() registered as a builtin so it resolves via the standard static-method path.
  • Dynamic SOQL via Database.query() now checks system object mocks (Organization, UserRole) before hitting the database.
Fixed
  • Profile SOQL queries are now fully mocked at the interpreter level for all query shapes: literal Name = 'X', bind variables (Name = :profileName), Name IN (...), and by-Id lookups. This eliminates stale isdeleted state from the daemon's cached database.
  • Schema.SObjectType.isAccessible(), isCreateable(), isUpdateable(), and isDeletable() now delegate to CheckObjectPermission instead of always returning true.
  • HttpResponse.getHeaderKeys() now returns keys in sorted order for deterministic output.
  • Profile seed SQL now resets isdeleted = FALSE on conflict, preventing stale soft-delete state.
  • checkRestrictedProfile uses the seeded profile ID lookup table, avoiding raw SQL queries against a potentially stale profile table.
  • DML insert now silently skips null elements in List<SObject> instead of throwing "Insert requires SObject records".
April 12, 2026
0.1.14
Added
  • Database.upsert now honours the external Id field argument for matching existing records.
  • System.Quiddity enum members are now fully enumerated. Returns RUNTEST_SYNC inside a test context.
  • ApexTrigger metadata rows are now seeded alongside ApexClass rows at startup.
  • SOQL FIELDS(ALL), FIELDS(STANDARD), and FIELDS(CUSTOM) expansion is now supported.
  • OrgLimits.getMap() now includes SingleEmail and MassEmail entries.
Fixed
  • Postfix ++ and -- on member fields accessed through a static property getter now actually increment.
  • Test.stopTest() now drains platform events published by async jobs during execution.
  • Partial-success Database.insert and Database.update now validate record Ids correctly.
  • User.Name is now a PostgreSQL generated column that survives schema autoSync.
  • Hierarchy custom settings getOrgDefaults() now populates SetupOwnerId with the org Id.
  • System exception classes are now recognised as subclasses of System.Exception during overload resolution.
  • Set and List now match System.Iterable parameters in addition to bare Iterable.
  • Default Object.toString() fallback for custom class instances.
  • BatchInsert now skips generated columns, matching the single-row Insert path.
  • Governor counters for callouts, publish-immediate DML, and email invocations now increment correctly.
April 11, 2026
0.1.13
Added
  • Regex patterns with lookbehind ((?<!...), (?<=...)) and lookahead ((?!...), (?=...)) now compile and run. Go's stdlib regexp uses RE2 and rejects lookaround; nimbus now transparently falls back to a .NET-compatible engine when RE2 refuses the pattern. String.replaceAll, String.replaceFirst, and the full System.Pattern / System.Matcher API all route through the wrapper, so data-masking rules, SSN scrubbers, and anything else using advanced regex now behaves like real Apex.
  • Cross-class dependency-injection patterns from test class static blocks now survive test isolation. When a test class's static { ... } block either directly writes another class's @TestVisible static field (MyService.client = new MockClient();) or triggers a side-effect via a helper method (MyService.useMocks();), nimbus records the resulting overrides and preserves them across the per-test-method reset. Previously the next access would lazily re-run the field initializer and silently swap the mock back to the production implementation, so any test that depended on the static-block mock would see real service behaviour and fail in surprising ways.
Fixed
  • SOQL date literals (TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH, THIS_QUARTER, THIS_YEAR, and the LAST_/NEXT_ variants) are now translated as half-open ranges instead of point comparisons. A query like WHERE CreatedDate = THIS_WEEK used to generate createddate = date_trunc('week', NOW()), which only matched records created exactly at midnight Monday; it now generates a proper createddate >= start AND createddate < end range. Every comparison operator (=, !=, <, >, <=, >=) respects Salesforce's boundary semantics. Week ranges start on Sunday to match Date.toStartOfWeek() on US English locales.
  • Regex replacement backreferences ($1, $2, …) now work correctly when the capture group number is followed by literal letters or digits. A replacement like '$1XXX-XX-$4' used to expand to an empty string because Go's stdlib regexp greedily consumed 1XXX as a named group; nimbus now translates Java/Apex replacement syntax to Go's unambiguous ${N} form so SSN/credit-card formatters that embed literal text next to backref numbers produce the expected output.
  • Matcher.replaceFirst() now expands group references in the replacement string. Previously it returned the replacement text verbatim without ever applying the captured groups, so a pattern (\d+) with replacement '[$1]' on the input 'abc 42 xyz' returned 'abc [$1] xyz' instead of 'abc [42] xyz'.
  • Aggregate SOQL queries with multi-level relationship chains in SELECT or GROUP BY now translate to valid SQL. A query like SELECT LogEntry__r.Log__r.Status__c, COUNT(Id) FROM LogEntryTag__c GROUP BY LogEntry__r.Log__r.Status__c used to emit three separately-quoted identifiers ("logentry__r"."log__r"."status__c") which PostgreSQL rejected with "missing FROM-clause entry for table log__r". The aggregate translator now builds the chained JOIN alias the same way the regular translator does.
  • ORDER BY with multi-level relationship fields now resolves to the correct chained JOIN alias. ORDER BY LogEntry__r.Log__r.LogRetentionDate__c used to emit nonsense column references for anything past the first hop; the translator now walks the full dotted path exactly like the SELECT clause does.
  • JSON.deserialize(..., Database.SaveResult.class) (and the UpsertResult, DeleteResult, UndeleteResult, Error variants) now reconstructs a typed object instead of a bare Map. Overload resolution against Database.Error parameters scored -1 when tests built mock results through serialize/deserialize round-trips; the typed path makes round-trip mocks behave the way integration code expects.
  • Schema.LogEntryArchive__b-style Big Object custom objects are now loaded from project metadata. The schema scanner's allow-list filtered directories by suffix (__c, __mdt, __e) and silently dropped __b; Schema.MyBigObject__b.SObjectType.getDescribe().fields.getMap() used to return an empty map and cascade into null-deref chains. (Schema cache is keyed per project — a first run after upgrading will re-scan and pick them up.)
  • SOQL ALL ROWS is now stripped during translation instead of forwarded to PostgreSQL as a literal identifier. Queries that wanted to include soft-deleted rows used to fail with syntax error near "ALL".
  • SELECT and WHERE clauses that use the .Type discriminator on a polymorphic relationship (Owner.Type, What.Type, Who.Type, LinkedEntity.Type, RelatedTo.Type) now project NULL under an alias instead of trying to join against a non-existent type column on the target table. Full polymorphic dispatch still isn't implemented, but queries that reference .Type for logging or filtering now run instead of erroring.
  • Bind variables written as bare method calls (:getNamespacePrefix(), no receiver) now consume the trailing parentheses instead of leaving them dangling in the translated SQL. Previously the translator emitted nonsense like ... IS NULL() when the method returned null.
  • Datetime.valueOf(Long) now constructs a datetime from a Unix-millisecond epoch, matching Salesforce's documented overload. Nimbus already handled the string overload; the Long overload silently returned null, so any code that round-tripped timestamps through Long.valueOf(epochString) ended up with null datetimes.
  • SObject.getPopulatedFieldsAsMap() now excludes fields whose value is null. Apex's real behaviour returns only populated (explicitly-set) fields, so tests that iterate the map and skip based on containsKey now observe the same contract nimbus uses for serialization.
  • Custom objects with a Name field of type Text are now auto-populated with the record Id on insert when the caller leaves Name null or empty. Salesforce performs this auto-fill on text Name fields for Master-Detail/Lookup children so rec.Name is never bare null after insert; nimbus mirrors the behaviour.
  • Formula-field resolution now skips rollup summary fields. Rollups stash their metadata in the Formula slot as a summary:op:field string (historical hack), and the formula evaluator was parsing that as Apex, returning null, and silently clobbering the correct aggregate value that the rollup pipeline had just computed.
  • Schema.DescribeFieldResult.getPrecision() and getScale() now read the field's declared metadata instead of returning hardcoded 18 / 2. Number fields with precision/scale populated via (3.14).setScale(fieldDescribe.getScale()) used to round to 2 digits regardless of the target column type.
  • getLength() on any Id field now unconditionally returns 18 regardless of whether the containing SObject ships with a JSON describe. Mock-id generators that build ids as keyPrefix + zeros(length - prefix - suffix) + suffix used to produce 255-character strings for SObjects without a describe (CronTrigger, AsyncApexJob), overflowing downstream Text(18) fields.
  • Schema.X prefixed references (e.g. Schema.AuthSession, Schema.LoginHistory) now resolve to the real standard SObject even when a project has an inner class with the same short name. The previous shadow-check short-circuit picked the inner class, so Schema.AuthSession.SObjectType.getDescribe() failed with "Method AuthSession.getDescribe not found" on any codebase that ships a fixture called AuthSession.
  • Test.setMock(HttpCalloutMock.class, mock) and Test.setMock(System.HttpCalloutMock.class, mock) now land under the same registration key. Previously the qualified form was stored under system.httpcalloutmock while Http.send looked up httpcalloutmock, so fully-qualified registration silently did nothing.
  • SOQL WHERE ... IN (SELECT ...) subqueries now keep only the first projected column. The translator's JOIN FK / parent-id auto-injection produced multi-column subqueries that PostgreSQL rejected; the new keepFirstSelectColumn rewrite fixes fflib selector patterns that embed subqueries.
  • System.ParentJobResult.SUCCESS (and the other enum members) now resolve correctly when the project also defines a class named ParentJobResult. The enum path now accepts both the System.-qualified form and the bare form after a namespace strip.
  • System.Flow is now recognised as a system class, so Flow.Interview and related APIs no longer fall through the auto-null stub path. Also added a generic TypeValue method dispatch so registered builtins under keyed names (Flow.Interview.createInterview, Cache.Org.getPartition, …) are found without an inline dispatcher stanza.
  • Added the ApexEmailNotification standard schema so SOQL queries that reference it no longer fail with "relation does not exist".
April 11, 2026
0.1.12
Fixed
  • System.LoggingLevel ordinals now match Salesforce's declaration order. The values are NONE, INTERNAL, FINEST, FINER, FINE, DEBUG, INFO, WARN, ERROR with ERROR as the highest (most severe, least verbose). Any logging framework that compares entries against the user's configured threshold via entry.ordinal() >= user.ordinal() now filters correctly instead of logging everything regardless of the threshold.
  • switch on null now correctly matches only a literal when null clause. Previously nimbus tried to evaluate every when label as an identifier even when the switch value was null, so a statement like switch on partitionType { when ORGANIZATION ... when SESSION ... } would throw Unknown identifier: SESSION whenever partitionType was null (e.g. from a mock delegate calling super(null, null)).
  • Child subqueries split across multiple lines are now detected. A formatted SOQL string of the form SELECT Id, (\n SELECT Id FROM Children__r\n ) FROM Parent__c used to silently drop the subquery, leaving the relationship field unpopulated and any parent.Children__r.size() call failing with Unknown method size on SObject Unknown.
  • Null-argument overload resolution now breaks ties using the declared type of the argument. Dispatching handler.execute(this.input.triggerNewMap) against overloads execute(List<SObject>) and execute(Map<Id, SObject>) now picks the map overload when triggerNewMap is null, instead of silently routing the call to the list overload and cascading into a null dereference.
  • Inner enum types declared inside a class (e.g. MyClass.MyEnum) now support valueOf(String) in addition to values(), including the standard NoSuchElementException on unknown names.
  • Overload resolution between sibling inner classes under the same outer type now matches correctly. A subclass OuterA.Mock that extends OuterB.Base is now recognised as a valid argument for a parameter typed Base, without the broader subclass-matching widening that caused regressions in earlier attempts.
  • Local variables now correctly shadow instance fields in assignments. A method that declared String conditionsLogic with the same name as a private String conditionsLogic field was silently writing to the field — the local was always returned empty. Apex scoping rules say the local wins, and nimbus now respects that.
  • Schema.DescribeFieldResult.getSoapType() returns the actual SOAP type of the field instead of hardcoded STRING. Field-type-driven comparators (e.g. choosing string vs decimal vs datetime comparison) now work on non-string fields.
  • Schema.DescribeFieldResult.getLength(), getPrecision(), and getScale() are now dispatched against the field's actual metadata instead of returning a single hardcoded default. Long text fields no longer get silently truncated because getLength() returned 255 for everything.
  • Cache.Visibility.NAMESPACE, ALL, and SESSION are now proper enum values. Calls to partition-delegate put(...) methods that accept a Cache.Visibility parameter now match the intended overload instead of failing with "No matching overload".
  • System.OrgLimits.getMap() now returns a map of stub System.OrgLimit objects so tests that iterate over governor limits run instead of throwing Method OrgLimits.getMap not found.
  • Type.forName(String) and Type.newInstance() now prefer a real SObject type over any SFDX-generated stub class of the same name. Tests that did Type.forName('Account').newInstance() used to silently return an empty ObjectValue for the Account shadow class.
  • SObject arguments now match parameters typed Schema.X (and vice versa). Nimbus already treated Schema.Account and Account as equivalent for type references; overload resolution now also honours that equivalence, fixing dispatch for framework methods declared as foo(Schema.User u).
  • Number fields declared with precision/scale in metadata (stored as bigint in the local schema) now map to the Double display type and the Double SOAP type. Tests that populate numeric fields via reflection no longer end up with the field name as a string in the column.
  • Max/min rollup summary defaults now respect the target field's type: they default to null instead of "0", so datetime rollups no longer throw "date/time field value out of range" when no child records exist.
  • Added standard schemas for AuthSession, LoginHistory, and Network, so projects that query session/network metadata during test setup no longer hit "table does not exist".
  • System.EventBus.publish() now dispatches correctly when called on a TypeValue reference. Publishing a platform event from a helper that resolves System.EventBus dynamically used to miss the method.
  • Static property getters no longer recurse when the getter body calls back into the same class via this. The inside-getter raw-value short-circuit now distinguishes instance-field reads from StaticVars reads and correctly returns the freshly-assigned value.
April 10, 2026
0.1.11
Fixed
  • Limits.getDMLStatements() now reflects DML done through Database.insert, Database.update, Database.delete, and Database.upsert. The counter used to only track the bare-statement form (insert records;), so libraries that route every write through Database.* method calls — fflib unit-of-work patterns, builder libraries — saw getDMLStatements() return 0 after a commit and any assertion against the counter failed.
  • Eliminated a duplicate "Standard User" Profile row in the seeded DB. The runner used to insert the configurable mock user profile at 00e000000000001AAA with the default name "Standard User" and seed the standard "Standard User" profile at 00e000000000003AAA, so queries like SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1 returned two rows and threw "List has more than 1 row for assignment to SObject". The mock user now points at whichever seeded standard profile matches its configured name; only custom names create a bespoke row. UserInfo.getProfileId() looks up the mock user's profile from the DB instead of returning a stale hard-coded id.
April 10, 2026
0.1.10
Fixed
  • Higher -p worker counts no longer get slower than lower ones. Running with -p 50 on a large suite used to be slower than -p 4 because workers serialized on shared locks during result reporting and metadata lookups. The result-reporting path and the metadata provider are now structured so workers can actually run in parallel.
April 10, 2026
0.1.9
Performance
  • Another round of interpreter hot-path tuning. Test runs on large suites are noticeably faster and use less CPU, with no changes to behavior or configuration.
April 10, 2026
0.1.8
Performance
  • Large cut in the number of DB round-trips the interpreter issues during test runs. On a 2,300-test production suite, total queries drop by ~75% (≈223k → 36k) and query time drops by ~70%. Repeatedly-fetched rows — custom settings, owner/parent records referenced from cross-object formulas, UserInfo.getUserId() fallbacks — are now cached per test class and invalidated on DML against the same row. Wall-clock savings depend on your DB backend: on the embedded unix-socket postgres each round-trip is already cheap, but CI runners, Docker-on-Mac, and remote postgres setups should see measurable run-time drops.
  • Interpreter hot-path cleanup. Formula builtins are now shared at package level instead of rebuilt per evaluator, parsed formulas are cached by source text, member-access metadata is cached on the AST node, and an AST-level negative cache short-circuits identifier resolution for names that are clearly not builtins.
Fixed
  • Cross-object formulas of the shape Owner:User.FirstName & " " & Owner:User.LastName (or anything richer than a single Rel:Type.Field reference) no longer fall into a fast-path that extracted nonsense table names and fired silently-failing SELECTs against imaginary tables. These formulas now route to the normal formula evaluator and return correct values.
April 9, 2026
0.1.7
Performance
  • Interpreter is ~9% faster on large test suites. No behavior changes, no flags to flip — just upgrade and runs are shorter.
April 9, 2026
0.1.6
New
  • PostgreSQL is now embedded directly in the Nimbus binary on all platforms — macOS, Linux, and Windows. No more first-run network fetch; Nimbus works offline from the first invocation. The bundle is stripped to the minimum required (three binaries, two plugins instead of 90, only libraries postgres dynamically links against) and compressed with zstd. Per-platform bundle: 14–16 MB. Shared cache in ~/.nimbus/pg/: ~55 MB (down from 132 MB per-project).
  • Stripped plugin set is measurably faster at runtime. On large production test suites, warm runs are ~15% faster compared to the unstripped distribution because postgres scans lib/postgresql/ during startup and catalog initialization — fewer plugins, less work.
Performance
  • PostgreSQL now communicates over a per-project unix domain socket on macOS and Linux instead of TCP loopback. Skipping the kernel TCP stack on every round-trip is worth ~10% wall-clock on warm runs of large suites. Windows continues to use localhost TCP (no unix socket support).
  • Migrated the database driver from lib/pq to pgx/v5. Binary protocol, automatic prepared statement caching, and active maintenance.
  • Aggressive PostgreSQL server tuning for the test workload: UNLOGGED tables, autovacuum=off, jit=off, larger shared_buffers / work_mem, and several activity-tracking knobs disabled. Cumulative speedup of 10–20% on a 2,300-test production suite.
April 9, 2026
0.1.5
New
  • Curl-based install script — curl https://install.testnimbus.dev | sh installs the latest binary without Homebrew.
Fixed
  • SOQL queries without an explicit ORDER BY clause now default to ORDER BY id, ensuring consistent, deterministic result ordering across all test runs.
  • VSCode extension metadata corrected — publisher, display name, and category fields updated.
  • Install script archive filename no longer includes a leading v prefix.
April 9, 2026
0.1.4
Changed
  • Background daemon (nimbus daemon) is now available on the free tier. Previously gated behind Pro to enable VSCode extension for free users
  • Database pre-warming - the startup optimization that keeps the embedded database warm between runs - remains a Pro feature.
April 9, 2026
0.1.3
Fixed
  • Embedded PostgreSQL server logs now redirect to .nimbus/db/postgres.log instead of being written to stdout, eliminating log noise in terminal output.
April 9, 2026
0.1.2
Fixed
  • Share tables (e.g., AccountShare, LeadShare, OpportunityShare) are now included in schema auto-sync.
April 8, 2026
0.1.1
Fixed
  • Free tier now automatically runs with 1 worker. Previously, parallel workers were always blocked on the free tier but users had to manually pass --workers 1 on every run.
  • Missing schemas are now detected and automatically synced at startup instead of failing silently.
  • Machine ID is now correctly recovered after a reinstall, preventing license validation failures on the same machine.
April 8, 2026
0.1.0
Initial
Initial release
  • Full Apex interpreter — classes, interfaces, enums, abstract classes, inheritance, generics, method overloading, exception handling.
  • Before and after triggers with complete context: Trigger.new, Trigger.old, Trigger.newMap, Trigger.oldMap, and all boolean flags.
  • Record-triggered flows, autolaunched flows, subflows, platform event flows, decisions, loops, formulas, collection processors.
  • SOQL to SQL translation with bind variables, relationship queries, aggregate functions, subqueries, and LIMIT/OFFSET.
  • DML operations: insert, update, delete, upsert, undelete — all fire triggers and roll back per-test.
  • fflib / ApexMocks support — stub API, argument captors, and verify/when patterns.
  • Background daemon (nimbus daemon start) for warm-start performance — avoids full cold-start on every run.
  • Coverage reporting: HTML, JSON, and Cobertura XML output formats.
  • Debugger with breakpoints, step in, step over, step out, and variable inspection.
  • Dev UI (nimbus dev) — browser-based dashboard with live test results, schema explorer, and anonymous Apex execution.
  • Watch mode (nimbus test:watch) — re-runs affected tests on file save.
  • Traces & Analytics: structured execution traces with 5 verbosity levels (method calls, SOQL, DML, triggers, timing).
  • Governor limit tracking — SOQL row counts, DML statement counts, heap, and CPU measurements.
  • Mutation testing (Pro) — automatically mutates code and verifies tests catch each change.
  • Benchmarking (Pro) — compare performance across branches or over time.
  • Auto schema sync from a connected Salesforce org.
  • VS Code extension: CodeLens run/debug buttons, Test Explorer integration, coverage gutter icons, SOQL preview, governor limit annotations, and trace viewer.
  • Install via Homebrew (macOS/Linux), Scoop (Windows), and direct binary download.