<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: René Knierim</title>
    <description>The latest articles on DEV Community by René Knierim (@oblitus).</description>
    <link>https://dev.to/oblitus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3987897%2F478c030f-4c47-4de1-82ad-74815f1d4f09.png</url>
      <title>DEV Community: René Knierim</title>
      <link>https://dev.to/oblitus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oblitus"/>
    <language>en</language>
    <item>
      <title>Rehearse a Salesforce data load before you load it</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Thu, 29 Oct 2026 19:56:57 +0000</pubDate>
      <link>https://dev.to/oblitus/rehearse-a-salesforce-data-load-before-you-load-it-5chf</link>
      <guid>https://dev.to/oblitus/rehearse-a-salesforce-data-load-before-you-load-it-5chf</guid>
      <description>&lt;p&gt;&lt;em&gt;Nimbus was renamed renmy runtime in October 2026; the record of the rename is at &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;https://renmy.net/blog/nimbus-is-now-renmy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; A Salesforce data load is a deploy in disguise: every row runs the org's triggers, validation rules and flows, and you find out what they did after the rows are in. renmy runtime is a local Salesforce Apex runtime that runs the same automation on your own computer, against a small database it brings with it, with no Salesforce org, scratch org or sandbox. The Data Loader in its JetBrains plugin, &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt;, maps a CSV onto an object, and Simulate runs every row through those triggers, flows and validation rules in a transaction that is rolled back, so you see which rows would be refused, and why, before any reach the org. Upload to Org then sends the same file to the org you pick. The real load still happens in the org; the rehearsal is local, free and repeats in seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why a Salesforce data load is a deploy
&lt;/h2&gt;

&lt;p&gt;Picture the file: 5,000 invoices from the old system, cleaned up in a spreadsheet, saved as a CSV. You map the columns, press go and watch the counter. When it stops, the error file holds 300 rows the org refused, and each of the 4,700 it accepted fired a flow that created a task nobody wanted.&lt;/p&gt;

&lt;p&gt;Nothing went wrong with the tool. A data load does what a user does when they save a record, five thousand times, without pausing. For every row the org runs the automation built up over the years, in a fixed order: before-save flows, before triggers, validation rules, duplicate rules, the save, after triggers, assignment rules, workflow rules, processes and after-save flows. The row that fails comes back as a line in an error file. The row that passed but did something you did not intend is harder to find, because nothing reported it.&lt;/p&gt;

&lt;p&gt;A developer who changes a trigger gets a validation step before the change reaches production. A data load has never had one: the first time your file meets the org's automation is the real run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; admins who load and update data, migration leads moving or consolidating orgs, and anyone who has opened an error CSV after a load and wished they had seen it first. No terminal, no code. IntelliJ IDEA Community is free, and the plugin installs the runtime for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a local runtime is, in plain words
&lt;/h2&gt;

&lt;p&gt;Apex, triggers, validation rules and flows run inside Salesforce, on Salesforce's servers. That is why every change is deployed and every test waits for the org. A local runtime is a copy of that engine that runs on your own computer instead.&lt;/p&gt;

&lt;p&gt;renmy runtime is one. It executes real Apex (classes and triggers), SOQL, DML and Salesforce Flows (record-triggered, autolaunched and screen flows) against an embedded PostgreSQL, a small database that ships inside the program that you never install or manage. No Salesforce org, no scratch org, no sandbox, no Docker, no JVM: one program for macOS, Linux and Windows. A Java or JavaScript developer who presses "run test" has always had this; Salesforce developers ran Apex tests in an org and waited. &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;What is a local Apex runtime?&lt;/a&gt; explains the idea from zero.&lt;/p&gt;

&lt;p&gt;It is not a mock or a guess. Governor limits are enforced. Triggers and flows fire in the platform's documented order of execution, and a flow fault arrives as the error Salesforce would raise: FIELD_CUSTOM_VALIDATION_EXCEPTION for a Custom Error element, CANNOT_EXECUTE_FLOW_TRIGGER for anything else (&lt;a href="https://renmy.net/flows" rel="noopener noreferrer"&gt;https://renmy.net/flows&lt;/a&gt;). Where the runtime matches the platform and where it does not is a public record at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;https://renmy.net/parity&lt;/a&gt;; read it rather than trusting any claim of a perfect match.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to rehearse a data load in IntelliJ IDEA
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install the plugin and open the project
&lt;/h3&gt;

&lt;p&gt;In IntelliJ IDEA, open Settings → Plugins → Marketplace and install &lt;strong&gt;Renmy Runtime - Salesforce IDE&lt;/strong&gt; (&lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide&lt;/a&gt;, version 1.15.1). IntelliJ IDEA Community, which is free, is enough. The Marketplace installs the one dependency, LSP4IJ; no JDK of your own is needed.&lt;/p&gt;

&lt;p&gt;Then open your Salesforce DX project: the folder with an &lt;code&gt;sfdx-project.json&lt;/code&gt; that your developers keep in version control. Ask them for a checkout; it holds the triggers, flows and validation rules the rehearsal will run. On first open the plugin installs the runtime into &lt;code&gt;~/.renmy/bin&lt;/code&gt; and starts it. The Free tier needs no account.&lt;/p&gt;

&lt;p&gt;If the org has automation the project does not, the rehearsal cannot run it. Drift, in the Org workspace, marks every file as in sync, differs, org only or local only, and Retrieve from Org fetches what the project lacks. Anything that talks to an org needs the Salesforce CLI; Authorize New Org… runs its browser login.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put the file on the Data Loader
&lt;/h3&gt;

&lt;p&gt;Open the Org workspace and its Data Loader tab. Drop the CSV on it, choose a file, paste rows, or right-click a file in the Project view and pick Use in Data Loader. Choose the operation: insert, update, upsert or delete.&lt;/p&gt;

&lt;p&gt;The object is read from the Id prefix of the key column and the columns are mapped onto fields. A cell that cannot be read lands in the Problems table before anything else happens. For &lt;code&gt;invoices-q3.csv&lt;/code&gt;, set to update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Id&lt;/strong&gt; → &lt;code&gt;Id&lt;/code&gt;, the key column; the prefix &lt;code&gt;a0B&lt;/code&gt; identifies &lt;code&gt;Invoice__c&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amount&lt;/strong&gt; → &lt;code&gt;Net_Total__c&lt;/code&gt;, matched by field label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status&lt;/strong&gt; → &lt;code&gt;Status__c&lt;/code&gt;, matched by API name; a picklist with 4 values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Due&lt;/strong&gt; → &lt;code&gt;Due_Date__c&lt;/code&gt;, matched by label, with a note: 3 cells are not a date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three bad dates are visible before anything has run.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Press Simulate
&lt;/h3&gt;

&lt;p&gt;Simulate runs every row through the local runtime in batches of 200, inside a transaction that is rolled back. The triggers, flows and validation rules in the project fire for each row, as the org's would. The report says what would change and which rows would be refused, and why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Simulate      200 rows · 1 batch · rolled back
197           would update
  3           refused by validation rule Amount_Positive:
              "Net total must be greater than zero"

Predicted Changes…   Problems…   Retry Failed Rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Predicted Changes lists what would change on each row, including what a before-save flow or a trigger writes on the way; Problems lists the refused rows with the platform's own message. The local database is left as it was.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Fix the file and simulate again
&lt;/h3&gt;

&lt;p&gt;Three invoices have a negative net total. Fix them in the spreadsheet, save, drop the file again, press Simulate. Each pass takes seconds, and the org has not been touched. Repeat until the report says zero refused and Predicted Changes shows what you meant.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Upload to Org
&lt;/h3&gt;

&lt;p&gt;Upload to Org sends the same file to the org you pick. The confirmation names the org and the row count, progress streams per batch, and the rows the org rejected can be exported and retried after you fix them. Anything in the plugin that writes to an org asks first and names the org.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rehearsal catches, and what it does not
&lt;/h2&gt;

&lt;p&gt;It catches cells that cannot be read as the field's type, validation rules with the message the user would have seen, triggers that throw, what triggers and before-save flows would write onto each row, and flow faults as the error the platform raises. It does not catch what is not on your machine: automation that exists only in the org is invisible until you retrieve it. For an update or upsert, the rows the file points at should exist locally too; Clone Org Data in the Workbench copies a slice of the org down for exactly that.&lt;/p&gt;

&lt;p&gt;This is the honest boundary. The rehearsal is local; the load goes to the org, and the org is the final authority. What it removes is every refusal the project's own automation would have produced, seen on your machine first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workbench: editing rows with the automation switched on
&lt;/h2&gt;

&lt;p&gt;The Data Loader is for files. The Workbench, in the Develop workspace, shows the local database one object at a time, in a grid, with a SOQL box above it. Editing a cell here is not a database update behind the automation's back. renmy runtime runs it as Apex DML on the same interpreter a test uses, so triggers fire, before-save flows run, and a validation rule refuses here what it would refuse in the org, with the platform's own message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;refused   INV-004209 · Net_Total__c · FIELD_CUSTOM_VALIDATION_EXCEPTION:
          Net total must be greater than zero (Amount_Positive)
          the row is unchanged · 1 DML statement · InvoiceTrigger before update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the Workbench the place to try one awkward record before you build a file of five thousand. Data from switches the grid to an org for reading, Pull into Local copies the selected rows down, and the Branch control keeps separate copies of the local data for separate pieces of work. Save as Fixture writes the rows in view as an &lt;code&gt;@IsTest&lt;/code&gt; data class, which is the hand-off to your developers: the records you found awkward become the records their tests use. &lt;a href="https://renmy.net/blog/apex-test-data-factories" rel="noopener noreferrer"&gt;Apex test data factories&lt;/a&gt; explains what they do with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I already have a sandbox. Why rehearse locally?
&lt;/h2&gt;

&lt;p&gt;Because a load into a sandbox is still a load, and if the result is wrong, undoing it is another load. The sandbox is shared, its data is whatever the last refresh and the last colleague left there, and a refresh is not something you do between two attempts at a file.&lt;/p&gt;

&lt;p&gt;The rehearsal rolls back. Each pass takes seconds, and nothing leaves your machine unless you point a panel at an org; source, schema and data stay local (&lt;a href="https://renmy.net/security" rel="noopener noreferrer"&gt;https://renmy.net/security&lt;/a&gt;). The sandbox keeps the job only it can do: the dress rehearsal with the org's full configuration, integrations and user interface, once the file is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;For an admin the path is the plugin: install &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt; in IntelliJ IDEA, open the Salesforce DX project, then Org → Data Loader. A developer who wants the same runtime in a shell needs one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Plugin: &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide&lt;/a&gt; and its product page, &lt;a href="https://renmy.net/jetbrains" rel="noopener noreferrer"&gt;https://renmy.net/jetbrains&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Quickstart: &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;https://renmy.net/quickstart&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;https://github.com/renmy-solutions/renmy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Slack: &lt;a href="https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a Salesforce org to simulate a data load?
&lt;/h3&gt;

&lt;p&gt;No. Simulate runs every row through renmy runtime on your own computer, in a transaction that is rolled back; no org is contacted. You need an org, with the Salesforce CLI logged in to it, only for Upload to Org, and the confirmation names that org and the row count before anything is sent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do triggers, flows and validation rules really run during the simulation?
&lt;/h3&gt;

&lt;p&gt;Yes. Every row runs through the local runtime in batches of 200, in the documented order of execution: before-save flows, before triggers, validation rules, duplicate rules, the save, after triggers, assignment rules, workflow rules, processes and after-save flows. Only what is in the project folder runs. The parity record at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;https://renmy.net/parity&lt;/a&gt; lists where the runtime matches the platform and where it does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the Data Loader free?
&lt;/h3&gt;

&lt;p&gt;Yes. The Data Loader with Simulate and Upload to Org, the Workbench, Schema and the SOQL and Apex consoles are in the Free tier of the plugin: $0, no card, no account, one machine. Pro, which adds debugger stepping, watch mode and mutation testing, is free during the public beta and lists at $19 a month afterwards. Pricing is at &lt;a href="https://renmy.net/#pricing" rel="noopener noreferrer"&gt;https://renmy.net/#pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I have to be a developer to use this?
&lt;/h3&gt;

&lt;p&gt;No. There is no code to write and no terminal to open. IntelliJ IDEA Community is free, and the plugin installs the runtime the first time you open a Salesforce DX project. You need a checkout of that project from your developers, since it holds the automation the rehearsal runs, and the Salesforce CLI for anything that talks to an org.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens to rows the org rejects during the upload?
&lt;/h3&gt;

&lt;p&gt;Progress streams per batch, and the rows the org rejected can be exported and retried once you have fixed them. A row the org refuses after a clean rehearsal usually points at automation the org has and the project does not; Drift marks it and Retrieve from Org fetches it before the next attempt.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>lowcode</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Onboarding Salesforce developers: day one, without an org</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Thu, 29 Oct 2026 19:55:32 +0000</pubDate>
      <link>https://dev.to/oblitus/onboarding-salesforce-developers-day-one-without-an-org-53jd</link>
      <guid>https://dev.to/oblitus/onboarding-salesforce-developers-day-one-without-an-org-53jd</guid>
      <description>&lt;p&gt;&lt;em&gt;renmy runtime was called Nimbus until October 2026. Same tool, same binary; the record of the rename is at &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;https://renmy.net/blog/nimbus-is-now-renmy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; Onboarding a Salesforce developer today means waiting for a sandbox, a user, a permission set and VPN access before the first test can run. renmy runtime, a local Salesforce Apex runtime, executes Apex, SOQL, DML and Salesforce Flows on the developer's machine against an embedded PostgreSQL, so a new hire can clone the repository, install one binary and run the entire Apex test suite on day one, with no org of any kind. The test environment is a &lt;code&gt;renmy.properties&lt;/code&gt; file committed to the repository, and the same &lt;code&gt;renmy test "*"&lt;/code&gt; runs in CI. Org access still matters for deployment, but it stops being what a new developer waits for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Team leads and engineering managers who onboard Salesforce developers: permanent hires, contractors, partner teams. You own the checklist that starts with "request sandbox access" and have watched it take a week. For the whole toolchain in one read, start with &lt;a href="https://renmy.net/blog/renmy-runtime-explained" rel="noopener noreferrer"&gt;renmy runtime explained&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does day one look like for a Salesforce developer today?
&lt;/h2&gt;

&lt;p&gt;The checklist most teams run has the same shape, and none of it is in the new developer's hands:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request a sandbox, or a seat in the shared one. An admin has to act.&lt;/li&gt;
&lt;li&gt;Request a user in it, with the right profile and permission sets. Another admin, another day.&lt;/li&gt;
&lt;li&gt;Get on the VPN, authorise the Salesforce CLI.&lt;/li&gt;
&lt;li&gt;Push the repository to the sandbox, which already holds somebody else's branch.&lt;/li&gt;
&lt;li&gt;Run the first test. Wait for the deploy, wait for the test queue, read the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://renmy.net/teams" rel="noopener noreferrer"&gt;teams page&lt;/a&gt; puts the typical calendar plainly: day one, request org access; day three, an admin provisions a sandbox; day five, the first test run. Then the loop begins: change a class, push it to the org, run the tests, read the result. Four minutes a cycle on a good day.&lt;/p&gt;

&lt;p&gt;A Java developer clones the repository, runs &lt;code&gt;mvn test&lt;/code&gt;, and has a result before the welcome meeting. A JavaScript developer runs &lt;code&gt;npm install &amp;amp;&amp;amp; npm test&lt;/code&gt;. Salesforce developers have never had that option: the Apex compiler, the Apex runtime and the database all live inside the org, so the only way to run an Apex test has been to put the code in an org and ask the org to run it. The sandbox request is how a team rents an Apex runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a local Apex runtime?
&lt;/h2&gt;

&lt;p&gt;renmy runtime is a local runtime for Salesforce Apex, made by Renmy UG in Berlin. It executes real Apex (classes, interfaces, enums, generics, triggers), SOQL, DML and Salesforce Flows (autolaunched, record-triggered, screen) on the developer's machine against an embedded PostgreSQL. One binary for macOS, Linux and Windows. No Salesforce org, no scratch org, no sandbox, no Docker, no JVM.&lt;/p&gt;

&lt;p&gt;It is not a mock framework and not a transpiler. Governor limits are enforced, &lt;code&gt;Test.startTest&lt;/code&gt;/&lt;code&gt;Test.stopTest&lt;/code&gt; and &lt;code&gt;@testSetup&lt;/code&gt; behave as on the platform, every test method runs in its own transaction, and a trigger or a Salesforce Flow fires in its slot in the order of execution. How closely it matches the org is a public record, not a claim: &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;renmy.net/parity&lt;/a&gt;. A typical Apex test completes in tens of milliseconds, SOQL and DML included; the published benchmark puts four open-source suites, 837 tests, at about 1 min 46 s in a Salesforce org (deploy and run) against 2.4 s locally, roughly 45× faster (&lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;method and numbers&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For onboarding the consequence is simple: the Apex runtime a new developer needs is now a download, like the JVM or Node, and everything else is in the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day one with a local runtime, in four steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Clone the repository
&lt;/h3&gt;

&lt;p&gt;A standard SFDX project, with the &lt;code&gt;force-app/main/default&lt;/code&gt; layout you would push to an org.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:your-org/your-salesforce-project.git
&lt;span class="nb"&gt;cd &lt;/span&gt;your-salesforce-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Install renmy runtime
&lt;/h3&gt;

&lt;p&gt;One line, no admin rights. The embedded database ships inside the binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh

&lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex

renmy &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In IntelliJ IDEA the &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;JetBrains plugin&lt;/a&gt;, Renmy Runtime - Salesforce IDE, does this step itself on first open of a Salesforce DX project; the Free tier needs no account (&lt;a href="https://renmy.net/jetbrains" rel="noopener noreferrer"&gt;product page&lt;/a&gt;). There is also a &lt;a href="https://renmy.net/vscode" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the doctor
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;renmy doctor&lt;/code&gt; reads the project, validates the layout and lists anything it needs you to set up. On a clean SFDX project it usually reports green.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;renmy doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Run the whole suite
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Everything&lt;/span&gt;
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;

&lt;span class="c"&gt;# One class, one method, a pattern&lt;/span&gt;
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"OrderServiceTest"&lt;/span&gt;
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"OrderServiceTest.testDiscountApplies"&lt;/span&gt;
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"Order*Test.*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output mirrors &lt;code&gt;sf apex run test&lt;/code&gt;: pass and fail counts, failure messages, stack traces, coverage. No deploy, no queue and no org login anywhere in those four steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test environment is a file in the repository
&lt;/h2&gt;

&lt;p&gt;In an org, the test environment lives in Setup, Custom Settings and org-level metadata, none of it in source control, so it reaches a new developer through a wiki page or a colleague's memory. renmy runtime reads all of it from one properties file that the team lead writes once and commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Writes a documented renmy.properties with every property, its description&lt;/span&gt;
&lt;span class="c"&gt;# and example profiles. Uncomment what you need and commit the file.&lt;/span&gt;
renmy config init

&lt;span class="c"&gt;# Later: the effective configuration and where each value came from&lt;/span&gt;
renmy config show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What a team commits, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# renmy.properties - committed, shared by every developer and by CI
&lt;/span&gt;
&lt;span class="c"&gt;# Org environment defaults
&lt;/span&gt;&lt;span class="py"&gt;renmy.org.currency&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;EUR&lt;/span&gt;
&lt;span class="py"&gt;renmy.org.locale&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;de_DE&lt;/span&gt;
&lt;span class="py"&gt;renmy.org.timezone&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Europe/Berlin&lt;/span&gt;

&lt;span class="c"&gt;# Custom setting org defaults, so no test class needs a @testSetup for them
&lt;/span&gt;&lt;span class="py"&gt;renmy.seed.org-default.TriggerSettings__c&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;IsEnabled__c=true&lt;/span&gt;
&lt;span class="py"&gt;renmy.seed.org-default.AppConfig__c&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Endpoint__c=https://api.example.com,Retries__c=3&lt;/span&gt;

&lt;span class="c"&gt;# Governor limits: relaxed locally, strict in CI
&lt;/span&gt;&lt;span class="py"&gt;renmy.governor.mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;warn&lt;/span&gt;
&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="py"&gt;ci.renmy.governor.mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="py"&gt;ci.renmy.trace.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in that file matter for onboarding. The org defaults mean &lt;code&gt;UserInfo.getDefaultCurrency()&lt;/code&gt; and &lt;code&gt;UserInfo.getLocale()&lt;/code&gt; return what they return in your org, on every machine. The custom setting seeds mean the feature flags and endpoints your Apex reads through &lt;code&gt;getOrgDefaults()&lt;/code&gt; exist before the first test runs, without &lt;code&gt;@testSetup&lt;/code&gt; boilerplate. The &lt;code&gt;%ci.&lt;/code&gt; profile means CI reads the same file with stricter settings, activated with &lt;code&gt;--profile ci&lt;/code&gt; or &lt;code&gt;RENMY_PROFILE=ci&lt;/code&gt;. Permission sets load from the &lt;code&gt;.permissionset-meta.xml&lt;/code&gt; files already in the repository and &lt;code&gt;System.runAs()&lt;/code&gt; works locally. The full reference is on the &lt;a href="https://renmy.net/configuration" rel="noopener noreferrer"&gt;configuration page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the repository does not carry
&lt;/h2&gt;

&lt;p&gt;This is where day one can still stall. renmy runtime works from source. If the suite depends on something the repository does not hold, the local run says so: the doctor output and the failure trace name the missing capability.&lt;/p&gt;

&lt;p&gt;Two cases are common. Custom settings nobody seeded: add them to &lt;code&gt;renmy.properties&lt;/code&gt; as above. Managed packages: if the Apex calls into a package whose source the org hides, someone who already has org access runs &lt;code&gt;renmy stub pull --org &amp;lt;alias&amp;gt; --namespaces &amp;lt;namespace&amp;gt;&lt;/code&gt; once and commits the stubs; the new developer never needs the org for it (&lt;a href="https://renmy.net/managed-packages" rel="noopener noreferrer"&gt;managed packages&lt;/a&gt;). A multi-currency org needs &lt;code&gt;renmy.org.features=MultiCurrency&lt;/code&gt;, because the runtime starts single-currency, like a new org.&lt;/p&gt;

&lt;p&gt;The onboarding checklist has become a test of the repository. If a new hire cannot run the suite from a fresh clone, the repository is missing something, and the team finds out before an incident does. Committing the stub or the seed fixes it for everyone who follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a team lead gets out of this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Contractors are productive before their org user exists.&lt;/strong&gt; Running the suite needs no connected org, no JWT certificate and no sandbox. The org access request still goes in on day one, because deployment and anything a browser touches still need an org, but it is off the critical path. When the contract ends, revoking org access is the whole off-boarding. Nothing is uploaded to Renmy UG; the &lt;a href="https://renmy.net/security" rel="noopener noreferrer"&gt;security page&lt;/a&gt; documents it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No sandbox per person, and no shared one to trample.&lt;/strong&gt; Several developers sharing one sandbox means a one-line trigger change fails because of someone else's test data. Locally, each test runs in its own transaction on the developer's own machine: no shared state, no queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same command in CI.&lt;/strong&gt; The onboarding instructions and the pipeline agree because they are the same lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/test.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apex Tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -fsSL https://renmy.net/install.sh | sh&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;renmy test "*" --profile ci --results-xml results.xml --coverage-report cobertura.xml&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;RENMY_LICENSE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RENMY_LICENSE_KEY }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test-results&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;results.xml&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No connected org, no scratch org pool, no Dev Hub. CI runs read a license key from the portal through the &lt;code&gt;RENMY_LICENSE_KEY&lt;/code&gt; secret and are part of the Team tier, free during the public beta. The &lt;a href="https://renmy.net/ci" rel="noopener noreferrer"&gt;CI page&lt;/a&gt; has the GitLab equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  "I already give every new developer a sandbox. Why change?"
&lt;/h2&gt;

&lt;p&gt;Keep giving them one. A sandbox is the right place for Lightning pages, for clicking through a flow and for the deployment itself. What it is not good at is being provisioned in minutes by the person who needs it. A sandbox is provisioned by somebody else, on their timeline. A local runtime is provisioned by &lt;code&gt;git clone&lt;/code&gt;. And most of an Apex developer's day is the save-run-read cycle, which uses the org as a test runner and nothing else. &lt;a href="https://renmy.net/blog/the-scratch-org-tax" rel="noopener noreferrer"&gt;The scratch org tax&lt;/a&gt; prices the same split for scratch orgs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it cost?
&lt;/h2&gt;

&lt;p&gt;Running locally is the Free tier: $0 per developer, no card, unlimited local test runs, gated deploys with &lt;code&gt;renmy deploy&lt;/code&gt;, Salesforce CLI pass-through and IDE integration, on one machine. A contractor installs it on their own laptop and pays nothing. Team adds test runs on CI runners, a licence management portal to assign and revoke seats, and a dedicated CI runner licence; it is free during the public beta, with a list price of $49 per developer per month. The &lt;a href="https://renmy.net/teams" rel="noopener noreferrer"&gt;teams page&lt;/a&gt; has a calculator with a row for developers waiting for org access.&lt;/p&gt;

&lt;h2&gt;
  
  
  A day-one checklist for the team lead
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;renmy config init&lt;/code&gt; once, fill in the org defaults and custom setting seeds, commit &lt;code&gt;renmy.properties&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the code calls managed packages, pull the stubs once with org access and commit them.&lt;/li&gt;
&lt;li&gt;Put clone, install, &lt;code&gt;renmy doctor&lt;/code&gt; and &lt;code&gt;renmy test "*"&lt;/code&gt; at the top of the README.&lt;/li&gt;
&lt;li&gt;Add the same &lt;code&gt;renmy test "*"&lt;/code&gt; to CI with the &lt;code&gt;ci&lt;/code&gt; profile.&lt;/li&gt;
&lt;li&gt;Still request org access on day one. It is for deployment now, not what the first week waits on.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quickstart is at &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;https://renmy.net/quickstart&lt;/a&gt;. Source, issues, docs and released binaries: &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;https://github.com/renmy-solutions/renmy&lt;/a&gt;. The JetBrains plugin: &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide&lt;/a&gt;. The VS Code extension: &lt;a href="https://renmy.net/vscode" rel="noopener noreferrer"&gt;https://renmy.net/vscode&lt;/a&gt;. Questions go to the &lt;a href="https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can a new Salesforce developer run Apex tests with no org access at all?
&lt;/h3&gt;

&lt;p&gt;Yes. renmy runtime executes Apex, SOQL, DML and Salesforce Flows on the developer's machine against an embedded PostgreSQL, so a fresh clone of a standard SFDX project plus one installed binary runs the entire Apex test suite. No sandbox, scratch org or VPN is involved. Org access is needed later, for deployment and for anything that happens in a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does renmy config init write, and should the file be committed?
&lt;/h3&gt;

&lt;p&gt;It writes a documented &lt;code&gt;renmy.properties&lt;/code&gt; file listing every available property with its description and example profiles. The team lead uncomments what the project needs, such as org defaults, custom setting seeds and a stricter &lt;code&gt;ci&lt;/code&gt; profile, and commits the file. Every developer and the CI runner then share the same test environment. A project set up before v1.50.0 may have a &lt;code&gt;nimbus.properties&lt;/code&gt; file instead; it keeps working.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when the suite depends on something that is not in the repository?
&lt;/h3&gt;

&lt;p&gt;The local run tells you. The doctor output and the failure trace name the missing capability, so a failing test on day one points at a concrete gap: a custom setting that needs a seed in &lt;code&gt;renmy.properties&lt;/code&gt;, or a managed package that needs stubs pulled once with &lt;code&gt;renmy stub pull&lt;/code&gt; and committed. Closing the gap in the repository fixes it for everyone who follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does every developer on the team need a licence?
&lt;/h3&gt;

&lt;p&gt;No. The Free tier covers an individual developer on one machine with unlimited local test runs and gated deploys, at no cost and with no card. The Team tier adds test runs on CI runners and a licence management portal for assigning and revoking seats; it is free during the public beta, and the list price is $49 per developer per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a local runtime replace the sandbox for new developers?
&lt;/h3&gt;

&lt;p&gt;No. Final validation and deployment happen in Salesforce, and anything a browser or an external system needs still happens in an org. The local runtime removes the org from the one step where it only slowed the new developer down: running the Apex tests. Request the sandbox on day one as before.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Where does the Apex go? A security review of a local runtime</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:14:35 +0000</pubDate>
      <link>https://dev.to/oblitus/where-does-the-apex-go-a-security-review-of-a-local-runtime-ilc</link>
      <guid>https://dev.to/oblitus/where-does-the-apex-go-a-security-review-of-a-local-runtime-ilc</guid>
      <description>&lt;p&gt;&lt;em&gt;Nimbus was renamed renmy runtime in October 2026. Same tool, same binary; &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;the record of the rename&lt;/a&gt; has the details.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; renmy runtime is a local Salesforce Apex runtime and developer toolchain, made by Renmy UG in Berlin: it executes Apex, SOQL, DML and Salesforce Flows on the developer's machine against an embedded PostgreSQL, with no org, no Docker and no JVM. This is the security review for the vendor questionnaire. Apex source is read locally and transmitted to nobody; test data lives in an in-process database with no network port and is rolled back after each test. The Free tier needs no network access at all, telemetry is one activation event that a single setting turns off, and a Pro or Team licence check sends the licence key and a machine fingerprint, never code. The Salesforce org stays the system of record and the deployment target.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a local Apex runtime, and why does it need a review?
&lt;/h2&gt;

&lt;p&gt;A Java developer who presses "run test" runs the code on the JVM on their own laptop. Salesforce developers have never had that choice: Apex executes only inside a Salesforce org, so every test run is a deploy into Salesforce's data centres, a queue and a log. A local Apex runtime closes that gap. renmy runtime is an interpreter for the Apex language, an embedded PostgreSQL for SOQL and DML, and the platform's rules (governor limits, triggers, order of execution, per-test transaction isolation) enforced on the developer's machine. It is not a mock; the record of what it runs and what it does not is public at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;renmy.net/parity&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A compiler does not get a questionnaire. A tool that reads Apex source and builds a database from the project's metadata does, and it should. Three questions cover almost every row of the form: where does the code go, where does the data live, and what leaves the machine. The answers below come from the published &lt;a href="https://renmy.net/security" rel="noopener noreferrer"&gt;security page&lt;/a&gt; and the &lt;a href="https://renmy.net/teams" rel="noopener noreferrer"&gt;teams page&lt;/a&gt; and go no further than those two pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Security teams, procurement and enterprise architects who have been handed a request for renmy runtime and need the answers in the order the questionnaire asks them. Developers making that request can start with &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;what a local Apex runtime is&lt;/a&gt; and &lt;a href="https://renmy.net/blog/how-to-evaluate-a-local-apex-runtime" rel="noopener noreferrer"&gt;how to evaluate one&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the Apex source go?
&lt;/h2&gt;

&lt;p&gt;Nowhere. renmy runtime reads the project's &lt;code&gt;.cls&lt;/code&gt;, &lt;code&gt;.trigger&lt;/code&gt; and &lt;code&gt;.flow-meta.xml&lt;/code&gt; files from the working tree and executes them in an interpreter that runs as a subprocess on the same machine. No source is transmitted to Renmy UG, to Salesforce or to any third party, and no code is sent to a remote execution environment.&lt;/p&gt;

&lt;p&gt;The schema the tests run against comes from the same place. Object definitions are read from &lt;code&gt;sfdx-project.json&lt;/code&gt; and the project's &lt;code&gt;.object-meta.xml&lt;/code&gt; files; a test run fetches no schema from a Salesforce org. One consequence belongs on the form: running the suite needs no connected org, no JWT certificate and no sandbox. Org access is used for validation and deployment, through the Salesforce CLI the team already trusts, and for nothing else.&lt;/p&gt;

&lt;p&gt;For a team that has treated a sandbox per developer as the price of running tests, this is a reduction, not an addition: a developer or a CI runner whose only job is to run the tests holds no credential to any org.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does test data live?
&lt;/h2&gt;

&lt;p&gt;In an embedded PostgreSQL on the local disk. Records inserted during a test go into that database and nowhere else. Three properties belong on the questionnaire as written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs in-process.&lt;/strong&gt; There is no database server to install or operate separately; the engine is embedded in the renmy binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has no network port open.&lt;/strong&gt; Nothing listens for another host, so there is nothing for a network scan to find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing persists between test runs.&lt;/strong&gt; Each test runs in its own transaction, rolled back when the test ends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because a test run needs no connected org, the rows in that database are the ones the tests insert. "Does the tool copy production data onto developer laptops?" has a short answer: a test run has no org to copy from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What leaves the machine?
&lt;/h2&gt;

&lt;p&gt;Case by case, as the security page states it. The &lt;code&gt;RENMY_&lt;/code&gt; prefix on the variables below is the product's former name, kept so existing configuration keeps working.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Free tier needs no network access
&lt;/h3&gt;

&lt;p&gt;The Free tier requires no network access at all: there is no licence to validate and no account to sign in to. What the runtime can send, on any tier, follows; the complete table, including the update check and the Dev UI, is further down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telemetry: one activation event
&lt;/h3&gt;

&lt;p&gt;By default renmy runtime records one privacy-minimal activation event. It goes to Supabase, hosted in the EU, and is used only to measure which installation channels work; it is not sold, shared or used for advertising. The published list of what it never contains is the useful part for the form: no source code, file paths, test names, record data, repository, branch, workflow or command arguments.&lt;/p&gt;

&lt;p&gt;Ordinary first-run telemetry is excluded from CI. After a Pro licence validates in headless CI, the runtime records one de-duplicated activation per licence fingerprint, CI provider and installation source, so repeated builds do not inflate installation counts. The same opt-out settings disable that event too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Licence validation: the key and a machine fingerprint
&lt;/h3&gt;

&lt;p&gt;Pro and Team licences are validated with the licensing API, Keygen, on activation and at most once a day after that. A validation sends the licence key. Activating a machine with &lt;code&gt;renmy login&lt;/code&gt; sends more, and the security page lists it: a one-way hash of a random installation ID, the hostname, the machine's display name (on macOS, the Computer Name), the OS and architecture, the number of CPU cores, and your account ID, to enforce seat limits and list your machines in the portal. Nothing from your code, tests or org is sent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sign in via browser and activate this machine&lt;/span&gt;
renmy login

&lt;span class="c"&gt;# What's sent to the licensing API (Keygen):&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "key": "RENMY-XXXX-XXXX-XXXX",&lt;/span&gt;
&lt;span class="c"&gt;#   "machine": "3f9c…"&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;span class="c"&gt;# key: handed to the CLI by your signed-in browser&lt;/span&gt;
&lt;span class="c"&gt;# machine: one-way hash of a random installation ID&lt;/span&gt;
&lt;span class="c"&gt;# On activation also: hostname, display name, OS/arch,&lt;/span&gt;
&lt;span class="c"&gt;# CPU cores and your account ID&lt;/span&gt;

&lt;span class="c"&gt;# What's NOT sent:&lt;/span&gt;
&lt;span class="c"&gt;# - File paths, project names, or source code&lt;/span&gt;
&lt;span class="c"&gt;# - Test output, coverage data, or Salesforce data&lt;/span&gt;
&lt;span class="c"&gt;# - Anything from your codebase or org&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do I turn telemetry off?
&lt;/h2&gt;

&lt;p&gt;One line, in whichever form fits the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disable telemetry permanently: add this line to&lt;/span&gt;
&lt;span class="c"&gt;# renmy.properties (in the project or ~/.nimbus/)&lt;/span&gt;
renmy.telemetry&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# Or per shell or CI job&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RENMY_TELEMETRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# RENMY_NO_TELEMETRY=1 and RENMY_NO_TRACKING=1&lt;/span&gt;
&lt;span class="c"&gt;# are honoured as well&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these also disables the CI activation event. A build server with &lt;code&gt;RENMY_TELEMETRY=false&lt;/code&gt; in its job variables makes no call other than the licence validation while it runs the tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it work offline, and can it be installed air-gapped?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Offline operation
&lt;/h3&gt;

&lt;p&gt;The Free tier needs no network at all. For Pro and Team, the licence is cached locally and validated offline for up to 7 days, so a developer behind a strict proxy or without a connection keeps working for a week without the licensing API answering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Air-gapped install
&lt;/h3&gt;

&lt;p&gt;renmy runtime supports fully air-gapped installation. The PostgreSQL engine is embedded in the binary, so nothing is downloaded; the binary can be distributed through an internal artifact repository without internet access and copied into place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Air-gapped install: copy the binary directly.&lt;/span&gt;
&lt;span class="c"&gt;# The PostgreSQL engine is embedded in it,&lt;/span&gt;
&lt;span class="c"&gt;# so nothing is downloaded.&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;renmy /usr/local/bin/renmy

&lt;span class="c"&gt;# Turn off telemetry (or renmy.telemetry=false)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RENMY_TELEMETRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="c"&gt;# and the daily update check&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RENMY_NO_UPGRADE_CHECK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  On-premise licence validation for Enterprise
&lt;/h3&gt;

&lt;p&gt;A 7-day cache is not a licensing model for a network that never connects. For Enterprise customers, on-premise licence validation is available on request, so no outbound network is required for licence checks. Enterprise starts at $20,000 per year on a custom contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network access, command by command
&lt;/h2&gt;

&lt;p&gt;The full table from the security page, reproduced so the questionnaire can quote it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command or event&lt;/th&gt;
&lt;th&gt;Reaches&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renmy test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Fully local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renmy test:watch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Fully local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renmy dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;localhost only&lt;/td&gt;
&lt;td&gt;WebSocket to the browser on the same machine; &lt;code&gt;--addr&lt;/code&gt; beyond loopback requires a token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renmy login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;portal + licensing API&lt;/td&gt;
&lt;td&gt;Browser handshake to fetch your key, then the key and the machine details above to Keygen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Licence check (Pro, Team)&lt;/td&gt;
&lt;td&gt;licensing API (Keygen)&lt;/td&gt;
&lt;td&gt;At most once a day; Pro and Team features keep working offline for up to 7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renmy upgrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitHub releases&lt;/td&gt;
&lt;td&gt;Binary download. Homebrew and Scoop fetch the same files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update check&lt;/td&gt;
&lt;td&gt;api.github.com&lt;/td&gt;
&lt;td&gt;At most once a day, interactive terminals only, never in CI or under agents. Off: &lt;code&gt;RENMY_NO_UPGRADE_CHECK=1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anonymous telemetry&lt;/td&gt;
&lt;td&gt;Supabase (EU)&lt;/td&gt;
&lt;td&gt;One activation event. Opt-out: &lt;code&gt;renmy.telemetry=false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rows are absent, and should be absent from the form too: a remote execution environment, because no code is sent to one, and the Salesforce org, because running the tests does not connect to one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is the final authority, the runtime or the org?
&lt;/h2&gt;

&lt;p&gt;The org. Hold the vendor to this; the teams page says it in the vendor's own words: your org stays the deployment target and the system of record; renmy runtime handles the steps that never needed an org. Final validation and deployment happen in Salesforce, and anything a browser or an external system needs still happens in an org.&lt;/p&gt;

&lt;p&gt;Local tests are the fast loop; deployment is gated. &lt;code&gt;renmy deploy&lt;/code&gt;, in the Free tier, snapshots the payload, runs the local gates, validates the same bytes in the target org with a real check-only Salesforce validation, and only then deploys. &lt;code&gt;renmy release validate&lt;/code&gt; and &lt;code&gt;renmy release deploy&lt;/code&gt; (Pro) split that into a validation that writes an immutable receipt, a human approval, and a deploy of exactly the validated payload. For auditors, signed release receipts and the on-premise Assurance console record what was validated and what was deployed. Details: &lt;a href="https://renmy.net/deploy" rel="noopener noreferrer"&gt;deploy&lt;/a&gt; and &lt;a href="https://renmy.net/release" rel="noopener noreferrer"&gt;release&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to attach to the questionnaire
&lt;/h2&gt;

&lt;p&gt;Everything above is documented publicly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://renmy.net/security" rel="noopener noreferrer"&gt;Security &amp;amp; privacy&lt;/a&gt;: what runs locally, telemetry, licence validation, the network table, air-gapped installation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://renmy.net/teams" rel="noopener noreferrer"&gt;For teams&lt;/a&gt;: the security and procurement section, the plans, and the licence management portal (assign and revoke seats, licence a dedicated CI runner, see who has what).&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://renmy.net/dpa" rel="noopener noreferrer"&gt;Data Processing Addendum&lt;/a&gt; and the &lt;a href="https://renmy.net/subprocessors" rel="noopener noreferrer"&gt;subprocessor list&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://renmy.net/eula" rel="noopener noreferrer"&gt;EULA&lt;/a&gt;, the &lt;a href="https://renmy.net/terms" rel="noopener noreferrer"&gt;terms&lt;/a&gt; and the &lt;a href="https://renmy.net/privacy" rel="noopener noreferrer"&gt;privacy policy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A mutual NDA on request, yours or ours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On price: Free for individual developers is $0, no card. Pro and Team are free during the public beta (list prices $19 per month and $49 per developer per month). Enterprise is from $20,000 per year on a custom contract, with a DPA, PO and invoice billing, and SSO (SAML) on request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; takes it from here. Issues, docs, examples and released binaries: &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;github.com/renmy-solutions/renmy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does renmy runtime send Apex source code to Renmy UG or to Salesforce?
&lt;/h3&gt;

&lt;p&gt;No. renmy runtime reads .cls, .trigger and .flow-meta.xml files locally and executes them in an interpreter on the same machine. No source is transmitted to Renmy UG, to Salesforce or to any third party, and no code is sent to a remote execution environment. The schema is parsed from the project's metadata, so a test run fetches nothing from an org.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can renmy runtime run without internet access?
&lt;/h3&gt;

&lt;p&gt;Yes. The Free tier requires no network access at all. Pro and Team licences are cached locally and validated offline for up to 7 days. Air-gapped installation is supported, since the PostgreSQL engine is embedded in the binary, and Enterprise customers can have on-premise licence validation with no outbound network.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does renmy runtime's telemetry contain, and how do I turn it off?
&lt;/h3&gt;

&lt;p&gt;One privacy-minimal activation event, sent to Supabase in the EU and used only to measure which installation channels work. It never contains source code, file paths, test names, record data, repository, branch, workflow or command arguments. Set renmy.telemetry=false in the settings file or export RENMY_TELEMETRY=false; either also disables the separate CI activation event.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does renmy login send to the licensing API?
&lt;/h3&gt;

&lt;p&gt;The licence key and a one-way hash of a random installation ID, plus, on activation, the hostname, the machine's display name, OS and architecture, CPU core count and your account ID, to enforce seat limits and list your machines in the portal. Pro and Team licences are then validated at most once a day. Nothing from your code, tests or org is sent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a local Apex runtime replace the Salesforce org as the system of record?
&lt;/h3&gt;

&lt;p&gt;No. The org stays the deployment target and the system of record. renmy runtime runs the tests locally, then validates the same bytes in the org with a real check-only validation before deploying; final validation and deployment happen in Salesforce. The parity record of what runs locally is public at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;https://renmy.net/parity&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Test Salesforce Flows locally before you activate them</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Wed, 28 Oct 2026 20:31:07 +0000</pubDate>
      <link>https://dev.to/oblitus/test-salesforce-flows-locally-before-you-activate-them-229n</link>
      <guid>https://dev.to/oblitus/test-salesforce-flows-locally-before-you-activate-them-229n</guid>
      <description>&lt;p&gt;&lt;em&gt;renmy runtime was called Nimbus until October 2026. Same tool, same binary, new name; the &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;record of the rename&lt;/a&gt; has the details.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; renmy runtime is a local Salesforce Apex runtime: a program on your own computer that executes Apex, SOQL, DML and Salesforce Flows (autolaunched, record-triggered and screen flows) against an embedded database, with no org, sandbox or scratch org involved. A record-triggered flow saved in your project runs locally the moment a test creates or updates the record it watches, together with the Apex triggers on the same object, so an overlap between your flow and somebody's trigger shows up in milliseconds instead of after activation. The simplest path is one command, &lt;code&gt;renmy test "*"&lt;/code&gt;, run by a developer colleague or by you; the Dev UI, &lt;code&gt;renmy dev&lt;/code&gt;, puts the same runner in a browser tab. It executes the logic of a flow; it does not draw the screens of a screen flow. The Free tier covers all of it, with no account and no card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for.&lt;/strong&gt; Salesforce admins, flow builders and low-code consultants who build automation in Flow Builder, test it by clicking through a sandbox, and have never typed a command into a terminal. Every command below is explained before it appears. The developer side of the idea is in &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;What a local runtime is, and why Salesforce never gave you one&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why testing a Salesforce Flow in a sandbox is slow and risky
&lt;/h2&gt;

&lt;p&gt;The way most flows get tested today: build the flow in a sandbox, press Debug, create a record by hand, watch what happens, adjust, repeat. For a record-triggered flow you may add Flow Tests in Flow Builder. Then you activate, deploy to production, and watch the first real records go through.&lt;/p&gt;

&lt;p&gt;Debug and Flow Tests are useful, and both run in the org. That is where the trouble starts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sandbox is shared.&lt;/strong&gt; Someone else's half-finished flow on Opportunity, a validation rule added on Friday, a trigger a developer is midway through: all of it fires when your record saves. When the result is wrong, you spend the afternoon finding out whose automation did it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data is yours to build, every time.&lt;/strong&gt; An Account, an Opportunity in the right stage, an owner with the right profile. You click them together, and after the next sandbox refresh they are gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order of execution is invisible until it bites.&lt;/strong&gt; Salesforce runs before-save flows, then Apex triggers, then after-save flows, in a fixed order you cannot see in Flow Builder. A flow and a trigger that both create a follow-up Task produce two Tasks, and nobody notices until a sales rep asks why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every round takes minutes.&lt;/strong&gt; Save, activate, find the record, edit it, open the related list, read, reset the data. Twenty rounds is an afternoon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a flaw in your flow. It is what happens when the only place that can run a flow is a shared org.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "on your machine" means when you build flows
&lt;/h2&gt;

&lt;p&gt;When you click Save in Flow Builder, Salesforce stores your flow as a file: an XML document with every element, decision, formula and assignment in it. Your developers already pull these files down into a project folder on their computers, together with every object, field, trigger and class in the org. That folder is what they keep in version control and deploy from. It is the whole org, as files, on a laptop.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;local runtime&lt;/strong&gt; is a program that reads those files and executes them right there, with its own small database standing in for the org's. This is how every other software platform works: a Java developer presses "run test" and a program on their machine answers in a second, without talking to a server. Salesforce never shipped this for Apex or for flows, because the Apex compiler, the database and the test runner all live in Salesforce's data centres. renmy runtime is that missing program. For you, the practical picture is a private, instant sandbox on one computer, with no other users, that starts empty and throws everything away after each test.&lt;/p&gt;

&lt;p&gt;Two words you will meet below. A &lt;strong&gt;terminal&lt;/strong&gt; is the plain text window developers type commands into: Terminal on a Mac, PowerShell on Windows. You type one line, press Enter, and read what comes back. A &lt;strong&gt;test&lt;/strong&gt; is a few lines of Apex that create a record, do the thing your flow reacts to, and check the result. A developer colleague writes it; you can read it, because it says in plain words what the flow is supposed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What renmy runtime does with a Salesforce Flow
&lt;/h2&gt;

&lt;p&gt;renmy runtime executes the flow as it is saved in the file, against an embedded PostgreSQL database, on the computer where the project folder lives. It runs &lt;a href="https://renmy.net/flows" rel="noopener noreferrer"&gt;autolaunched, record-triggered and screen flows&lt;/a&gt;, including subflows, formulas and decisions. The Apex triggers on the same object run in the same transaction, in the platform's order of execution, with governor limits enforced. It is the same runner that runs the Apex tests in the project, so flows and triggers are exercised together, by the same test. Not a mock of Salesforce and not a copy of it: a runtime that reproduces its rules.&lt;/p&gt;

&lt;p&gt;Concretely: when a test updates the stage of an Opportunity, the before-save flows run, then the Apex triggers, then the after-save flows, and the test reads the records that are left. If your new flow and an existing trigger both create a Task, the test sees two Tasks: the order-of-execution problem from the previous section, found in a fraction of a second, before the flow has touched production or a shared sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test a record-triggered flow before you activate it
&lt;/h2&gt;

&lt;p&gt;The example: a record-triggered flow on Opportunity called &lt;strong&gt;Closed Won Follow-Up&lt;/strong&gt;. After an Opportunity is saved with the stage Closed Won, it creates a Task "Schedule kickoff call" for the owner. The org also has an Apex trigger on Opportunity, written two years ago by someone who has since left.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Get the flow into the project folder
&lt;/h3&gt;

&lt;p&gt;Ask your developer to retrieve the flow into the project. On their side it is one Salesforce CLI command, and the result is one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sf project retrieve start &lt;span class="nt"&gt;--metadata&lt;/span&gt; Flow:Closed_Won_Follow_Up
&lt;span class="c"&gt;# -&amp;gt; force-app/main/default/flows/Closed_Won_Follow_Up.flow-meta.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Write down what the flow should do, as a test
&lt;/h3&gt;

&lt;p&gt;This is the part a developer writes and you read. The whole test is: make an Opportunity, close it, count the Tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="nd"&gt;@IsTest&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClosedWonFollowUpFlowTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@IsTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;closingAnOpportunityCreatesOneFollowUpTask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;acct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Harbourlight Ltd'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;acct&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Opportunity&lt;/span&gt; &lt;span class="n"&gt;opp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Opportunity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Harbourlight renewal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;AccountId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;StageName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Prospecting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CloseDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;opp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;opp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;StageName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Closed Won'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;opp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// the record-triggered flow and the trigger run here&lt;/span&gt;
        &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;WhatId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;opp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;Assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Expected exactly one follow-up Task'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;Assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Schedule kickoff call'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it top to bottom and it is your acceptance criterion: one Opportunity moved to Closed Won, exactly one Task, with that subject. The records exist only inside the test; nothing is left behind.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run it
&lt;/h3&gt;

&lt;p&gt;In the terminal, inside the project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;renmy &lt;span class="nb"&gt;test &lt;/span&gt;ClosedWonFollowUpFlowTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;renmy test&lt;/code&gt; means "run tests"; the word after it names which ones. &lt;code&gt;renmy test "*"&lt;/code&gt; runs every test in the project (the asterisk means "all"; the quotes stop the terminal from interpreting it). A test like this finishes in tens of milliseconds, SOQL and DML included. The &lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;published benchmark&lt;/a&gt; puts four open-source suites, 837 tests, at about 1 min 46 s in an org against 2.4 s locally, every test passing in both.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Read the result
&lt;/h3&gt;

&lt;p&gt;If the old trigger also creates a Task on Closed Won, the first assertion fails: expected 1, actual 2. You now know, before activation, that the flow and the trigger overlap. The fix is one of two edits: retire the Task logic in the trigger, or add an entry condition to the flow. Edit, save, run again. Each round is a second, not a quarter of an hour, and no shared sandbox was touched.&lt;/p&gt;

&lt;p&gt;When the flow is ready, the same test becomes a gate: &lt;a href="https://renmy.net/deploy" rel="noopener noreferrer"&gt;&lt;code&gt;renmy deploy&lt;/code&gt;&lt;/a&gt; runs the local gates, your tests among them, before anything reaches the org. It is in the Free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a Salesforce Flow without a terminal
&lt;/h2&gt;

&lt;p&gt;Three other faces sit on the same runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Dev UI.&lt;/strong&gt; &lt;code&gt;renmy dev&lt;/code&gt; starts a &lt;a href="https://renmy.net/dev-ui" rel="noopener noreferrer"&gt;browser-based test runner&lt;/a&gt;: a page where you pick a test, click run, and read the result, plus a schema browser and a coverage explorer. One command to start it; everything after is clicking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VS Code.&lt;/strong&gt; The &lt;a href="https://renmy.net/vscode" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; puts a run button above every test and shows the result inline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IntelliJ IDEA.&lt;/strong&gt; The JetBrains plugin, &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt;, does the same inside &lt;a href="https://renmy.net/jetbrains" rel="noopener noreferrer"&gt;IntelliJ IDEA&lt;/a&gt;, Community edition included. The JetBrains plugin also opens a flow as a graph (View Flow), so you can read its elements and decisions beside the test that exercises it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It executes the logic of a screen flow. It does not render the screens.&lt;/strong&gt; Decisions, assignments, lookups, record creates and updates in a screen flow run. Nobody clicks through the flow on your laptop; what the user sees (labels, help text, conditional visibility, layout) is checked in a sandbox, with a user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not Salesforce.&lt;/strong&gt; It reproduces the platform's rules closely enough that the four suites in the benchmark pass identically in both places, but the honest statement is the &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;parity record&lt;/a&gt;, not a percentage. If a flow uses something the runtime does not cover yet, that is where you find out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not replace review in a sandbox.&lt;/strong&gt; Permissions, page layouts, what the sales rep actually sees, user acceptance: still an org, still humans.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Do I still need a sandbox?
&lt;/h2&gt;

&lt;p&gt;Yes. The sandbox is where you activate the flow, where a user clicks through it, where permissions and layouts are checked, where the business signs off. What changes is what happens before that: the twenty rounds of "does the flow do the right thing to the records, with the triggers in the loop" move to a laptop, cost a second each, and leave the sandbox clean for the review it is good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS or Linux: open Terminal, paste the line, press Enter&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh

&lt;span class="c"&gt;# Windows: open PowerShell, paste the line, press Enter&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex

&lt;span class="c"&gt;# Go to the project folder (ask your developer for the path), then run every test&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;

&lt;span class="c"&gt;# Or start the browser face&lt;/span&gt;
renmy dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; walks through the first run in under five minutes. Issues, docs and released binaries are on &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;; for IntelliJ IDEA, the plugin is &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt;. The Free tier covers unlimited local test runs on one machine, no account, no card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I test a Salesforce Flow without a sandbox?
&lt;/h3&gt;

&lt;p&gt;Yes, for what the flow does to records. renmy runtime executes autolaunched, record-triggered and screen flows on your own computer against an embedded database, together with the Apex triggers on the same object, so a test can create a record, let the flow react, and check the outcome in milliseconds. A sandbox is still where the flow is activated and where users click through it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does renmy runtime show me a screen flow the way a user sees it?
&lt;/h3&gt;

&lt;p&gt;No. It executes the logic of a screen flow: decisions, assignments, lookups and record changes. It does not render the screens, so layout, labels, help text and conditional visibility are checked in a sandbox with a user, not locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to know Apex to test a flow locally?
&lt;/h3&gt;

&lt;p&gt;Not to run one. The test that exercises a flow is a short Apex class that creates a record, changes it and counts the result; a developer colleague writes it and an admin can read it as an acceptance criterion. Running it is one command, &lt;code&gt;renmy test&lt;/code&gt;, or one click in the Dev UI, VS Code or IntelliJ IDEA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the flow run in the same order as in Salesforce, with my Apex triggers?
&lt;/h3&gt;

&lt;p&gt;Order of execution with triggers and flows is part of what renmy runtime reproduces: the flow and the Apex triggers on the same object run in one transaction, in the order the platform defines, with governor limits enforced. The parity record at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;renmy.net/parity&lt;/a&gt; lists what matches and what does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does it cost?
&lt;/h3&gt;

&lt;p&gt;Nothing for an individual: the Free tier includes unlimited local test runs, flows included, on one machine, with no account and no card. Pro and Team add watch mode, debugger stepping, the daemon and CI runs, and both are free during the public beta.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>flow</category>
      <category>lowcode</category>
      <category>testing</category>
    </item>
    <item>
      <title>Apex test sharding: split one suite across CI jobs</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:47:47 +0000</pubDate>
      <link>https://dev.to/oblitus/apex-test-sharding-split-one-suite-across-ci-jobs-18la</link>
      <guid>https://dev.to/oblitus/apex-test-sharding-split-one-suite-across-ci-jobs-18la</guid>
      <description>&lt;p&gt;&lt;em&gt;renmy runtime was called Nimbus until October 2026. Same tool, same binary; the record of the rename is at &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;https://renmy.net/blog/nimbus-is-now-renmy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; Test sharding splits one test suite into M slices and runs each slice in its own CI job at the same time, so a suite that takes twelve minutes on one runner takes roughly three, plus setup, on four. renmy runtime, a local Salesforce Apex runtime made by Renmy UG, executes Apex tests on the CI runner itself against an embedded PostgreSQL with no org, which is what makes sharding possible for Apex at all: &lt;code&gt;renmy test --shard 2/4&lt;/code&gt; runs the second of four slices. A slice is a set of whole test classes, balanced by the durations recorded in &lt;code&gt;.nimbus/test-durations.json&lt;/code&gt;, so &lt;code&gt;@testSetup&lt;/code&gt; stays correct and the shards finish at about the same time. With &lt;code&gt;--results-xml&lt;/code&gt;, each shard's JUnit XML carries a &lt;code&gt;shard="N/M"&lt;/code&gt; attribute so a test reporter can merge the files. Running on a CI runner is part of the Team tier, free during the public beta.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;DevOps engineers and platform teams whose Apex test suite has outgrown a single CI job: thousands of tests, several packages in one repository, a pull-request queue where the time to a green check is the bottleneck. You should know your way around a GitHub Actions matrix; you do not need to have used a local runtime before. If your suite already finishes in under two minutes on one runner, read the section on when sharding is not worth it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is test sharding?
&lt;/h2&gt;

&lt;p&gt;A suite has N test classes and you have M runners. Sharding assigns every class to exactly one runner, each runner executes its assignment, and the results are merged into one report. Nothing runs twice or is skipped, and the wall-clock time drops to roughly the slowest shard plus setup. It does not make the suite cheaper, since the compute is the same plus M times the setup; it makes the suite shorter, which is what the developer waiting for a pull-request check cares about.&lt;/p&gt;

&lt;p&gt;Every other ecosystem does this routinely: a JavaScript team puts &lt;code&gt;jest --shard=1/4&lt;/code&gt; in a matrix, a Python team splits pytest across containers. Two conditions make it work. The tests must be independent of each other, and the runner must be able to execute the tests itself, so that adding runners adds capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Apex tests could never be sharded in a Salesforce org
&lt;/h2&gt;

&lt;p&gt;In an org, "run the tests" means: deploy the source, ask the org to run the tests, wait in the org's test queue, download the results. The CI runner's CPU is idle the whole time; the work happens in Salesforce's data centre. The CI page on renmy.net puts the org-based cycle at 12 to 20 minutes per run, 4 to 8 of them the deploy and another 3 to 5 the test run. That second condition is the one Apex failed: adding a runner adds nothing, because it would need a second org, a second deploy and a second place in the same queue. The only lever left was to run fewer tests.&lt;/p&gt;

&lt;p&gt;renmy runtime removes the constraint. It is a local runtime for Salesforce Apex: an interpreter that executes Apex classes, interfaces, enums, generics and triggers, SOQL and DML, and Salesforce Flows (autolaunched, record-triggered and screen) against an embedded PostgreSQL, on whatever machine it runs on. One binary for macOS, Linux and Windows; no org, no scratch org, no sandbox, no Docker, no JVM. Governor limits are enforced, &lt;code&gt;Test.startTest&lt;/code&gt;/&lt;code&gt;Test.stopTest&lt;/code&gt;, &lt;code&gt;@testSetup&lt;/code&gt;, per-test transaction isolation and the Stub API behave as they do on the platform. It is not a mock framework and not a transpiler; how closely it matches the org is a public record, &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;renmy.net/parity&lt;/a&gt;, and the idea from the beginning is in &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;What is a local Apex runtime?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the tests execute on the runner, they are a process like any other and the ordinary CI arithmetic applies. The published benchmark runs four open-source suites, 837 tests, in 2.4 s locally against about 1 min 46 s in an org (&lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;method and numbers&lt;/a&gt;); for a suite that size one job is plenty, and sharding is for the suites that are not. "I already have a CI sandbox" is the usual objection, and you keep it: it stays where the payload is validated and deployed. What moves to the runner is the part that ran on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does &lt;code&gt;renmy test --shard N/M&lt;/code&gt; split the suite?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Four shards. This runner takes the second one.&lt;/span&gt;
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--shard&lt;/span&gt; 2/4 &lt;span class="nt"&gt;--results-xml&lt;/span&gt; results-2.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Job N runs only its assigned slice of the test classes. Three rules decide what that slice is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classes stay whole
&lt;/h3&gt;

&lt;p&gt;All methods of a class stay together on one shard. &lt;code&gt;@testSetup&lt;/code&gt; runs once per class, and its records are what every method in that class sees; splitting one class across two shards would mean two setups and a result that can differ from an unsharded run. The unit of distribution is the class, not the method.&lt;/p&gt;

&lt;h3&gt;
  
  
  Balanced by recorded duration
&lt;/h3&gt;

&lt;p&gt;Test classes are not equal: one has eighty methods and a heavy setup, another has three one-liners, and an even split by count leaves one shard running long after the others. renmy runtime balances classes by recorded test duration, read from &lt;code&gt;.nimbus/test-durations.json&lt;/code&gt;, so shards finish in roughly equal time. A project with no duration history yet falls back to an even split by class name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every shard reads the same file
&lt;/h3&gt;

&lt;p&gt;Each shard computes the whole assignment on its own and takes its slice, so the partition is only consistent if every shard sees the same durations file. If shard 1 has it and shard 3 does not, they compute different partitions and a class can run twice or not at all. Commit &lt;code&gt;.nimbus/test-durations.json&lt;/code&gt; or restore it from a cache in every job; committing is the simplest guarantee, because every shard checks out the same revision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitHub Actions matrix
&lt;/h2&gt;

&lt;p&gt;This is the sharded workflow from the &lt;a href="https://renmy.net/ci" rel="noopener noreferrer"&gt;CI/CD page&lt;/a&gt;, with &lt;code&gt;fail-fast&lt;/code&gt; turned off so that one red shard does not cancel the other three and you see every failure in one pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apex tests (sharded)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install renmy runtime&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -fsSL https://renmy.net/install.sh | sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests (shard ${{ matrix.shard }}/4)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;renmy test \&lt;/span&gt;
            &lt;span class="s"&gt;--shard ${{ matrix.shard }}/4 \&lt;/span&gt;
            &lt;span class="s"&gt;--results-xml results-${{ matrix.shard }}.xml&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;NIMBUS_LICENSE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NIMBUS_LICENSE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;NIMBUS_PROFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish test results&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EnricoMi/publish-unit-test-result-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results-${{ matrix.shard }}.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;strategy.matrix.shard&lt;/code&gt; fans the job out into four, and &lt;code&gt;--shard ${{ matrix.shard }}/4&lt;/code&gt; tells the runtime which slice to take. &lt;code&gt;--results-xml results-N.xml&lt;/code&gt; gives each shard its own JUnit file, and &lt;code&gt;if: always()&lt;/code&gt; publishes a failed shard's results too. &lt;code&gt;NIMBUS_LICENSE_KEY&lt;/code&gt; is the only secret in the job: no JWT certificate, no connected app, no Dev Hub. &lt;code&gt;NIMBUS_PROFILE: ci&lt;/code&gt; activates the &lt;code&gt;%ci.&lt;/code&gt; overrides in your committed &lt;code&gt;renmy.properties&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merging the results
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;--shard&lt;/code&gt; is combined with &lt;code&gt;--results-xml&lt;/code&gt;, each shard's JUnit XML includes a &lt;code&gt;shard="N/M"&lt;/code&gt; attribute, so a reporter that reads several files knows they are slices of one run and can merge them. For one combined report instead of a check per shard, upload each file as an artifact and publish once from a job that waits for the matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results-${{ matrix.shard }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results-${{ matrix.shard }}.xml&lt;/span&gt;

  &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results-*&lt;/span&gt;
          &lt;span class="na"&gt;merge-multiple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EnricoMi/publish-unit-test-result-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results-*.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coverage is a separate question: a shard measures coverage only for the tests it ran, so one shard's Cobertura file is a partial picture. Measure a whole-suite number where the whole suite runs in one process. &lt;code&gt;renmy deploy --min-coverage&lt;/code&gt; refuses a payload below the floor before the org sees it, and &lt;code&gt;renmy coverage diff&lt;/code&gt; reports the line and branch delta for a pull request (&lt;a href="https://renmy.net/coverage" rel="noopener noreferrer"&gt;renmy.net/coverage&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache what the shards share
&lt;/h3&gt;

&lt;p&gt;Every shard starts cold. renmy runtime caches parsed ASTs in &lt;code&gt;.nimbus/cache/&lt;/code&gt;, which on a project with hundreds of classes brings startup from several seconds to under a second. Restore it in every shard, and the binary with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache renmy runtime&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;~/.nimbus&lt;/span&gt;
            &lt;span class="s"&gt;.nimbus/cache&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;renmy-${{ hashFiles('force-app/**/*.cls', 'force-app/**/*.trigger') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;renmy-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache binary&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bin/renmy&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;renmy-bin-${{ runner.os }}-v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When is sharding worth it, and when is it not?
&lt;/h2&gt;

&lt;p&gt;Measure the test step on its own before deciding. The CI page puts a single unsharded job at 30 to 90 seconds in total, install included, and a typical Apex test completes in tens of milliseconds with SOQL and DML. For most projects the suite is not the long pole; the checkout and the install are. Four shards means four checkouts, four installs, four embedded PostgreSQL start-ups and four reporter runs; shard a ninety-second job and you will wait longer.&lt;/p&gt;

&lt;p&gt;Three things to try first, all from the CI page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parallel workers inside one job.&lt;/strong&gt; &lt;code&gt;%ci.renmy.test.parallel=4&lt;/code&gt; in &lt;code&gt;renmy.properties&lt;/code&gt; runs the suite on four workers within the same process (Pro, free during the beta): parallelism without the per-job overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cache.&lt;/strong&gt; &lt;code&gt;.nimbus/cache/&lt;/code&gt; and the binary, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An external PostgreSQL&lt;/strong&gt; for large schemas. A &lt;code&gt;postgres:16-alpine&lt;/code&gt; service container with &lt;code&gt;DATABASE_URL&lt;/code&gt; removes the embedded database start-up and costs nothing extra, because it starts in parallel with checkout and install.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shard when the test step itself, after those three, still runs for several minutes: many thousands of tests, classes with heavy &lt;code&gt;@testSetup&lt;/code&gt;, several packages in one repository. Then the run costs the slowest shard plus about a minute of setup, instead of the whole suite.&lt;/p&gt;

&lt;p&gt;Two caveats. Sharding does not make a slow test class faster; it hides it. A four-minute class sets a four-minute floor for its shard, and the same recorded durations that balance the shards tell you which classes dominate. And sharding changes which classes run next to each other. Each test runs in its own transaction, so a class's result should not depend on its shard; if it does, you have found an order-dependent test. &lt;code&gt;renmy history --flaky&lt;/code&gt; records run conditions for that search, and &lt;a href="https://renmy.net/blog/flaky-apex-tests" rel="noopener noreferrer"&gt;Flaky Apex tests&lt;/a&gt; covers the usual causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What running sharded Apex tests in CI costs
&lt;/h2&gt;

&lt;p&gt;Running the suite on a CI runner is part of the Team tier, which includes a CI runner licence with its own machine pool. JUnit XML and Cobertura XML output are Pro features. Pro and Team are both free during the public beta; the list prices are $19 per month and $49 per developer per month. The Free tier, $0 and no card, covers unlimited local test runs on one machine and gated deploys. Pricing: &lt;a href="https://renmy.net/#pricing" rel="noopener noreferrer"&gt;renmy.net/#pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; takes you from install to a first green run; the unsharded CI job, for GitLab CI as well as GitHub Actions, is in &lt;a href="https://renmy.net/how-to/apex-ci-without-an-org" rel="noopener noreferrer"&gt;Apex CI without an org&lt;/a&gt;; the sharded workflow is on &lt;a href="https://renmy.net/ci" rel="noopener noreferrer"&gt;renmy.net/ci&lt;/a&gt;. Issues, docs, examples and the released binaries are at &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;github.com/renmy-solutions/renmy&lt;/a&gt;; questions go to the &lt;a href="https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;--shard&lt;/code&gt; split Apex test classes or test methods?
&lt;/h3&gt;

&lt;p&gt;Classes. All methods of a test class stay together on one shard, so &lt;code&gt;@testSetup&lt;/code&gt; runs once for the class and every method sees the same records, exactly as in an unsharded run. Shards are balanced by recorded class duration, not method count, so a class with a heavy setup counts for what it costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if &lt;code&gt;.nimbus/test-durations.json&lt;/code&gt; is missing?
&lt;/h3&gt;

&lt;p&gt;If no shard has the file, renmy runtime falls back to an even split by class name; the run is still correct, just less evenly balanced. If some shards have it and others do not, they compute different partitions and a class can run twice or be skipped, which is why the file should be committed or restored from the same cache key in every job.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I merge JUnit XML from sharded Apex test runs?
&lt;/h3&gt;

&lt;p&gt;Pass &lt;code&gt;--results-xml&lt;/code&gt; to every shard with its own file name, such as &lt;code&gt;results-2.xml&lt;/code&gt;. When &lt;code&gt;--shard&lt;/code&gt; is in effect, each file carries a &lt;code&gt;shard="N/M"&lt;/code&gt; attribute so a CI aggregator knows the files are slices of one run. Publish per shard with &lt;code&gt;if: always()&lt;/code&gt;, or upload the files as artifacts and publish them together from a job that runs after the matrix.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many shards should an Apex test suite use?
&lt;/h3&gt;

&lt;p&gt;Time the test step alone first. If it is under a minute, use one job and turn on parallel workers with &lt;code&gt;%ci.renmy.test.parallel&lt;/code&gt; instead; each extra shard costs a checkout, an install and a database start-up. If the step runs for several minutes, start with four and add shards while the slowest shard is still much longer than the setup.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>ci</category>
      <category>devops</category>
    </item>
    <item>
      <title>Let your AI agent run Apex tests: MCP for Salesforce</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Sun, 25 Oct 2026 20:56:38 +0000</pubDate>
      <link>https://dev.to/oblitus/let-your-ai-agent-run-apex-tests-mcp-for-salesforce-9f</link>
      <guid>https://dev.to/oblitus/let-your-ai-agent-run-apex-tests-mcp-for-salesforce-9f</guid>
      <description>&lt;p&gt;&lt;em&gt;Nimbus was renamed renmy runtime in October 2026: same tool, same binary. &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;The record of the rename is here.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; AI coding agents are good at Python and TypeScript because they can run what they write and read the result within a second, dozens of times per task. They stall on Salesforce Apex because the only place Apex could run was a Salesforce org, minutes away over the network. renmy runtime is a local Salesforce Apex runtime: it executes real Apex, SOQL, DML and Salesforce Flows on your machine against an embedded PostgreSQL, with no org, no scratch org, no sandbox, no Docker and no JVM. Its &lt;code&gt;renmy mcp&lt;/code&gt; command is an MCP server that gives Claude Code, Cursor, GitHub Copilot and any MCP client tools such as &lt;code&gt;run_apex_tests&lt;/code&gt; and &lt;code&gt;explain_failure&lt;/code&gt;, so the agent gets the same write-run-fix loop it has everywhere else and the org only sees code that has already passed. Deploying stays behind the gates of &lt;code&gt;renmy deploy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why coding agents work in TypeScript and stall on Apex
&lt;/h2&gt;

&lt;p&gt;An AI coding agent is a loop: write a change, run something, read what came back, decide what to do next. In a TypeScript or Python project, "run something" means starting a process on the same machine: &lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, a one-off script. It returns in a second or two with a stack trace the agent can read. An agent goes round that loop twenty or forty times on one task, and what it hands back is code it has watched pass.&lt;/p&gt;

&lt;p&gt;Salesforce Apex breaks the loop at "run something". Apex compiles and executes only inside a Salesforce org: the compiler, the database and the test runner live in Salesforce's data centres, not on the developer's machine. To run one test, an agent has to deploy the class to an org, wait, start the test run, poll until it finishes, then fetch and parse a debug log. That is minutes per cycle, and it requires the agent to hold credentials for an org. The published measurement: four open-source Apex suites, 837 tests, about 1 min 46 s in a Salesforce org for deploy and run (&lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;method and numbers&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Agents respond in one of two ways, and both are bad. Some stop iterating: they write the class and the test, describe what the test "would" check, and hand back code nobody has run. Others keep going and spend minutes and tokens on every round trip. Either way, the one thing an agent is good at, relentless and cheap verification, is the thing Apex took away from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a local Apex runtime changes
&lt;/h3&gt;

&lt;p&gt;renmy runtime is a local Salesforce Apex runtime and developer toolchain. It executes real Apex (classes, interfaces, enums, generics, triggers), SOQL and DML, and Salesforce Flows (autolaunched, record-triggered, screen) on the developer's machine against an embedded PostgreSQL. No Salesforce org, no scratch org, no sandbox, no Docker, no JVM: one binary for macOS, Linux and Windows. It is not a mock and not a transpiler to Java. Governor limits are enforced; &lt;code&gt;Test.startTest&lt;/code&gt;/&lt;code&gt;Test.stopTest&lt;/code&gt;, &lt;code&gt;@testSetup&lt;/code&gt;, per-test transaction isolation, the Stub API and the order of execution with triggers and flows behave as they do on the platform, and the &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;parity record is public&lt;/a&gt;. A typical Apex test completes in tens of milliseconds, SOQL and DML included. The 837-test benchmark above runs in 2.4 s locally, roughly 45 times faster, every test passing in both places.&lt;/p&gt;

&lt;p&gt;For a Java developer this is what the JVM on their laptop has always been. For an AI agent it is the missing half of the loop. If a local runtime is a new idea to you, &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;What a local runtime is, and why Salesforce never gave you one&lt;/a&gt; explains it from zero, and &lt;a href="https://renmy.net/blog/renmy-runtime-explained" rel="noopener noreferrer"&gt;renmy runtime explained&lt;/a&gt; tours the whole toolchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What renmy mcp exposes to an agent
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) is how coding agents call tools. A server describes a set of functions with typed inputs; the client, the agent, calls them over JSON-RPC and reads a structured result. &lt;code&gt;renmy mcp&lt;/code&gt; is such a server. It runs as a child process of the agent and talks over stdio, so there is no port, no network and no credential. The agent reaches the project on the disk in front of it and nothing else.&lt;/p&gt;

&lt;p&gt;The tools are the runtime's own commands with the same engine behind them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_apex_tests&lt;/code&gt;: run a class, a method or a pattern such as &lt;code&gt;"*"&lt;/code&gt; and get the results back as data, not as a log to parse.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;explain_failure&lt;/code&gt;: one failure in full. The exception, the source location, expected versus actual, the SOQL and DML that ran just before it, and what the engine could not establish. It is the same payload &lt;code&gt;renmy explain&lt;/code&gt; prints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_coverage&lt;/code&gt;: line and branch coverage for the run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_execution_trace&lt;/code&gt;: the execution trace of a run, what ran and in what order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execute_anonymous&lt;/code&gt;: run a snippet of Apex against the local database, the agent's equivalent of a REPL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_test_classes&lt;/code&gt;: find out what is already tested before writing more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is in the &lt;a href="https://renmy.net/docs" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt;. The MCP server is a Pro feature, free during the public beta. An agent that can run shell commands, as Claude Code can, gets most of the loop on the Free tier from &lt;code&gt;renmy test&lt;/code&gt; and &lt;code&gt;renmy explain&lt;/code&gt;; the MCP server returns the same results as structured data instead of terminal output.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to connect Claude Code, Cursor or Copilot to renmy runtime
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install the runtime
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that last command yourself once before you hand the project to an agent, so you know the suite runs locally and how long it takes. The &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers the first run in detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register the MCP server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Registers renmy mcp with the agent clients on this machine&lt;/span&gt;
renmy mcp &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Or in Claude Code directly. The server's registered name is nimbus.&lt;/span&gt;
claude mcp add nimbus &lt;span class="nt"&gt;--&lt;/span&gt; renmy mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor reads &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in the project. The entry is the same command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nimbus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"renmy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Copilot's agent mode and any other MCP client take the same stdio command, &lt;code&gt;renmy mcp&lt;/code&gt;. The registered name stayed &lt;code&gt;nimbus&lt;/code&gt; through the rename, so an agent configured before October 2026 keeps working. The how-to &lt;a href="https://renmy.net/how-to/let-ai-agents-run-apex-tests" rel="noopener noreferrer"&gt;Let AI agents run Apex tests&lt;/a&gt; walks through each client and &lt;code&gt;renmy skills&lt;/code&gt;, curated agent workflows for common Salesforce tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent session looks like with a local Apex runtime
&lt;/h2&gt;

&lt;p&gt;Say you tell Claude Code: "Invoices over 10,000 get a 5 per cent discount. Add it to InvoiceService with tests." The loop, with the tool calls named:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;list_test_classes&lt;/code&gt; finds &lt;code&gt;InvoiceServiceTest&lt;/code&gt;. The agent reads it and the service.&lt;/li&gt;
&lt;li&gt;It writes the method and a test:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="nd"&gt;@IsTest&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;appliesFivePerCentAboveThreshold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Invoice__c&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Invoice__c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Amount__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;InvoiceService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyDiscounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Discount__c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Invoice__c&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;Assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Discount__c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;run_apex_tests&lt;/code&gt; on &lt;code&gt;InvoiceServiceTest&lt;/code&gt;. One failure, in well under a second.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;explain_failure&lt;/code&gt; on that method. Expected 600, actual 0, and the SOQL and DML before the assertion show that the service queried the invoice but never issued an update: its query filtered on a status the test never set. The agent fixes the filter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_apex_tests&lt;/code&gt; again. Green. &lt;code&gt;get_coverage&lt;/code&gt; for the class shows branch coverage, which flags the untested path at exactly 10,000, so the agent adds a boundary test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_apex_tests&lt;/code&gt; with &lt;code&gt;"*"&lt;/code&gt;. The whole suite, in a couple of seconds. A change to &lt;code&gt;InvoiceService&lt;/code&gt; that broke a trigger test somewhere else shows up now, not in the org.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each round is seconds. The agent runs the whole suite after every edit because it costs nothing, a discipline most humans drop by the third iteration. Nothing has touched an org. What you review is a diff and a test run you can reproduce in your own terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;renmy &lt;span class="nb"&gt;test &lt;/span&gt;InvoiceServiceTest
renmy explain InvoiceServiceTest.appliesFivePerCentAboveThreshold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Agentforce, Headless 360 and the Apex behind your MCP tools
&lt;/h2&gt;

&lt;p&gt;There is a second MCP in the picture. Salesforce Headless 360 (April 2026) lets a team configure custom MCP servers in Salesforce that wrap existing Apex hooks: &lt;code&gt;@InvocableMethod&lt;/code&gt;, &lt;code&gt;@AuraEnabled&lt;/code&gt;, &lt;code&gt;@RestResource&lt;/code&gt; and autolaunched Flows become tools an Agentforce agent, or an external one, calls in production. Those hooks are ordinary Apex and flow metadata, and renmy runtime already executes all of them locally, so the code behind a Headless 360 tool is tested offline against the embedded PostgreSQL with no extra setup. &lt;a href="https://renmy.net/headless360" rel="noopener noreferrer"&gt;How renmy runtime fits Headless 360&lt;/a&gt; has the detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the agent should stop: deploying
&lt;/h2&gt;

&lt;p&gt;A green local run is necessary and not sufficient. The org has permission sets, managed packages, validation rules and other teams' automation that your repository does not contain, and the parity record is public precisely because it is not 100 per cent. So the agent should not deploy on its own, and the tool for that boundary is &lt;code&gt;renmy deploy&lt;/code&gt;. It snapshots the payload into a bundle, runs the local test and coverage gates against that bundle, validates the identical bytes in the target org with a real Salesforce check-only validation, and only then deploys. It is in the Free tier. &lt;code&gt;renmy release validate&lt;/code&gt; and &lt;code&gt;renmy release deploy&lt;/code&gt; (Pro, free during the beta) split the two halves with an immutable receipt (bundle digest, git revision, org fingerprint, gate results, Salesforce validation job id, quick-deploy expiry) so a human can approve between them. Let the agent run the tests; keep the deploy, or at least the approval, in a human's hands. &lt;a href="https://renmy.net/deploy" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; and &lt;a href="https://renmy.net/release" rel="noopener noreferrer"&gt;Release&lt;/a&gt; describe both commands.&lt;/p&gt;

&lt;p&gt;What stays in the org does not change: the UI, the integrations, the final validation. The runtime is where the hundred edits before that happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Developers using Claude Code, Cursor or GitHub Copilot on a Salesforce codebase who have watched the agent produce Apex it never ran. Teams building Agentforce or Headless 360 tools who want the Apex behind them tested before it reaches an org. AI-tooling leads who need to answer "can our agents work on Salesforce?" with a demonstration rather than a slide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
renmy mcp &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; takes five minutes. The &lt;a href="https://renmy.net/agentic" rel="noopener noreferrer"&gt;AI and agentic coding&lt;/a&gt; page has the integration patterns. Issues, docs, examples and released binaries are at &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;github.com/renmy-solutions/renmy&lt;/a&gt;; questions go to the &lt;a href="https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does an AI agent need a Salesforce org to run Apex tests with renmy runtime?
&lt;/h3&gt;

&lt;p&gt;No. renmy runtime executes Apex, SOQL, DML and Salesforce Flows on the developer's machine against an embedded PostgreSQL. The &lt;code&gt;renmy mcp&lt;/code&gt; server runs as a child process of the agent over stdio, so it needs no org, no credentials, no network and no port. The org is only involved when you deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which AI coding agents work with renmy mcp?
&lt;/h3&gt;

&lt;p&gt;Any MCP client. Claude Code registers it with &lt;code&gt;claude mcp add nimbus -- renmy mcp&lt;/code&gt;, Cursor through its &lt;code&gt;mcp.json&lt;/code&gt;, and GitHub Copilot's agent mode and other clients take the same stdio command, &lt;code&gt;renmy mcp&lt;/code&gt;. &lt;code&gt;renmy mcp install&lt;/code&gt; does the registration for you. The server's registered name is &lt;code&gt;nimbus&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is renmy mcp free?
&lt;/h3&gt;

&lt;p&gt;The MCP server is part of the Pro tier, which is free during the public beta; the list price after the beta is $19 per month. The Free tier, $0 and no card, includes unlimited local test runs, so an agent that can run shell commands already gets the loop from &lt;code&gt;renmy test&lt;/code&gt; and &lt;code&gt;renmy explain&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an AI agent deploy Apex to Salesforce?
&lt;/h3&gt;

&lt;p&gt;Not on its own. A local pass is necessary and not sufficient, because the org holds metadata and automation your repository does not. Deploy through &lt;code&gt;renmy deploy&lt;/code&gt;, which runs the local gates and then a real check-only validation of the same bytes in the org before deploying, or through &lt;code&gt;renmy release validate&lt;/code&gt; and &lt;code&gt;renmy release deploy&lt;/code&gt;, which put a human approval and an immutable receipt between validation and deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is renmy runtime the same as Nimbus?
&lt;/h3&gt;

&lt;p&gt;Yes. Nimbus was renamed renmy runtime in October 2026. It is the same binary from the same repository line; the &lt;code&gt;nimbus&lt;/code&gt; command, the &lt;code&gt;NIMBUS_*&lt;/code&gt; variables, &lt;code&gt;nimbus.properties&lt;/code&gt; and the MCP server's registered name &lt;code&gt;nimbus&lt;/code&gt; all keep working. The record of the rename is at renmy.net/blog/nimbus-is-now-renmy.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>renmy runtime explained: Apex tests, flows and deploys</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Sun, 25 Oct 2026 07:37:10 +0000</pubDate>
      <link>https://dev.to/oblitus/renmy-runtime-explained-apex-tests-flows-and-deploys-2ff1</link>
      <guid>https://dev.to/oblitus/renmy-runtime-explained-apex-tests-flows-and-deploys-2ff1</guid>
      <description>&lt;p&gt;&lt;em&gt;Nimbus was renamed renmy runtime in October 2026. Same tool, same binary; the &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;record of the rename&lt;/a&gt; has the details.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; renmy runtime is a local Salesforce Apex runtime and developer toolchain. It executes real Apex, SOQL, DML and Salesforce Flows on your machine against an embedded PostgreSQL, so an Apex test finishes in tens of milliseconds instead of after an org round trip. Around that runtime sit a language server, a live debugger, line and branch coverage, mutation testing, a browser Dev UI, an MCP server for AI agents, and a gated path to production through &lt;code&gt;renmy deploy&lt;/code&gt; and &lt;code&gt;renmy release&lt;/code&gt;. It is one binary for macOS, Linux and Windows, with no org, no scratch org, no Docker and no JVM. The Free tier covers unlimited local test runs and gated deploys; Pro and Team are free during the public beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You have heard the name Nimbus or renmy, you write Salesforce Apex or maintain the pipeline that deploys it, and you want the whole toolchain in one read rather than twelve product pages. If the idea of a local runtime is new to you, &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;What a local runtime is, and why Salesforce never gave you one&lt;/a&gt; starts from zero. This article assumes you have the idea and want the inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is renmy runtime?
&lt;/h2&gt;

&lt;p&gt;Every other ecosystem gives a developer a runtime on their own machine. A Java developer presses "run test" and the JVM on their laptop answers in about a second; a JavaScript developer types &lt;code&gt;npm test&lt;/code&gt; and Node answers. Salesforce never shipped that for Apex. The compiler, the database and the test runner live in Salesforce's data centres, so the only way to learn whether a class works has been to deploy it to an org, wait, and read the result.&lt;/p&gt;

&lt;p&gt;renmy runtime is the missing piece. It is an interpreter for Salesforce Apex that runs on your machine, backed by an embedded PostgreSQL that stands in for the org's database, and it enforces the platform's rules: governor limits, order of execution with triggers and flows, &lt;code&gt;Test.startTest&lt;/code&gt; and &lt;code&gt;Test.stopTest&lt;/code&gt;, &lt;code&gt;@testSetup&lt;/code&gt;, per-test transaction isolation, the Stub API. It is not a mock library and not a transpiler to Java. It executes your source tree as written.&lt;/p&gt;

&lt;p&gt;Two numbers frame what that buys. A typical Apex test completes in tens of milliseconds, SOQL and DML included. In the published benchmark, four open-source Apex suites with 837 tests took about 1 min 46 s to deploy and run in a Salesforce org and 2.4 s locally, roughly 45 times faster, with every test passing in both places. The &lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;method and numbers are public&lt;/a&gt;, and so is the &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;parity record&lt;/a&gt; that lists what matches the platform and what does not.&lt;/p&gt;

&lt;p&gt;The rest of this article is a tour in six stops: run, edit, test, debug, integrate, ship. Each stop has one command block. Everything is free unless marked Pro, and Pro is free during the public beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does renmy runtime run Apex without an org?
&lt;/h2&gt;

&lt;p&gt;Install one binary, step into an SFDX project, run the tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;                        &lt;span class="c"&gt;# every test in the project&lt;/span&gt;
renmy &lt;span class="nb"&gt;test &lt;/span&gt;InvoiceServiceTest         &lt;span class="c"&gt;# one class&lt;/span&gt;
renmy &lt;span class="nb"&gt;test &lt;/span&gt;InvoiceServiceTest.bulk    &lt;span class="c"&gt;# one method&lt;/span&gt;
renmy &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"System.debug([SELECT COUNT() FROM Account]);"&lt;/span&gt;   &lt;span class="c"&gt;# anonymous Apex, local database&lt;/span&gt;
renmy validate                        &lt;span class="c"&gt;# compile and type-check, run nothing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;code&gt;renmy test&lt;/code&gt;, classes, interfaces, enums, generics and triggers execute as Apex; SOQL is translated and run against PostgreSQL; DML fires the triggers on the object and the record-triggered flows on it, in the platform's order. Autolaunched, record-triggered and screen flows run from their metadata, so a Salesforce Flow and the trigger that shares its object are tested together, which is where order-of-execution bugs live. &lt;a href="https://renmy.net/flows" rel="noopener noreferrer"&gt;Flows&lt;/a&gt; and &lt;a href="https://renmy.net/governor-limits" rel="noopener noreferrer"&gt;governor limits&lt;/a&gt; each have their own page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which editors does renmy runtime work in?
&lt;/h2&gt;

&lt;p&gt;The runtime ships a language server, and every editor integration is built on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The Apex language server, for any LSP client&lt;/span&gt;
renmy lsp

&lt;span class="c"&gt;# Or install renmy runtime as a Salesforce CLI plugin&lt;/span&gt;
sf plugins &lt;span class="nb"&gt;install&lt;/span&gt; @nimbus-solution/nimbus-sf-plugin
sf nimbus apex run &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Completion, hover, go to definition, rename, diagnostics, code lenses and inlay hints work in VS Code, Cursor, Windsurf, Neovim, Zed, Helix, JetBrains IDEs and Emacs. The &lt;a href="https://renmy.net/vscode" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; is on the Microsoft Marketplace and Open VSX, so it installs in Cursor, Windsurf and VSCodium as well. For IntelliJ IDEA there is &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt; on the JetBrains Marketplace: run icons in the gutter on every test, a native debug session against the local runtime, Apex and SOQL consoles, and it works in the Community edition. &lt;a href="https://renmy.net/blog/renmy-runtime-for-jetbrains-salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime for JetBrains&lt;/a&gt; walks through it. Both plugins run on the backends the CLI ships; nothing is reimplemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does renmy runtime measure about your tests?
&lt;/h2&gt;

&lt;p&gt;Passing is the lowest bar. The test stop measures whether the tests are any good.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;--coverage&lt;/span&gt;     &lt;span class="c"&gt;# line and branch coverage; HTML, JSON, Cobertura, JUnit XML&lt;/span&gt;
renmy coverage diff           &lt;span class="c"&gt;# what this change did to coverage, for the pull request&lt;/span&gt;
renmy fixture                 &lt;span class="c"&gt;# generate a test data factory class from the schema&lt;/span&gt;
renmy &lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;--flaky&lt;/span&gt;         &lt;span class="c"&gt;# tests that pass and fail without a code change&lt;/span&gt;
renmy &lt;span class="nb"&gt;test&lt;/span&gt;:watch              &lt;span class="c"&gt;# re-run affected tests on save (Pro)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coverage is measured for lines and branches; the org reports lines only. Reports come out as HTML, JSON, Cobertura XML and JUnit XML, and a &lt;code&gt;--min-coverage&lt;/code&gt; gate fails a deploy or release below the bar. &lt;a href="https://renmy.net/mutation" rel="noopener noreferrer"&gt;Mutation testing&lt;/a&gt; (Pro) goes further than coverage can: it changes your code in small ways and checks whether the tests notice. To our knowledge it is the first for Apex. &lt;code&gt;renmy fixture&lt;/code&gt; reads required fields and lookups from the local schema and writes a test data factory class, and &lt;code&gt;renmy history --flaky&lt;/code&gt; finds tests that flip without a code change. &lt;a href="https://renmy.net/coverage" rel="noopener noreferrer"&gt;Coverage&lt;/a&gt; has the formats and flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you debug Apex with renmy runtime?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;renmy explain InvoiceServiceTest.bulk   &lt;span class="c"&gt;# one failure in full, and what could not be established&lt;/span&gt;
renmy triage                            &lt;span class="c"&gt;# a run's failures, grouped by recorded cause&lt;/span&gt;
renmy graph                             &lt;span class="c"&gt;# what a change to a class can reach&lt;/span&gt;
renmy dev                               &lt;span class="c"&gt;# browser test runner, coverage, schema, Apex REPL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://renmy.net/debugger" rel="noopener noreferrer"&gt;debugger&lt;/a&gt; is live, over the Debug Adapter Protocol. Set a breakpoint in a &lt;code&gt;.cls&lt;/code&gt; or &lt;code&gt;.trigger&lt;/code&gt; file in VS Code or a JetBrains IDE, run the test, and it stops there with variables per frame, stepping and watches, while the test is running. That is not the same as replaying a debug log afterwards. Recording is free; stepping is Pro.&lt;/p&gt;

&lt;p&gt;When you would rather read than step, &lt;code&gt;renmy explain&lt;/code&gt; reports one failure with the exception, the source location, expected versus actual and the SOQL and DML just before it, and says what it could not establish. &lt;code&gt;renmy triage&lt;/code&gt; groups a whole run's failures by the cause the engine recorded, never by matching message text. &lt;code&gt;renmy graph&lt;/code&gt; shows what a change to a class can reach. &lt;code&gt;renmy dev&lt;/code&gt; opens the &lt;a href="https://renmy.net/dev-ui" rel="noopener noreferrer"&gt;Dev UI&lt;/a&gt; in a browser, with a test runner, a coverage explorer, a schema browser and an Apex REPL, no editor required. Structured execution traces are Pro.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do AI agents and other tools connect to renmy runtime?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Register the MCP server with Claude Code&lt;/span&gt;
claude mcp add nimbus &lt;span class="nt"&gt;--&lt;/span&gt; renmy mcp

&lt;span class="c"&gt;# Or register it with the clients renmy knows&lt;/span&gt;
renmy mcp &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# A Salesforce-compatible REST and gRPC Pub/Sub API on localhost (Pro)&lt;/span&gt;
renmy serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coding agents work in Python and TypeScript because they can write, run and fix in a loop measured in seconds. On Apex they stalled, because the only runtime was an org away. &lt;code&gt;renmy mcp&lt;/code&gt; is a Model Context Protocol server with tools such as &lt;code&gt;run_apex_tests&lt;/code&gt;, &lt;code&gt;execute_anonymous&lt;/code&gt;, &lt;code&gt;get_coverage&lt;/code&gt; and &lt;code&gt;explain_failure&lt;/code&gt;, so Claude Code, Cursor, Copilot or any MCP client can run and verify Apex on your machine, and the org only sees code that has already passed. The MCP server is part of Pro, free during the beta. &lt;a href="https://renmy.net/blog/ai-agents-for-salesforce-apex" rel="noopener noreferrer"&gt;Let your AI agent run Apex tests&lt;/a&gt; covers the setup and what a session looks like; the &lt;a href="https://renmy.net/agentic" rel="noopener noreferrer"&gt;agentic coding page&lt;/a&gt; has the patterns.&lt;/p&gt;

&lt;p&gt;Beyond agents, &lt;code&gt;renmy serve&lt;/code&gt; (Pro) exposes a Salesforce-compatible REST and gRPC Pub/Sub API on localhost, and &lt;code&gt;renmy app&lt;/code&gt; (Pro) hosts an LWC or Vite UI bundle against the local runtime, so a front end runs offline against real Apex. The daemon (Pro) keeps all of it warm between runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does renmy runtime deploy to Salesforce?
&lt;/h2&gt;

&lt;p&gt;The inner loop proves the code. The org still has to receive it, and renmy runtime owns that path too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Gated one-shot deploy: snapshot, local gates, org check-only validation, deploy. Free.&lt;/span&gt;
renmy deploy

&lt;span class="c"&gt;# Split it: validate now, approve, deploy the validated payload later, with a receipt (Pro)&lt;/span&gt;
renmy release validate
renmy release deploy

&lt;span class="c"&gt;# Read the org deploy queue, and resubmit a stuck job&lt;/span&gt;
renmy release status
renmy release watch
renmy release requeue

&lt;span class="c"&gt;# Any Salesforce CLI command, unchanged; and pin the CLI version for CI&lt;/span&gt;
renmy sf org list
renmy toolchain sf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;renmy deploy&lt;/code&gt; snapshots the selected source into a bundle, runs local validation and the configured test and coverage gates against that bundle, validates the identical bytes in the target org with a real Salesforce check-only validation, and only then deploys. It is Free, because the safe default should not sit behind a paywall. &lt;code&gt;renmy release validate&lt;/code&gt; and &lt;code&gt;renmy release deploy&lt;/code&gt; (Pro) split validation from deployment and write an immutable receipt: bundle digest, git revision, org fingerprint, gate results, the Salesforce validation job id and the quick-deploy expiry. A team can validate on one runner, hold for approval, and quick-deploy the exact validated payload from another machine. &lt;code&gt;renmy release status&lt;/code&gt;, &lt;code&gt;watch&lt;/code&gt; and &lt;code&gt;requeue&lt;/code&gt; read and recover the org's deploy queue. &lt;code&gt;renmy sf &amp;lt;anything&amp;gt;&lt;/code&gt; passes any Salesforce CLI command through with arguments, prompts and exit codes intact, and &lt;code&gt;renmy toolchain sf&lt;/code&gt; pins the CLI version so CI runners reproduce the toolchain. &lt;a href="https://renmy.net/deploy" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; and &lt;a href="https://renmy.net/release" rel="noopener noreferrer"&gt;release&lt;/a&gt; have the full detail; &lt;a href="https://renmy.net/blog/salesforce-ci-without-scratch-orgs" rel="noopener noreferrer"&gt;Salesforce CI without scratch orgs&lt;/a&gt; shows the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  I already have a sandbox. Why would I need this?
&lt;/h2&gt;

&lt;p&gt;Keep the sandbox. It is still where the deploy lands, where a user clicks through a Lightning page and where an integration is checked end to end. What a sandbox is bad at is the hundred small edits before that: each one costs a deploy, a wait and a log, and a shared sandbox runs everyone else's automation alongside yours. renmy runtime takes that loop off the org. You edit, the test answers in milliseconds, and when the class is right, &lt;code&gt;renmy deploy&lt;/code&gt; validates the same bytes in the sandbox or in production before anything lands. Org-shaped work stays in orgs; code-shaped work moves to your machine. A scratch org has the same shape, with an expiry date added; &lt;a href="https://renmy.net/blog/the-scratch-org-tax" rel="noopener noreferrer"&gt;the scratch org tax&lt;/a&gt; prices it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is free and what is Pro?
&lt;/h2&gt;

&lt;p&gt;Free, for individual developers, $0 and no card: unlimited local test runs, gated deploys with &lt;code&gt;renmy deploy&lt;/code&gt;, Salesforce CLI pass-through and the IDE integrations, on one machine. Pro adds the daemon, &lt;code&gt;renmy serve&lt;/code&gt;, parallel runs, watch mode, debugger stepping, mutation testing and the MCP server, on three machines. Team adds CI runs, a CI runner licence, the on-prem assurance console and seats. Pro and Team are free during the public beta; the list prices are $19 per month and $49 per developer per month. Enterprise starts at $20,000 per year. The &lt;a href="https://renmy.net/#pricing" rel="noopener noreferrer"&gt;pricing section&lt;/a&gt; is the current word.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, the &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; for issues, docs and releases, and for IntelliJ IDEA the &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;JetBrains plugin&lt;/a&gt;. Questions go to the &lt;a href="https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is renmy runtime the same thing as Nimbus?
&lt;/h3&gt;

&lt;p&gt;Yes. Nimbus was renamed renmy runtime in October 2026. It is the same tool, the same binary and the same repository line, and the &lt;code&gt;nimbus&lt;/code&gt; command, the &lt;code&gt;NIMBUS_*&lt;/code&gt; variables, &lt;code&gt;nimbus.properties&lt;/code&gt;, &lt;code&gt;~/.nimbus&lt;/code&gt; and the &lt;code&gt;.nimbus/&lt;/code&gt; directory in a project all keep working. The record of the rename is at &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;https://renmy.net/blog/nimbus-is-now-renmy&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does renmy runtime need a Salesforce org, Docker or a JVM?
&lt;/h3&gt;

&lt;p&gt;No. renmy runtime is one binary for macOS, Linux and Windows. It executes Apex, SOQL, DML and flows against an embedded PostgreSQL on your machine. An org is needed only when you deploy to it or pass a command through to the Salesforce CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is renmy runtime a mock?
&lt;/h3&gt;

&lt;p&gt;No. It executes real Apex against a real database, with governor limits, order of execution with triggers and flows, Test.startTest and Test.stopTest, @testSetup and per-test transaction isolation enforced. Where it differs from the platform is recorded publicly in the parity record at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;https://renmy.net/parity&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does renmy runtime cost?
&lt;/h3&gt;

&lt;p&gt;The Free tier is $0 with no card and covers unlimited local test runs, gated deploys, Salesforce CLI pass-through and the IDE integrations on one machine. Pro and Team are free during the public beta; their list prices are $19 per month and $49 per developer per month. Enterprise starts at $20,000 per year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which editors does renmy runtime support?
&lt;/h3&gt;

&lt;p&gt;VS Code, Cursor, Windsurf and VSCodium through the VS Code extension; IntelliJ IDEA and the other JetBrains IDEs through the Renmy Runtime - Salesforce IDE plugin; and Neovim, Zed, Helix and Emacs through the language server, &lt;code&gt;renmy lsp&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Salesforce IDE in IntelliJ IDEA: renmy runtime for JetBrains</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Sat, 24 Oct 2026 18:29:17 +0000</pubDate>
      <link>https://dev.to/oblitus/salesforce-ide-in-intellij-idea-renmy-runtime-for-jetbrains-3bp2</link>
      <guid>https://dev.to/oblitus/salesforce-ide-in-intellij-idea-renmy-runtime-for-jetbrains-3bp2</guid>
      <description>&lt;p&gt;&lt;em&gt;Nimbus was renamed renmy runtime in October 2026, and the JetBrains plugin that was listed as "Nimbus - Local Apex Runtime" is the same plugin under its new name; the record of the rename is at &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;https://renmy.net/blog/nimbus-is-now-renmy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; renmy runtime is a local Salesforce Apex runtime: it executes real Apex, SOQL, DML and Salesforce Flows on your machine against an embedded PostgreSQL, with no Salesforce org, no scratch org, no Docker and no JVM. Its JetBrains plugin, &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt; (version 1.15.1), puts that runtime inside IntelliJ IDEA Community or Ultimate: a run icon on every Apex test, a native debugger with breakpoints, Apex and SOQL consoles against the local runtime or any org, and one tool window with six workspaces. It installs its own runtime on first open of a Salesforce DX project, the Free tier needs no account, and it coexists with Illuminated Cloud.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a Java developer expects from an IDE, and what Apex developers never had
&lt;/h2&gt;

&lt;p&gt;Open IntelliJ IDEA on a Java project. A run icon sits in the gutter next to every test; click it and the JVM on your laptop answers in under a second. Set a breakpoint, press Debug, and execution stops on that line with every variable in view. Open a database console, type a query, read the rows. None of this is remarkable; it is what an IDE is for.&lt;/p&gt;

&lt;p&gt;Now open a Salesforce DX project in the same IDE. The Apex you write cannot run on your machine, because the compiler, the database and the test runner all live inside a Salesforce org. So the workflow became: save, deploy to a sandbox or scratch org, wait, run the test there, download a debug log, read it. The gutter icon has nowhere to send the test. The breakpoint has nothing to stop.&lt;/p&gt;

&lt;p&gt;A local runtime is the missing piece: a program on your machine that executes Apex the way the platform does, SOQL and DML included, against a real database it brings along. That is what renmy runtime is, and the JetBrains plugin wires it into IntelliJ IDEA. &lt;a href="https://renmy.net/blog/what-is-a-local-apex-runtime" rel="noopener noreferrer"&gt;What a local runtime is, and why Salesforce never gave you one&lt;/a&gt; explains the idea from zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Java and Kotlin developers who also touch Salesforce and want their ordinary workflow back; Salesforce developers already in IntelliJ IDEA with Illuminated Cloud; teams standardised on JetBrains who do not want a second editor for one language. &lt;a href="https://renmy.net/blog/salesforce-for-java-developers" rel="noopener noreferrer"&gt;Coming to Salesforce from Java: where did my test runner go?&lt;/a&gt; is the companion piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is renmy runtime for JetBrains?
&lt;/h2&gt;

&lt;p&gt;The plugin is listed on the JetBrains Marketplace as &lt;strong&gt;Renmy Runtime - Salesforce IDE&lt;/strong&gt;: &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide&lt;/a&gt;. The current version is 1.15.1 (October 2026). Before 1.15 the listing was called "Nimbus - Local Apex Runtime" (1.0.x); an installed 1.0.x updates in place. The product page is &lt;a href="https://renmy.net/jetbrains" rel="noopener noreferrer"&gt;https://renmy.net/jetbrains&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What it needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IntelliJ IDEA Community or Ultimate, or any JetBrains IDE from 2025.3 that ships JCEF.&lt;/li&gt;
&lt;li&gt;No JDK of your own, and no Salesforce org for the local loop.&lt;/li&gt;
&lt;li&gt;LSP4IJ, which the Marketplace installs for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything the IDE does runs on the same two backends the CLI ships, &lt;code&gt;renmy lsp&lt;/code&gt; for language features and &lt;code&gt;renmy daemon&lt;/code&gt; for execution. Nothing is reimplemented in the plugin, so a test that passes in the IDE passes the same way under &lt;code&gt;renmy test&lt;/code&gt; in a terminal or in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install the Salesforce IDE plugin in IntelliJ IDEA
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install from Settings → Plugins
&lt;/h3&gt;

&lt;p&gt;Open Settings → Plugins, search for "Renmy Runtime", install, and restart if the IDE asks. Or open the &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Marketplace page&lt;/a&gt; and press Install.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Open a Salesforce DX project
&lt;/h3&gt;

&lt;p&gt;On first open of a project with an &lt;code&gt;sfdx-project.json&lt;/code&gt;, the plugin installs its runtime into &lt;code&gt;~/.nimbus/bin&lt;/code&gt; (macOS, Linux and Windows). The Free tier needs no account: no sign-up, no card, one machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run a test
&lt;/h3&gt;

&lt;p&gt;Open any &lt;code&gt;@IsTest&lt;/code&gt; class. There is a run icon in the gutter. Click it.&lt;/p&gt;

&lt;p&gt;If you also want the runtime in a terminal, the CLI is the same binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The tool window: six workspaces
&lt;/h2&gt;

&lt;p&gt;One tool window, six workspaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Home&lt;/strong&gt;: the runtime, project health, your account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Develop&lt;/strong&gt;: Apex and SOQL consoles, Schema, Workbench, Events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Org&lt;/strong&gt;: Browse, Drift, Logs, Data Loader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt;: Tests, Coverage, History, Governor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release&lt;/strong&gt;: deploys and releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin&lt;/strong&gt;: Users, Permissions, Tracing, Setup, Limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Opening a Salesforce file selects the panel that belongs to it without taking focus; the cursor stays in the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Apex tests from the gutter in IntelliJ IDEA
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;@IsTest&lt;/code&gt; class and method gets a run icon in the gutter and four code lenses: Run, Debug, Coverage and Trace. Results stream into IntelliJ's own test runner tree. After a run, each method shows its time and its SOQL and DML counts inline, so a method that issued forty queries announces itself before a governor limit does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="nd"&gt;@IsTest&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceTotalsTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@TestSetup&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;makeData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Local Test Account'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@IsTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sumsOpenLines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;Invoice__c&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Invoice__c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Status__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Open'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Invoice_Line__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Invoice_Line__c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoice__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Invoice_Line__c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoice__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InvoiceTotals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;Assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click Run on &lt;code&gt;sumsOpenLines&lt;/code&gt; and the test executes on your machine: &lt;code&gt;@TestSetup&lt;/code&gt; runs, the DML lands in the embedded PostgreSQL, any triggers on &lt;code&gt;Invoice__c&lt;/code&gt; fire in the platform's order of execution, &lt;code&gt;Test.startTest&lt;/code&gt; resets the limits, and the result appears in the test tree in tens of milliseconds. Tests can also run in an org from the same place.&lt;/p&gt;

&lt;p&gt;Governor limits are enforced; &lt;code&gt;Test.startTest/stopTest&lt;/code&gt;, &lt;code&gt;@TestSetup&lt;/code&gt;, per-test transaction isolation, the Stub API and the order of execution across triggers and flows behave as on the platform. It is not a mock and not a transpiler. The parity record is public at &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;https://renmy.net/parity&lt;/a&gt;; read it before you trust any claim, including this one. The published benchmark at &lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;https://renmy.net/benchmarks&lt;/a&gt; ran four open-source Apex suites, 837 tests, in about 1 min 46 s in a Salesforce org (deploy and run) and in 2.4 s locally, every test passing in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Apex with breakpoints, not debug logs
&lt;/h2&gt;

&lt;p&gt;Click Debug instead of Run and IntelliJ opens a native debug session against the local runtime. Breakpoints go in &lt;code&gt;.cls&lt;/code&gt; and &lt;code&gt;.trigger&lt;/code&gt; files. Step over, into and out. Variables per frame, watches, evaluate, break on exceptions. It is the debugger Java developers already know, pointed at Apex, and it stops a running test rather than replaying a log.&lt;/p&gt;

&lt;p&gt;Replay adds Step Back, Reverse Continue and a Run Timeline, so a recorded run can be walked backwards to the statement that set the wrong value. Recording is free; stepping is Pro, free during the public beta. Details: &lt;a href="https://renmy.net/debugger" rel="noopener noreferrer"&gt;https://renmy.net/debugger&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apex and SOQL consoles: local runtime or any org
&lt;/h2&gt;

&lt;p&gt;The Develop workspace has an Apex console and a SOQL console, both with completion and diagnostics, each running against the local runtime or any org the Salesforce CLI is logged in to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Account__r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Amount__c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Invoice_Line__c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Invoice__r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Open'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;Account__r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things an org alone cannot do. "Run and Compare" runs the same code locally and in the org and shows what differs. "Find in All Orgs" runs one query in every org you are logged in to, so "which sandbox has this record" stops being a tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data: Workbench, and a Data Loader that simulates first
&lt;/h2&gt;

&lt;p&gt;Workbench queries and edits local rows, keeps data branches, and clones an org's data into a local copy, so tests and consoles run against realistic records without touching the org.&lt;/p&gt;

&lt;p&gt;Data Loader maps a CSV onto an object and simulates the load locally first: triggers and validation rules run against the embedded database and the transaction is rolled back. You see which rows would be rejected before one reaches the org. Then it uploads. Anything that writes to an org asks first and names the org.&lt;/p&gt;

&lt;h2&gt;
  
  
  Org: Drift, Browse and debug-log replay
&lt;/h2&gt;

&lt;p&gt;Browse an org and retrieve what the project lacks. Drift marks every file as in sync, differs, org only or local only; "Compare with Org" opens a diff, and "Retrieve from Org" refuses to overwrite uncommitted work. Logs reads the org's debug logs and replays them locally, so a failure that only happened in the org is not only a text file. Admin shows users, permissions, trace flags and limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing: inspections, refactorings and metadata as forms
&lt;/h2&gt;

&lt;p&gt;The language features come from &lt;code&gt;renmy lsp&lt;/code&gt;: completion, hover docs, go to definition, find references, rename, diagnostics and inlay hints. On top of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sixteen inspections with quick-fixes:&lt;/strong&gt; SOQL, DML or callouts in loops, hardcoded Ids, empty catch blocks, &lt;code&gt;seeAllData&lt;/code&gt;, tests without assertions, unused locals, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate:&lt;/strong&gt; constructor, getters and setters, implement and override, &lt;code&gt;toString&lt;/code&gt;, &lt;code&gt;equals&lt;/code&gt; and &lt;code&gt;hashCode&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactor:&lt;/strong&gt; Extract Variable, Extract Constant, Extract Method, Change Signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename a custom field everywhere:&lt;/strong&gt; across Apex, SOQL, metadata XML, LWC, Aura and Visualforce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nineteen live templates:&lt;/strong&gt; &lt;code&gt;tm&lt;/code&gt;, &lt;code&gt;soqlf&lt;/code&gt;, &lt;code&gt;bulkt&lt;/code&gt;, &lt;code&gt;trycatch&lt;/code&gt;, &lt;code&gt;mapid&lt;/code&gt; and others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go to Test&lt;/strong&gt; and &lt;strong&gt;Create New Test&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Metadata opens as forms: objects, fields, permission sets, labels, custom metadata, page layouts, Lightning pages and more. &lt;code&gt;sfdx-project.json&lt;/code&gt; gets a Form tab beside the text.&lt;/p&gt;

&lt;p&gt;Previews (Pro, free during the beta) render React, LWC and Visualforce from your source against the local API server. Visualforce renders 25 of the 77 &lt;code&gt;apex:&lt;/code&gt; components; the rest are drawn as labelled placeholders, not faked. Previews need Node 20.19+ or 22.12+. Renmy Learn adds ten replayable workshops inside the editor and a searchable field manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it work with Illuminated Cloud?
&lt;/h2&gt;

&lt;p&gt;Yes. Illuminated Cloud handles editing and org deployment for many IntelliJ teams; renmy runtime adds the local runtime and test loop beside it. Shortcuts live in the Alt+Shift namespace so nothing collides: Alt+Shift+D deploys, Alt+Shift+U retrieves. When Illuminated Cloud is installed it keeps the Apex file types, and a setting under Tools → Renmy decides. Guide: &lt;a href="https://renmy.net/how-to/intellij-illuminated-cloud" rel="noopener noreferrer"&gt;https://renmy.net/how-to/intellij-illuminated-cloud&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  I already have a sandbox. Why would I need this?
&lt;/h2&gt;

&lt;p&gt;The sandbox is still where you deploy, where the UI lives and where integrations are checked. The runtime is where the hundred edits before that happen, each costing a click instead of a deploy and a log. When the code is right, &lt;code&gt;renmy deploy&lt;/code&gt; (Free) snapshots the payload, runs the local gates, validates the same bytes in the org with a real check-only validation, and only then deploys. The org sees proven code, less often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Plugin: &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Quickstart: &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;https://renmy.net/quickstart&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub (issues, docs, examples, binaries): &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;https://github.com/renmy-solutions/renmy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Slack: &lt;a href="https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;https://join.slack.com/t/renmy/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does renmy runtime for JetBrains work in IntelliJ IDEA Community Edition?
&lt;/h3&gt;

&lt;p&gt;Yes. The plugin works in IntelliJ IDEA Community and Ultimate, and in any JetBrains IDE from 2025.3 that ships JCEF. You do not need a JDK of your own. LSP4IJ, which the plugin builds on, is installed for you by the Marketplace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a Salesforce org to run Apex tests in IntelliJ IDEA?
&lt;/h3&gt;

&lt;p&gt;Not for the local loop. The plugin installs renmy runtime into ~/.nimbus/bin the first time you open a Salesforce DX project and runs Apex tests against an embedded PostgreSQL on your machine. You need an org for Browse, Drift, running tests in an org, Data Loader uploads and deploys. Anything that writes to an org asks first and names the org.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the plugin work alongside Illuminated Cloud?
&lt;/h3&gt;

&lt;p&gt;Yes. The two are complementary. Shortcuts live in the Alt+Shift namespace so nothing collides (Alt+Shift+D deploys, Alt+Shift+U retrieves), and when Illuminated Cloud is installed it keeps the Apex file types, with a setting under Tools → Renmy to decide. The guide is at &lt;a href="https://renmy.net/how-to/intellij-illuminated-cloud" rel="noopener noreferrer"&gt;https://renmy.net/how-to/intellij-illuminated-cloud&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happened to the Nimbus - Local Apex Runtime plugin?
&lt;/h3&gt;

&lt;p&gt;It is the same plugin. Nimbus was renamed renmy runtime in October 2026, and the JetBrains listing became Renmy Runtime - Salesforce IDE with version 1.15. An installed 1.0.x updates in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the JetBrains plugin free?
&lt;/h3&gt;

&lt;p&gt;The Free tier is for individual developers: $0, no card, no account, with unlimited local test runs, gated deploys, CLI pass-through and IDE integration on one machine. Pro adds the daemon, parallel runs, watch mode, debugger stepping, mutation testing, previews and the MCP server on three machines; it is free during the public beta, with a list price of $19 a month afterwards. Pricing is at &lt;a href="https://renmy.net/#pricing" rel="noopener noreferrer"&gt;https://renmy.net/#pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>intellij</category>
      <category>jetbrains</category>
    </item>
    <item>
      <title>What a local Apex runtime is, and why you never had one</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Thu, 22 Oct 2026 22:43:55 +0000</pubDate>
      <link>https://dev.to/oblitus/what-a-local-apex-runtime-is-and-why-you-never-had-one-c4</link>
      <guid>https://dev.to/oblitus/what-a-local-apex-runtime-is-and-why-you-never-had-one-c4</guid>
      <description>&lt;p&gt;&lt;em&gt;Nimbus was renamed renmy runtime in October 2026. Same tool, same binary; &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;the record of the rename is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; A local Apex runtime is a program that executes Salesforce Apex, SOQL and DML on your own machine, with no Salesforce org involved. A Java developer's tests run on the JVM on their laptop; a Salesforce developer's run in an org, after a deploy. renmy runtime, a local Salesforce Apex runtime runs Apex classes, triggers and Salesforce Flows against an embedded PostgreSQL, enforces governor limits and order of execution, and completes a typical Apex test in tens of milliseconds. It does not replace the org; it replaces the hundred round trips before you deploy to it, and it is free for individual developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How developers run tests everywhere except Salesforce
&lt;/h2&gt;

&lt;p&gt;A Java developer saves a file, clicks the run icon beside a test method in IntelliJ IDEA, and the JVM on their laptop compiles and executes it. Green or red in about a second. A JavaScript developer types &lt;code&gt;npm test&lt;/code&gt;; a Go developer types &lt;code&gt;go test ./...&lt;/code&gt;. In every case the compiler, the runtime and the test runner are programs on the developer's own machine. Nothing is deployed anywhere for code to run.&lt;/p&gt;

&lt;p&gt;If you have only ever written Salesforce Apex, you have never done that. Your loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Save the class, then:&lt;/span&gt;
sf project deploy start &lt;span class="nt"&gt;--source-dir&lt;/span&gt; force-app             &lt;span class="c"&gt;# send the edit to an org, which compiles it&lt;/span&gt;
sf apex run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--tests&lt;/span&gt; AccountTierServiceTest &lt;span class="nt"&gt;--wait&lt;/span&gt; 10  &lt;span class="c"&gt;# queue the test and wait&lt;/span&gt;
sf apex get log &lt;span class="nt"&gt;--number&lt;/span&gt; 1                                 &lt;span class="c"&gt;# read what happened&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three round trips to the org to learn whether one assertion holds. The cycle takes a minute or two on a quiet org, longer on a busy one with a shared test queue. The answer comes back as a debug log to read, not a running process to inspect. Most Salesforce developers have never experienced anything else, so it does not register as a cost. Everywhere else in software it would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Salesforce never gave you a local Apex runtime
&lt;/h2&gt;

&lt;p&gt;Apex has only ever run on Salesforce's servers. There is no compiler to download and no runtime to start on a laptop. The language, the database its SOQL reads, the test runner and the governor limits are one system, and it lives in Salesforce's data centers.&lt;/p&gt;

&lt;p&gt;The reasons are architectural. Apex is multi-tenant by design: your code shares a server with thousands of other customers, which is why governor limits exist at all. And it is inseparable from the platform's data model, since every &lt;code&gt;insert&lt;/code&gt; runs triggers, validation rules, flows and sharing in a fixed order of execution. Everything a Salesforce developer finds slow about Apex follows from that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every edit costs a round trip.&lt;/strong&gt; Nothing compiles until it reaches an org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback arrives in minutes, not milliseconds.&lt;/strong&gt; The published benchmark ran four open-source Apex suites, 837 tests, in about 1 min 46 s in a Salesforce org (deploy plus run) and in 2.4 s locally, roughly 45× faster, every test passing in both (&lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;method and numbers&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI needs an org.&lt;/strong&gt; A JWT certificate, a Dev Hub, a scratch org pool or a CI sandbox, just to run unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An outage stops development.&lt;/strong&gt; When the org is unavailable, nobody can run a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI coding agents cannot iterate.&lt;/strong&gt; An agent is good at Python because it runs the tests every few seconds; for Apex, the only runtime was a deploy away.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a local Apex runtime is
&lt;/h2&gt;

&lt;p&gt;A local Apex runtime is a program on your machine that does what the org does when it runs your code, without the org. It has three parts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An interpreter for Apex.&lt;/strong&gt; It parses your &lt;code&gt;.cls&lt;/code&gt; and &lt;code&gt;.trigger&lt;/code&gt; files and executes them: classes, interfaces, enums, generics, triggers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real database.&lt;/strong&gt; SOQL and DML need somewhere to go. renmy runtime starts an embedded PostgreSQL as a local process and runs your queries and inserts against it. &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;[SELECT ...]&lt;/code&gt;, savepoints and rollbacks are real operations on real rows (&lt;a href="https://renmy.net/why-postgres" rel="noopener noreferrer"&gt;why PostgreSQL&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The platform's rules.&lt;/strong&gt; This is what makes it an Apex runtime rather than an interpreter for a Java-like language. Governor limits are counted and enforced (&lt;a href="https://renmy.net/governor-limits" rel="noopener noreferrer"&gt;governor limits&lt;/a&gt;). Triggers and flows fire in Salesforce's order of execution. &lt;code&gt;Test.startTest()&lt;/code&gt; and &lt;code&gt;Test.stopTest()&lt;/code&gt;, &lt;code&gt;@testSetup&lt;/code&gt;, per-test transaction isolation and the Stub API behave as they do in an org. Salesforce Flows (autolaunched, record-triggered and screen) run from their metadata, next to the Apex triggers on the same object (&lt;a href="https://renmy.net/flows" rel="noopener noreferrer"&gt;flows&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Take a test class you have written a version of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="nd"&gt;@IsTest&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountTierServiceTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@IsTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;assignsEnterpriseTierAboveThreshold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Northwind Traders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AnnualRevenue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;AccountTierService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assignTiers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Tier__c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;Assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Enterprise'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Tier__c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run locally, &lt;code&gt;insert acc&lt;/code&gt; writes a row to the embedded database and fires every Account trigger and record-triggered flow in your project, with their SOQL and DML counted against the limits. &lt;code&gt;Test.startTest()&lt;/code&gt; resets those limits. The query reads the row back from the same database, and when the method ends its transaction is rolled back, so the next test starts clean. The whole thing takes tens of milliseconds, and nothing left your machine. Standard objects are built in; custom objects and fields such as &lt;code&gt;Tier__c&lt;/code&gt; are read from the metadata in your project (&lt;a href="https://renmy.net/configuration" rel="noopener noreferrer"&gt;configuration&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a local Apex runtime is not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not a mock.&lt;/strong&gt; A mocking framework replaces the database with stubs you write by hand so that a unit test can avoid DML. It is a legitimate technique and the opposite of a runtime: the runtime executes the real DML, SOQL and triggers and tells you whether they do what you think (&lt;a href="https://renmy.net/compare/apexmocks" rel="noopener noreferrer"&gt;runtime vs mocking&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a scratch org.&lt;/strong&gt; A scratch org is a real Salesforce org, provisioned on demand in Salesforce's cloud. It is still an org: you deploy to it, wait for its test queue and rebuild it when it expires (&lt;a href="https://renmy.net/compare/scratch-orgs" rel="noopener noreferrer"&gt;renmy runtime vs scratch orgs&lt;/a&gt;; &lt;a href="https://renmy.net/blog/the-scratch-org-tax" rel="noopener noreferrer"&gt;what the rebuild costs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a transpiler.&lt;/strong&gt; It does not translate your Apex into Java or JavaScript and run that. The interpreter executes the Apex itself, under Apex semantics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a replacement for the org.&lt;/strong&gt; Which is the next question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays in the org
&lt;/h2&gt;

&lt;p&gt;Lightning pages, screen flows as a user clicks through them, permission checks, anything a human looks at: those are checked in an org. Integrations need real endpoints and real credentials. And the deploy itself, with Salesforce's own validation of your metadata, happens in the org and nowhere else.&lt;/p&gt;

&lt;p&gt;A local runtime narrows what you use the org for; it does not remove it. The hundred edits between "I think this works" and "it works" move to your machine; the deploy and its validation stay in the org. &lt;code&gt;renmy deploy&lt;/code&gt; keeps that order explicit: local test and coverage gates, then a real Salesforce check-only validation of the identical bytes in the target org, then the deploy (&lt;a href="https://renmy.net/deploy" rel="noopener noreferrer"&gt;deploy and release&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  I have a sandbox. Why would I need a local runtime?
&lt;/h2&gt;

&lt;p&gt;Because the sandbox is still where you deploy, and the runtime is where you run the hundred edits before that.&lt;/p&gt;

&lt;p&gt;A Java team has a staging environment too, and nobody proposes running unit tests only there, after a deploy, in a shared queue. Staging is for integration, UI and a final check close to production. A sandbox plays that role, and a local runtime does not take it away; it takes away the sandbox's second job as the only place your code can execute.&lt;/p&gt;

&lt;p&gt;An org also hands you a log rather than a running process, so a sandbox never gave you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A debugger that stops a running test at a breakpoint rather than replaying a log (&lt;a href="https://renmy.net/debugger" rel="noopener noreferrer"&gt;debugger&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Branch coverage, which the org does not report (&lt;a href="https://renmy.net/coverage" rel="noopener noreferrer"&gt;coverage&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Tests on a train, during an outage, or in a CI job with no org credentials (&lt;a href="https://renmy.net/ci" rel="noopener noreferrer"&gt;CI/CD&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A write-run-fix loop fast enough for an AI coding agent (&lt;a href="https://renmy.net/agentic" rel="noopener noreferrer"&gt;AI and agentic coding&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  renmy runtime: a local Apex runtime you can install today
&lt;/h2&gt;

&lt;p&gt;renmy runtime is the local Salesforce Apex runtime described above, and the one I build. One binary for macOS, Linux and Windows; no JVM, no Docker, no org:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then narrow it down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;renmy &lt;span class="nb"&gt;test &lt;/span&gt;AccountTierServiceTest            &lt;span class="c"&gt;# one class&lt;/span&gt;
renmy &lt;span class="nb"&gt;test &lt;/span&gt;AccountTierServiceTest.assignsEnterpriseTierAboveThreshold
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;--coverage&lt;/span&gt;                    &lt;span class="c"&gt;# line and branch coverage&lt;/span&gt;
renmy dev                                    &lt;span class="c"&gt;# browser Dev UI: runner, coverage, schema, Apex REPL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you trust it, two pages: the &lt;a href="https://renmy.net/benchmarks" rel="noopener noreferrer"&gt;benchmark&lt;/a&gt; publishes its method, and the &lt;a href="https://renmy.net/parity" rel="noopener noreferrer"&gt;parity record&lt;/a&gt; lists what matches and what does not; it is not a claim of 100 percent.&lt;/p&gt;

&lt;p&gt;A toolchain sits on the runtime: coverage with Cobertura and JUnit XML output, mutation testing (Pro), a live debugger in VS Code and JetBrains, &lt;code&gt;renmy explain&lt;/code&gt; for failures, an MCP server for Claude Code, Cursor and Copilot, and gated deploys with release receipts. The whole of it is in &lt;a href="https://renmy.net/blog/renmy-runtime-explained" rel="noopener noreferrer"&gt;renmy runtime explained&lt;/a&gt;. If you distrust tooling claims on principle, &lt;a href="https://renmy.net/blog/how-to-evaluate-a-local-apex-runtime" rel="noopener noreferrer"&gt;how to evaluate a local Apex runtime before you trust it&lt;/a&gt; is the checklist; its first item is to run &lt;code&gt;renmy test "*"&lt;/code&gt; on your own repository.&lt;/p&gt;

&lt;p&gt;It works from the terminal, from a &lt;a href="https://renmy.net/vscode" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; and from a &lt;a href="https://renmy.net/jetbrains" rel="noopener noreferrer"&gt;JetBrains plugin&lt;/a&gt; for IntelliJ IDEA Community and Ultimate (&lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE on the Marketplace&lt;/a&gt;). It is &lt;a href="https://renmy.net/#pricing" rel="noopener noreferrer"&gt;free for individual developers&lt;/a&gt;, no card; Pro and Team are free during the public beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Salesforce developers who write Apex and have only ever run it inside an org: this is the piece of a toolchain every other ecosystem takes for granted. If you came to Salesforce from Java, Kotlin or TypeScript and miss your test runner, &lt;a href="https://renmy.net/blog/salesforce-for-java-developers" rel="noopener noreferrer"&gt;coming to Salesforce from Java&lt;/a&gt; picks up from here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://renmy.net/install.sh | sh          &lt;span class="c"&gt;# macOS, Linux&lt;/span&gt;
irm https://renmy.net/install.ps1 | iex              &lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-sfdx-project
renmy &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://renmy.net/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; takes it from there. Issues, docs, examples and the released binaries are on GitHub at &lt;a href="https://github.com/renmy-solutions/renmy" rel="noopener noreferrer"&gt;renmy-solutions/renmy&lt;/a&gt;. If you work in IntelliJ IDEA, the plugin is &lt;a href="https://plugins.jetbrains.com/plugin/32360-renmy-runtime--salesforce-ide" rel="noopener noreferrer"&gt;Renmy Runtime - Salesforce IDE&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a local Apex runtime?
&lt;/h3&gt;

&lt;p&gt;A local Apex runtime is a program that executes Salesforce Apex, SOQL and DML on a developer's machine instead of in a Salesforce org. It combines an Apex interpreter, a real database and the platform's rules (governor limits, order of execution, test isolation), so tests behave as in an org and complete in milliseconds. renmy runtime, formerly Nimbus, is one: it runs Apex and Salesforce Flows against an embedded PostgreSQL, with no org, no Docker and no JVM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a local Apex runtime the same as a scratch org?
&lt;/h3&gt;

&lt;p&gt;No. A scratch org is a real Salesforce org created on demand in Salesforce's cloud; you still deploy to it, wait for its test queue and rebuild it when it expires. A local Apex runtime runs on your machine with no org at all. Scratch orgs and sandboxes remain the right place for UI work, integrations and the final validation before a deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a local Apex runtime replace my sandbox?
&lt;/h3&gt;

&lt;p&gt;No. The sandbox is still where you deploy and where anything a user looks at is checked. The runtime is where you run the many edit-and-test cycles before that deploy; renmy deploy then runs local gates, a real check-only validation in the org, and the deploy, in that order.&lt;/p&gt;

&lt;h3&gt;
  
  
  How accurate is a local Apex runtime compared with a Salesforce org?
&lt;/h3&gt;

&lt;p&gt;renmy runtime enforces governor limits, runs triggers and flows in Salesforce's order of execution, and supports Test.startTest and Test.stopTest, @testSetup, per-test transaction isolation and the Stub API. It interprets your real Apex against a real database; it is not a mock and not a transpiler. The parity record at renmy.net/parity is public and lists what matches and what does not. In the published benchmark, all 837 tests of four open-source suites passed both locally and in an org.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does renmy runtime cost anything?
&lt;/h3&gt;

&lt;p&gt;renmy runtime is free for individual developers: unlimited local test runs, gated deploys, Salesforce CLI pass-through and IDE integration on one machine, with no card. The Pro and Team tiers (daemon, watch mode, parallel runs, debugger stepping, mutation testing, CI runs) are free during the public beta; the list prices are $19 per month and $49 per developer per month.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>testing</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Salesforce Down During Dreamforce 2026: How I Kept Shipping Apex Without an Org</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:57:11 +0000</pubDate>
      <link>https://dev.to/oblitus/salesforce-down-during-dreamforce-2026-how-i-kept-shipping-apex-without-an-org-2mek</link>
      <guid>https://dev.to/oblitus/salesforce-down-during-dreamforce-2026-how-i-kept-shipping-apex-without-an-org-2mek</guid>
      <description>&lt;p&gt;&lt;em&gt;Note (October 2026): Nimbus, the tool this article is about, has been renamed renmy runtime. Same tool, same binary, new name; the record of the rename is at &lt;a href="https://renmy.net/blog/nimbus-is-now-renmy" rel="noopener noreferrer"&gt;renmy.net/blog/nimbus-is-now-renmy&lt;/a&gt;. The text below has been updated to use the new name.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;September 16, 2026&lt;/strong&gt;, Salesforce went down. Globally. On day two of &lt;strong&gt;Dreamforce&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While tens of thousands of people in San Francisco were hearing about AIforce, Agentforce, and Headless 360, customers around the world were staring at login errors and spinning loaders. Developers refreshed the Trust status page. Deployments stalled. Standups turned into "is it just me?"&lt;/p&gt;

&lt;p&gt;I didn't notice.&lt;/p&gt;

&lt;p&gt;By the time a colleague pinged me, I had already written, tested, and refactored all the Apex I planned to finish that day. Not because I got lucky, but because &lt;strong&gt;my development loop doesn't need an org to be online.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened, what it reveals about how we build on Salesforce, and how to make sure the next outage doesn't stop you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened: the September 16, 2026 Salesforce outage in brief
&lt;/h2&gt;

&lt;p&gt;Based on Salesforce's status updates and press coverage at the time of writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~07:50 UTC:&lt;/strong&gt; The service disruption began, affecting Core Service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; Hundreds of instances across all regions, including the US, UK, Germany, France, Japan, and India.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symptoms:&lt;/strong&gt; Severe delays, intermittent errors, and users unable to log in or access services. Even new support cases were hard to submit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause (preliminary):&lt;/strong&gt; Salesforce pointed to requests stalling on an internal login service, which exhausted available capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late morning UTC:&lt;/strong&gt; A fix was validated and rolled out, but some instances took longer to recover, and some customers reported scheduled jobs not running as expected afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check &lt;a href="https://status.salesforce.com" rel="noopener noreferrer"&gt;status.salesforce.com&lt;/a&gt; for the official root cause analysis once it's published.&lt;/p&gt;

&lt;p&gt;The timing couldn't have been worse: the biggest week of the Salesforce year, with the entire ecosystem watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem: your Salesforce dev loop has a single point of failure
&lt;/h2&gt;

&lt;p&gt;Look at a typical Apex developer's inner loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write code in VS Code or IntelliJ&lt;/li&gt;
&lt;li&gt;Deploy to a scratch org or sandbox&lt;/li&gt;
&lt;li&gt;Run tests in the org&lt;/li&gt;
&lt;li&gt;Wait&lt;/li&gt;
&lt;li&gt;Dig through debug logs, fix, repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only step 1 is local. Your compiler, test runner, and debugger all live in Salesforce's data centers.&lt;/p&gt;

&lt;p&gt;On a normal day, that means slow feedback. On a day like September 16, it means &lt;strong&gt;zero productivity&lt;/strong&gt;. And Salesforce outages aren't one-offs. There have been major incidents in 2019, 2021, 2023, and 2025, from faulty permission scripts to DNS changes.&lt;/p&gt;

&lt;p&gt;No other ecosystem accepts this. Java developers don't stop coding when Maven Central blips. Go developers don't stop testing when AWS has a regional incident. &lt;strong&gt;Local-first development is standard everywhere, except Salesforce.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to develop and test Apex without a Salesforce org
&lt;/h2&gt;

&lt;p&gt;For the past months I've been building and using &lt;a href="https://renmy.net" rel="noopener noreferrer"&gt;renmy runtime&lt;/a&gt;, a local toolchain for Apex. It lets you run and test Apex on your machine, like any other language.&lt;/p&gt;

&lt;p&gt;Here's what my outage morning actually looked like:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Running Apex unit tests locally
&lt;/h3&gt;

&lt;p&gt;No deployment, no test queue. Feedback in seconds instead of minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SOQL and DML against a real database
&lt;/h3&gt;

&lt;p&gt;renmy runtime embeds PostgreSQL, so queries and inserts hit a real data layer, not brittle mocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Testing Platform Events offline
&lt;/h3&gt;

&lt;p&gt;A local Pub/Sub gRPC server means event-driven Apex works without an org.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Measuring test quality with mutation testing
&lt;/h3&gt;

&lt;p&gt;Coverage tells you code &lt;em&gt;ran&lt;/em&gt;. Mutation testing tells you whether your tests would &lt;em&gt;catch a bug&lt;/em&gt;. (More on this below.)&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Tracing instead of debug logs
&lt;/h3&gt;

&lt;p&gt;OpenTelemetry traces show exactly what executed, without the debug log scavenger hunt.&lt;/p&gt;

&lt;p&gt;When the platform came back, the only step left was the deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 75% Apex code coverage isn't enough
&lt;/h2&gt;

&lt;p&gt;Salesforce requires 75% coverage to deploy to production. But look at this test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="nd"&gt;@IsTest&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;calculatesDiscount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PricingService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertNotEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fully covers &lt;code&gt;applyDiscount&lt;/code&gt;. It also passes if the method returns &lt;code&gt;100&lt;/code&gt;, &lt;code&gt;80&lt;/code&gt;, or &lt;code&gt;-42&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutation testing&lt;/strong&gt; fixes this blind spot. It deliberately changes your code (flips &lt;code&gt;&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;&lt;/code&gt;, swaps constants, deletes statements) and reruns your tests. If nothing fails, a "mutant survived," and you've found a gap.&lt;/p&gt;

&lt;p&gt;Running this in an org would take hours. Locally, it's part of the normal loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-first matters even more in the Agentforce era
&lt;/h2&gt;

&lt;p&gt;This year's Dreamforce was all about agents: Agentforce, Agent Script, Headless 360, MCP, vibe coding. AI is writing more and more Salesforce code.&lt;/p&gt;

&lt;p&gt;That raises a question nobody on stage answered: &lt;strong&gt;where does AI-generated Apex get verified before it touches a real org?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If every check needs a live org, your AI agents stall during outages just like you do. And letting agents iterate directly against sandboxes is slow and risky.&lt;/p&gt;

&lt;p&gt;renmy runtime ships an &lt;strong&gt;MCP server&lt;/strong&gt;, so coding agents like Claude Code can run tests, check mutations, and verify Apex locally. The org becomes the place you deploy proven code, not the place you experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways for Salesforce teams
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outages will happen again.&lt;/strong&gt; Plan for them like any other dependency failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decouple your inner dev loop&lt;/strong&gt; from platform availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage is a deployment gate, not a quality metric.&lt;/strong&gt; Use mutation testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI pipelines get more reliable&lt;/strong&gt; when unit tests don't need an org for every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agents need a safe, local sandbox&lt;/strong&gt; to verify their code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Was Salesforce down on September 16, 2026?&lt;/strong&gt;&lt;br&gt;
Yes. Salesforce reported a global service disruption starting around 07:50 UTC, affecting instances across all regions during Dreamforce 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you run Apex tests without a Salesforce org?&lt;/strong&gt;&lt;br&gt;
Yes, with a local toolchain like &lt;a href="https://renmy.net" rel="noopener noreferrer"&gt;renmy runtime&lt;/a&gt;, which runs Apex, SOQL, DML, and Platform Events on your machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I keep developing during a Salesforce outage?&lt;/strong&gt;&lt;br&gt;
Move your write-test-refactor loop local. Only deployment and integration testing truly need the org.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;renmy runtime is available for free for everyone, and Pro features are free during the launch period. If the outage cost you half a day, spend twenty minutes setting up a loop that won't care next time.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://renmy.net" rel="noopener noreferrer"&gt;renmy.net&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How did your team handle the outage? Were you stuck, or did you have a workaround? Tell me in the comments. 👇&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Run your Salesforce development without an org. Run it with renmy runtime.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apex</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Design Salesforce Like a Distributed System</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:06:02 +0000</pubDate>
      <link>https://dev.to/oblitus/design-salesforce-like-a-distributed-system-57i0</link>
      <guid>https://dev.to/oblitus/design-salesforce-like-a-distributed-system-57i0</guid>
      <description>&lt;p&gt;Distributed systems engineers don't get to reach across service boundaries whenever it's convenient. The network stops them. A microservice that needs data owned by another service can't just query it. It has to ask, wait, and handle failure. That friction is annoying. It's also the thing that enforces discipline: every team knows exactly where its responsibility ends, because the boundary is physical.&lt;/p&gt;

&lt;p&gt;Salesforce gives you no such friction. Every object in your org, every &lt;code&gt;Account&lt;/code&gt;, every &lt;code&gt;Order&lt;/code&gt;, every custom object owned by another team, is one SOQL query away. Any Apex class can read it. Any trigger can write to it. Any Flow can fire on it. There is no network. There is no boundary. Nothing stops one domain from reaching into another's territory, and nothing makes that reach-in visible until something breaks.&lt;/p&gt;

&lt;p&gt;That's the core problem this article addresses: &lt;strong&gt;how to design a Salesforce org with the discipline distributed systems engineers are forced to have, in an environment that will never force it on you.&lt;/strong&gt; Platform Events and a principled trigger architecture are the tools. Bounded contexts, facts vs commands, and choreography or orchestration are the concepts. The prize is the same thing distributed systems get from their hard boundaries: teams that can ship independently without breaking each other.&lt;/p&gt;

&lt;p&gt;This article is opinionated, grounded in real patterns from a production org, and the goal is that it shifts how you think about who's &lt;em&gt;allowed&lt;/em&gt; to touch what, not just how to get work done async.&lt;br&gt;
It is also not an argument that every Salesforce org should adopt a distributed systems pattern. Rather, this is a thought experiment exploring what changes when we analyze Salesforce from a distributed systems lens.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. What Salesforce doesn't force on you (and why that's dangerous)
&lt;/h2&gt;

&lt;p&gt;In a distributed system, the boundary between services is enforced by physics. Service B cannot read Service A's database directly. If it needs data, it calls an API, subscribes to an event stream, or maintains its own projection. These patterns (APIs, events, read models) didn't emerge from clever design thinking. They emerged because there was literally no other option.&lt;/p&gt;

&lt;p&gt;The result is that distributed systems engineers develop a reflex: before touching anything, ask "do I own this?" If the answer is no, the question becomes "what's the sanctioned channel for getting what I need?"&lt;/p&gt;

&lt;p&gt;Salesforce developers rarely develop this reflex, because the platform never punishes them for not having it. The consequence plays out slowly. In year one, one team writes a trigger on &lt;code&gt;Contact&lt;/code&gt; and it works fine. In year two, three teams have logic in the same trigger handler. In year five, no one team can change the &lt;code&gt;Contact&lt;/code&gt; object without coordinating with four others, running a full regression suite, and hoping nothing breaks in production. The org has become a &lt;strong&gt;non-distributed monolith&lt;/strong&gt;: all the coupling of a distributed system with none of the boundaries, and no network to make the coupling visible.&lt;/p&gt;

&lt;p&gt;Distributed systems thinking is the antidote. Not the mechanics (you don't need service meshes or gRPC), but the &lt;em&gt;mindset&lt;/em&gt;: treat domain boundaries as if they were network boundaries, even though they aren't. Refuse cross-domain reach-ins the same way a service refuses direct database access from a neighbour. Use events as the only sanctioned channel for cross-domain communication.&lt;/p&gt;

&lt;p&gt;The difference from the distributed world is that on Salesforce you have one significant advantage: &lt;strong&gt;the data is already local.&lt;/strong&gt; You don't need to copy data across service boundaries, because there are no service boundaries at the database level. Every subscriber can re-query the source data directly. It's a SOQL call away. This changes the shape of event design considerably, as we'll get to in §7.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Domain bleed: what coupling without a boundary looks like
&lt;/h2&gt;

&lt;p&gt;Distributed systems literature warns about the "distributed monolith": services so entangled they must deploy together, with all the pain of distribution and none of the independence. In Salesforce, you get a mirror-image problem: a &lt;strong&gt;non-distributed monolith&lt;/strong&gt;. One schema, one automation surface, every team in everyone else's business, and no network boundary to make the coupling visible or enforce a fix.&lt;/p&gt;

&lt;p&gt;Domain bleed is when one domain's logic executes inside another domain's territory. In Salesforce it looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpvwmwwmsvvb2dyesr6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpvwmwwmsvvb2dyesr6u.png" alt=" " width="799" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Concrete symptoms, all of which you have seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A trigger nobody owns.&lt;/strong&gt; &lt;code&gt;AccountTrigger&lt;/code&gt; (or the one handler class) has commits from five teams. Touching it for a Billing change risks a Sales regression. Code review needs three squads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation firing across domains.&lt;/strong&gt; A Billing flow triggered by a field change on &lt;code&gt;Opportunity&lt;/code&gt;, which Sales owns. Sales refactors a picklist; Billing breaks in production; nobody connected the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOQL reaching three domains deep.&lt;/strong&gt; A Support class queries &lt;code&gt;Case → Contact → Account → Opportunity → OpportunityLineItem&lt;/code&gt;, encoding assumptions about four domains' models. Any of them refactoring breaks the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation rules encoding someone else's invariants.&lt;/strong&gt; A rule on &lt;code&gt;Contact&lt;/code&gt; enforcing a Marketing constraint that fires for every team that ever inserts a contact, including integration users that know nothing about it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared fields with implicit contracts.&lt;/strong&gt; A field added "just for Marketing" that automation, reports, and tests across the org now silently depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is the same coupling a distributed system avoids by construction. Things that should be independent become one fragile thing, concentrated in a single org with a single deploy. You can't ship one domain without regression-testing the others. There is no slice any team fully owns. Velocity collapses as the org ages, and everyone blames "Salesforce technical debt" when the real diagnosis is &lt;em&gt;missing boundaries&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's the critical distinction: &lt;strong&gt;the shared database is not the bleed.&lt;/strong&gt; Co-located data is your advantage, not your problem. Distributed systems would kill for it. The bleed is &lt;em&gt;control flow and ownership&lt;/em&gt; crossing boundaries: one domain reaching in to query, fire on, or write to another's objects. A distributed system can't do that because the network stops it. In Salesforce, you have to stop it yourself. Events are how you do it.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. There are two kinds of event-driven Salesforce, and triggers are one of them
&lt;/h2&gt;

&lt;p&gt;Before Platform Events: Salesforce already had an event-driven primitive that every Apex developer uses every day. &lt;strong&gt;Triggers are synchronous events.&lt;/strong&gt; A record changes; the platform fires a before/after trigger; your code runs. The pattern is event-driven at its core.&lt;/p&gt;

&lt;p&gt;The problem is ownership. And this is where the two models diverge sharply.&lt;/p&gt;
&lt;h3&gt;
  
  
  The trigger ownership problem
&lt;/h3&gt;

&lt;p&gt;When a trigger is owned by &lt;em&gt;everyone&lt;/em&gt;, it's owned by &lt;em&gt;nobody&lt;/em&gt;. The classic symptom: a single &lt;code&gt;AccountTrigger.cls&lt;/code&gt; (or one handler class dispatching to a dozen unrelated features) with commits from Sales, Fulfillment, Finance, and Support teams. The result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0p9c5xyhdzrelpnk2y07.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0p9c5xyhdzrelpnk2y07.png" alt=" " width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every team coupling to one file means nobody can deploy safely alone. The trigger becomes a bottleneck: a coordination tax for every change, regardless of which domain is touching it.&lt;/p&gt;

&lt;p&gt;The old pattern, a &lt;code&gt;TriggerFactory&lt;/code&gt; loading handlers from custom settings metadata, improved things by registering handlers per-object. But the fundamental problem remained: multiple teams could still register multiple handlers for the same object, and the execution order and interactions between them were implicit.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trigger Actions: single responsibility, metadata-driven composition
&lt;/h3&gt;

&lt;p&gt;A prime example of a trigger frame work that solves this issue at the architecture level is Mitch Spano's Trigger Actions framework. Each action is a &lt;strong&gt;focused, single-responsibility class&lt;/strong&gt; that implements one interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One class, one concern, one owner&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TA_Order_Placed&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;TriggerAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;AfterInsert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;afterInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SObject&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;triggerNew&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// only Fulfillment domain logic here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actions are composed via metadata (&lt;code&gt;Trigger_Action__mdt&lt;/code&gt;), not code. The trigger file itself becomes trivial, a one-liner that hands off to the framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="n"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;OrderTrigger&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="k"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MetadataTriggerHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this buys you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Legacy &lt;code&gt;TriggerFactory&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Trigger Actions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;Multiple handlers, shared registration&lt;/td&gt;
&lt;td&gt;One class per concern, clear owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;td&gt;Code changes to add/remove&lt;/td&gt;
&lt;td&gt;Metadata record, no deploy to wire up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Handler class tests mixed concerns&lt;/td&gt;
&lt;td&gt;Each action tested in isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bypass&lt;/td&gt;
&lt;td&gt;Shared disable switch&lt;/td&gt;
&lt;td&gt;Per-action bypass via custom permission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order of execution&lt;/td&gt;
&lt;td&gt;Implicit / registration order&lt;/td&gt;
&lt;td&gt;Explicit order field on metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;metadata-driven composition&lt;/strong&gt; is the key unlock. Sales can add a &lt;code&gt;TA_Order_Placed&lt;/code&gt; action for Order without touching any file owned by Fulfillment. Fulfillment can add its own Order action independently. The trigger file never changes; it's infrastructure, not logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Async events: Platform Events
&lt;/h3&gt;

&lt;p&gt;Platform Events are the &lt;em&gt;asynchronous&lt;/em&gt; counterpart. Where trigger actions fire synchronously in the DML transaction, Platform Events cross transaction boundaries: publish now, process later, survive failures independently.&lt;/p&gt;

&lt;p&gt;The right mental model for on-platform work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Synchronous (trigger actions):&lt;/strong&gt; "This just happened in my domain. Process it now, in this transaction."&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Asynchronous (Platform Events):&lt;/strong&gt; "This happened in my domain. Other domains should know about it, at their own pace, in their own transaction, on their own failure boundary."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both are event-driven. The difference is timing, ownership, and what happens when something fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  The property that makes it all work: the producer doesn't know its consumers
&lt;/h3&gt;

&lt;p&gt;Here is the single most important thing about messaging, and the reason it cuts coupling where a direct call cannot. It's worth stating on its own because everything downstream depends on it.&lt;/p&gt;

&lt;p&gt;When you call something (an Apex method, a &lt;code&gt;@future&lt;/code&gt;, a queueable, a REST callout), &lt;strong&gt;the caller has to name the callee.&lt;/strong&gt; The dependency points from producer to consumer. If a second consumer needs to react, you go back and edit the producer to call it too. The producer accumulates knowledge of everyone who depends on it, and every new reaction is a change to the thing that fired the event. That's true even when the call is &lt;em&gt;asynchronous&lt;/em&gt;. A &lt;code&gt;@future&lt;/code&gt; method is still point-to-point; async timing doesn't decouple anything, it just moves the same hard-wired call off the synchronous path.&lt;/p&gt;

&lt;p&gt;Publish/subscribe inverts that arrow. &lt;strong&gt;The producer names an event, not a consumer.&lt;/strong&gt; It announces "this happened" into the bus and is done. It does not know who is listening, and must not care. Consumers register their &lt;em&gt;own&lt;/em&gt; interest by subscribing. The dependency now points from consumer to event contract, not from producer to consumer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2arjucbqs7vsxz371x6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2arjucbqs7vsxz371x6.png" alt=" " width="799" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxsr7rldx6qd5baolhnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxsr7rldx6qd5baolhnf.png" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two consequences fall out of this, and both are things a direct call (synchronous or asynchronous) simply cannot give you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;N consumers, zero producer changes.&lt;/strong&gt; One published fact can fan out to one subscriber or fifty. Adding the fifty-first means &lt;em&gt;deploying the new subscriber&lt;/em&gt;. The producer never learns it exists, never recompiles, never gets re-tested. In a point-to-point world, every new reaction is a change to the producer. In pub/sub, new reactions are purely additive and live entirely in the consumer's domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dependency direction is reversed, and that's the whole game.&lt;/strong&gt; "Who depends on whom" is the question that decides whether you can deploy independently. With a direct call, Sales depends on Fulfillment (it has to know Fulfillment exists to call it). With an event, Fulfillment depends on Sales's &lt;em&gt;event contract&lt;/em&gt; (a stable, published fact) and Sales depends on nobody. The domain that owns the data is now ignorant of, and immune to, everyone reacting to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why "fire an async job" is &lt;em&gt;not&lt;/em&gt; the same as "publish an event," even though both run later. The async job still couples the producer to a named consumer. The event doesn't. Decoupling comes from the inverted dependency, not from the asynchrony.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The cure: notify, don't reach in
&lt;/h2&gt;

&lt;p&gt;Replace every cross-domain reach-in with a published fact. The domain that owns the data publishes "this happened." Domains that care subscribe and react &lt;strong&gt;in their own territory, on their own automation, on their own deploy cadence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vwmlt5unpanv6hu2du3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vwmlt5unpanv6hu2du3.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing crosses a boundary except a fact. Fulfillment no longer fires on &lt;code&gt;Order&lt;/code&gt;; it reacts to &lt;code&gt;OrderPlaced__e&lt;/code&gt; and writes to its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;Shipment__c&lt;/code&gt;. Finance no longer reaches into Sales's object graph; it subscribes to the same fact and manages its own &lt;code&gt;Invoice__c&lt;/code&gt;. Sales can refactor everything behind &lt;code&gt;Order&lt;/code&gt; as long as the published fact stays stable. You have drawn a bounded-context border &lt;strong&gt;inside one org&lt;/strong&gt;, using events as the only sanctioned channel across it.&lt;/p&gt;

&lt;p&gt;This is the reframe the generic posts miss. The bus isn't a data pipe here. It's a &lt;strong&gt;membrane&lt;/strong&gt;: facts pass through, reach-ins don't. And the data stays exactly where it was, one query away, for whoever legitimately owns the context that needs it.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. A cautionary tale: the order-handoff anti-pattern
&lt;/h2&gt;

&lt;p&gt;Here is what domain bleed looks like when it wears an event-driven costume.&lt;/p&gt;

&lt;p&gt;Imagine an order-to-fulfillment flow that uses platform events to chain processing steps: &lt;code&gt;OrderHandoff_STEP1__e&lt;/code&gt; → &lt;code&gt;OrderHandoff_STEP2__e&lt;/code&gt; → &lt;code&gt;OrderHandoff_SHIPMENT__e&lt;/code&gt; → &lt;code&gt;OrderHandoff_STEP4__e&lt;/code&gt; → ... Each step publishes the next event from its &lt;code&gt;finally&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OrderHandoff_SHIPMENT handler (in the Sales domain)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;triggerNext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;publishEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;OrderHandoff_STEP4__e'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the shipment step, the Sales domain directly inserts &lt;code&gt;Shipment__c&lt;/code&gt; records, which belong to Fulfillment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sales reaching into Fulfillment's territory&lt;/span&gt;
&lt;span class="n"&gt;orderToShipmentMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Shipment__c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Order__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CarrierCode__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderDetail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;CarrierCode__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ServiceLevel__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderDetail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;ServiceLevel__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... a dozen more fields from Sales's data model&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderToShipmentMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The platform events look modern. But look at what Sales is actually doing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It knows &lt;code&gt;Shipment__c&lt;/code&gt; exists in the Fulfillment domain.&lt;/li&gt;
&lt;li&gt;It knows the exact field-level schema of that object.&lt;/li&gt;
&lt;li&gt;It writes directly to it.&lt;/li&gt;
&lt;li&gt;It orchestrates the entire sequence. Fulfillment has no say in when or whether its own records get created.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Sales is reaching into Fulfillment's territory.&lt;/strong&gt; The events just move the coupling off a shared trigger file and onto the data model. This is "domain bleed in a costume." If Fulfillment adds a required validation rule to &lt;code&gt;Shipment__c&lt;/code&gt;, Sales's handoff breaks. If Fulfillment renames a field, Sales breaks. The dependency arrow points the wrong way.&lt;/p&gt;

&lt;p&gt;The correct design: Sales publishes &lt;code&gt;OrderPlaced__e&lt;/code&gt; carrying just the record ID and the timestamp. Fulfillment subscribes, queries the data it needs (same database, one SOQL call away), and creates its own &lt;code&gt;Shipment__c&lt;/code&gt; in its own transaction, on its own schedule, under its own validation rules. Fulfillment &lt;em&gt;owns&lt;/em&gt; shipment creation. Sales just announces that an order was placed.&lt;/p&gt;

&lt;p&gt;This is the "notify, don't reach in" principle from §4. The event should state a fact in the publisher's past tense. &lt;code&gt;OrderHandoff_SHIPMENT__e&lt;/code&gt; is not a fact; it's a command telling a later step to create something specific in another domain. If you can name the one subscriber that's supposed to handle it, you've written a command, and the coupling is still there.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. A well-designed example: the Support domain
&lt;/h2&gt;

&lt;p&gt;By contrast, the Support domain shows what the pattern looks like when it's right.&lt;/p&gt;

&lt;p&gt;When a case is escalated, a Trigger Action publishes a &lt;code&gt;CaseEscalated&lt;/code&gt; event outbound. Notice what it &lt;em&gt;doesn't&lt;/em&gt; do: it doesn't reach into any other domain, it doesn't write to any other object, and it doesn't know who the subscribers are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TA_Case_Escalated&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;TriggerAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;AfterUpdate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IEventPublisher&lt;/span&gt; &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;afterUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SObject&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;triggerNew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SObject&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;triggerOld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;accountMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TA_Case_Queries&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccountMap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Case&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Case&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;triggerNew&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldPublishEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountMap&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;CaseEscalated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;AccountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Boolean&lt;/span&gt; &lt;span class="nf"&gt;shouldPublishEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Case&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;accountMap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;AccountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Tier__c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Enterprise'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's clean about this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single responsibility.&lt;/strong&gt; One action class, one concern. The trigger file is infrastructure: it delegates to &lt;code&gt;MetadataTriggerHandler&lt;/code&gt; and contains no logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact, not command.&lt;/strong&gt; &lt;code&gt;CaseEscalated&lt;/code&gt; names something true in the Support domain's past tense. It doesn't tell anyone what to do with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publisher is indifferent to consumers.&lt;/strong&gt; Support doesn't know whether an account manager, a downstream warehouse, or a notification system will react. It announces; others decide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain stays in its own territory.&lt;/strong&gt; The action queries its own data (account tier), publishes to an abstract interface (&lt;code&gt;IEventPublisher&lt;/code&gt;), and returns. No cross-domain DML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The event publisher interface&lt;/strong&gt; keeps the domain agnostic of the transport layer. Whether the platform event gets bridged to Kafka, routed to a middleware, or handled purely on-platform is infrastructure's concern:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IEventPublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;addMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="n"&gt;messageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SObject&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contrast with the order-handoff flow is instructive. Both use events. Both have Trigger Actions wiring the logic. The difference is entirely in what the event means and who controls the downstream writes.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The rule that flips at the boundary
&lt;/h2&gt;

&lt;p&gt;Here's the question that immediately follows "publish a fact": &lt;strong&gt;how much do you put in the event?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The microservices world has two answers, named by Martin Fowler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event notification:&lt;/strong&gt; a thin signal. "Order 0061x placed." Just an id and what changed. The subscriber goes and looks if it needs more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-carried state transfer:&lt;/strong&gt; a fat, self-contained event. Everything a consumer could need, baked in, so it never has to call back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People argue about which is "right." The argument is unresolvable because &lt;strong&gt;the right answer depends on whether the subscriber can cheaply query back, and on Salesforce that answer is different inside the org versus outside it.&lt;/strong&gt; This is the rule:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjsx506y7rb4nxlxlbl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjsx506y7rb4nxlxlbl4.png" alt=" " width="799" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside the org → thin notification events.&lt;/strong&gt; The subscriber is in the same database. Re-querying is one cheap, current, transactional SOQL call. This is the Salesforce advantage a distributed system never has: the data is already local. Stuffing state into the event would duplicate data that's right there and instantly stale. So on-platform, publish &lt;code&gt;OrderPlaced__e&lt;/code&gt; carrying little more than the record id and the change; let the Fulfillment trigger query the source data itself. Communication, not duplication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the external boundary → fat, self-contained facts.&lt;/strong&gt; The moment the consumer is outside Salesforce, re-querying is expensive and coupling: a callback over the API, an auth handshake, latency, rate limits, and a hard dependency on Salesforce being up. So the events you publish &lt;em&gt;outward&lt;/em&gt; should carry the full business fact: everything a downstream system needs to act without calling back.&lt;/p&gt;

&lt;p&gt;A common pattern is a single outbound platform event (&lt;code&gt;OutboundEvent__e&lt;/code&gt;) that acts as a generic envelope: on-platform triggers add messages via an event publisher interface, which serializes the payload and routes it outward (e.g. through a middleware layer to Kafka). The fat-vs-thin distinction maps cleanly onto that: inside the org, thin signals on domain-specific events; at the outbound envelope boundary, fat self-contained payloads.&lt;/p&gt;

&lt;p&gt;And this is where your earlier instinct finally becomes correct: &lt;strong&gt;the read model, the projection, the materialized copy all belong downstream, outside Salesforce, fed by the fat boundary events.&lt;/strong&gt; Not because projections are wrong, but because &lt;em&gt;that&lt;/em&gt; is the place where data locality genuinely breaks and a local copy genuinely earns its keep. Inside the org you'd never build one. Across the boundary, you must.&lt;/p&gt;

&lt;p&gt;So the same system uses both styles, and choosing by &lt;em&gt;location&lt;/em&gt; dissolves the whole thin-vs-fat debate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Inside the org&lt;/th&gt;
&lt;th&gt;At the external boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subscriber can re-query?&lt;/td&gt;
&lt;td&gt;Yes, cheap SOQL&lt;/td&gt;
&lt;td&gt;No, expensive callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event style&lt;/td&gt;
&lt;td&gt;Thin notification&lt;/td&gt;
&lt;td&gt;Fat, self-contained fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carry full state?&lt;/td&gt;
&lt;td&gt;No, it's already local&lt;/td&gt;
&lt;td&gt;Yes, consumer can't fetch it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build a projection?&lt;/td&gt;
&lt;td&gt;Never, data is co-located&lt;/td&gt;
&lt;td&gt;Yes, downstream where locality breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event's job&lt;/td&gt;
&lt;td&gt;Communication + ownership&lt;/td&gt;
&lt;td&gt;Communication + data transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you take one thing from this article, take this table. It's where Salesforce's data-locality advantage changes the rules, and where the distributed-systems playbook applies unchanged.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Facts, not commands (the bleed wears a costume)
&lt;/h2&gt;

&lt;p&gt;Domain bleed has a sneaky form that survives even after you've "gone event-driven." It's the command disguised as an event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bleed in a costume. Not an event.&lt;/span&gt;
&lt;span class="n"&gt;CreateShipment__e&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateShipment__e&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderId__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CreateShipment__e&lt;/code&gt; is not a fact. It's the publisher reaching across a boundary to tell one specific subscriber what to do, just routed through the bus so it &lt;em&gt;looks&lt;/em&gt; decoupled. The publisher still knows the shipment step exists, still knows what it should do, still breaks when that domain changes. You've moved the bleed onto the event bus, not removed it.&lt;/p&gt;

&lt;p&gt;A real fact names something true in the &lt;em&gt;publisher's&lt;/em&gt; domain, in the past tense, and is indifferent to who reacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A fact. Past tense. Publisher's domain. Reaches into no one.&lt;/span&gt;
&lt;span class="n"&gt;OrderPlaced__e&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderPlaced__e&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;OrderId__c&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AccountId__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;AccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PlacedAt__c&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The litmus test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A fact says "X happened." A command says "service Y, do Z."&lt;/strong&gt; If you can name the one subscriber that's &lt;em&gt;supposed&lt;/em&gt; to handle it, you've written a command, and the coupling you were trying to cut is still there, now harder to see.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  It's the name, not the transport
&lt;/h3&gt;

&lt;p&gt;Here's the crucial point, and it's where a lot of advice goes wrong: &lt;strong&gt;a Platform Event can legitimately carry either a fact or a command.&lt;/strong&gt; Nothing about the platform forces the distinction. The same &lt;code&gt;EventBus.publish()&lt;/code&gt; call ships both. The difference lives entirely in &lt;em&gt;how you name and shape the message&lt;/em&gt;, and in whether you're honest about which one it is.&lt;/p&gt;

&lt;p&gt;You don't need a separate piece of infrastructure (a queue product, a second bus) to carry commands. That's the import error again: borrowing "facts go on the event bus, commands go on a queue" from architectures where those are physically different systems. On Salesforce, both are just Platform Events. What keeps the line honest is naming discipline, not a second transport:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Fact&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;Past tense: &lt;code&gt;OrderPlaced__e&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Imperative: &lt;code&gt;RecalculateDiscount__e&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answers&lt;/td&gt;
&lt;td&gt;"What happened?"&lt;/td&gt;
&lt;td&gt;"What should be done?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knows the recipient?&lt;/td&gt;
&lt;td&gt;No, indifferent to who reacts&lt;/td&gt;
&lt;td&gt;Yes, aimed at a specific handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumers&lt;/td&gt;
&lt;td&gt;Zero to many&lt;/td&gt;
&lt;td&gt;Exactly one (it's a directed instruction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coupling&lt;/td&gt;
&lt;td&gt;Publisher depends on no one&lt;/td&gt;
&lt;td&gt;Publisher depends on the handler existing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Commands aren't evil; they're necessary. Sometimes you genuinely do need to instruct one specific handler to do one specific thing. The mistake is dressing a command up in fact's clothing (a thin event that &lt;em&gt;looks&lt;/em&gt; decoupled but names exactly one intended subscriber) and then believing you've cut the coupling. If it's a command, name it like one: imperative, owned by the caller, pointed at a known handler, so the coupling is at least visible and you can decide whether it belongs. The danger isn't commands on the bus; it's commands &lt;em&gt;pretending&lt;/em&gt; to be facts.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Who owns the cross-boundary flow? Choreography or orchestration
&lt;/h2&gt;

&lt;p&gt;Once you've drawn borders, some business processes still span them, and you have to decide how they're coordinated. The cheap version of this debate is "choreography good, orchestration bad." It's wrong, and on Salesforce you can fall into the failure mode of either one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to choreography.&lt;/strong&gt; Facts on the bus; whoever cares reacts. Loosely coupled, scales by addition. Right for "a thing happened, others may want to know."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach for orchestration deliberately&lt;/strong&gt;, when a process spans domains and &lt;em&gt;must&lt;/em&gt; complete, needs ordering or end-to-end visibility, or has to be undoable (compensation).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sentence to tattoo on the team:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Someone has to own the multi-step transaction. Pick the owner on purpose.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The order-handoff anti-pattern from §5 illustrates this. Several steps chained via platform events, each step publishing the next, with no single owner tracking whether the whole thing succeeded. It's "Lambda pinball": an emergent chain nobody owns end-to-end, impossible to trace holistically, with no clean answer to "did the handoff finish, and how do we compensate if a middle step failed?" Per-step observability and error logging helps, but that's a bandage on a structural problem.&lt;/p&gt;

&lt;p&gt;The fix is not a central god-orchestrator micromanaging anemic domains. It's a &lt;em&gt;local&lt;/em&gt; coordinator: a state machine that owns the process, issues commands, and tracks completion, while the business logic stays in the domain handlers. For long-running, undoable processes you want a &lt;strong&gt;saga&lt;/strong&gt;, where each step registers a compensating action so a half-finished process rolls back cleanly.&lt;/p&gt;

&lt;p&gt;One honest caveat: the elegant "just use a local in-process engine" advice from the JVM world assumes a free-form runtime. On Salesforce your coordination primitives are Flow, Platform Events, and async Apex, all under governor limits, so you own the process explicitly &lt;em&gt;within&lt;/em&gt; that async model.&lt;/p&gt;

&lt;p&gt;The two anti-patterns, both reachable on-platform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Events that are secretly commands → hidden coupling, no owner.&lt;/li&gt;
&lt;li&gt;An orchestrator babysitting steps that are really independent reactions → a bottleneck for nothing.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  10. Splitting the monorepo into domains
&lt;/h2&gt;

&lt;p&gt;The architecture above only pays off if you can actually enforce the boundaries it describes. Domain isolation in a Salesforce org isn't just about code style - it's a deployment question.&lt;/p&gt;

&lt;p&gt;A repo that takes this seriously reflects it in directory structure. Rather than one flat &lt;code&gt;src/&lt;/code&gt; folder where every team drops files, you organize by bounded context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── domain-core/              # Shared infrastructure (trigger       # Event bus infrastructure (platform event objects, routing metadata)
├── domain-sales/             # Leads, opportunities, orders
│   ├── lead-management/
│   ├── opportunity/
│   └── order/
├── domain-fulfillment/       # Shipments, inventory, logistics
│   ├── shipment/
│   ├── inventory/
│   └── logistics/
├── domain-finance/           # Invoicing, billing, payments
├── domain-support/           # Case management
└── _common-modules/          # Cross-cutting UI components and utilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each domain is a candidate for its own &lt;strong&gt;unlocked package&lt;/strong&gt; (2GP). A domain can refactor its internals freely as long as the events it publishes stay compatible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Deployment decoupling is the one that changes how fast your org can move&lt;/strong&gt; - and it's only reachable once cross-domain communication goes through facts instead of reach-ins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a bled org, the deployment unit is the whole org. With events as the only sanctioned cross-domain channel, each domain can become its own independently-shippable unit. Fulfillment deploys on Tuesday. Sales deploys on Thursday. Neither regression-tests the other, because neither reaches into the other's objects.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. The field guide: Platform Event mechanics that actually decide success
&lt;/h2&gt;

&lt;p&gt;Everything above is design. This is the part where the platform's specific behaviour makes or breaks it. These are fact-checked against current Salesforce documentation, because half the lore floating around is stale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publish behaviour, and a footgun
&lt;/h3&gt;

&lt;p&gt;When you define an event you choose &lt;strong&gt;when it publishes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish Immediately&lt;/strong&gt; (default): fires when the &lt;code&gt;publish()&lt;/code&gt; call executes, &lt;strong&gt;regardless of whether the transaction commits.&lt;/strong&gt; A subscriber can receive the event before - or even when - the publishing transaction rolls back. Great for fire-regardless and logging-style events; a footgun if the subscriber expects committed data, because you can emit &lt;code&gt;OrderPlaced&lt;/code&gt; for an order that then never existed. It counts against a separate limit of &lt;strong&gt;150 &lt;code&gt;EventBus.publish()&lt;/code&gt; calls per transaction&lt;/strong&gt; (&lt;code&gt;Limits.getPublishImmediateDML()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish After Commit&lt;/strong&gt;: publishes &lt;strong&gt;only after the transaction commits successfully&lt;/strong&gt;, not at all if it fails. Use it when the subscriber will go query the data you just wrote (which, given thin events, is most of the time on-platform). It counts as &lt;strong&gt;one DML statement&lt;/strong&gt; each (&lt;code&gt;Limits.getDMLStatements()&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction interacts directly with §7: thin notification events usually want &lt;strong&gt;Publish After Commit&lt;/strong&gt;, because the subscriber's whole plan is to re-query, and re-querying a rolled-back record is a bug. If you're using a generic outbound envelope event to bridge to external systems, &lt;code&gt;PublishAfterCommit&lt;/code&gt; is the right default for exactly this reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  A successful publish is a successful &lt;em&gt;enqueue&lt;/em&gt;, not a delivery
&lt;/h3&gt;

&lt;p&gt;High-volume events (the default for new events since API v45.0) publish &lt;strong&gt;asynchronously&lt;/strong&gt;. After &lt;code&gt;publish()&lt;/code&gt; returns success, the request is queued; the actual publish happens when resources free up, and in rare cases the async publish can fail &lt;em&gt;after&lt;/em&gt; your synchronous call already returned "success." If you need certainty of the final result, use &lt;strong&gt;Apex Publish Callbacks&lt;/strong&gt;. Don't treat a green &lt;code&gt;publish()&lt;/code&gt; as proof of delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscribers, and who pays the delivery bill
&lt;/h3&gt;

&lt;p&gt;You get a genuinely broad set of subscriber types: &lt;strong&gt;Apex triggers&lt;/strong&gt;, &lt;strong&gt;Flows/processes&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;empApi&lt;/code&gt;&lt;/strong&gt; (an LWC CometD client, for live UI), &lt;strong&gt;Pub/Sub API&lt;/strong&gt; (gRPC over HTTP/2, the modern external path), and &lt;strong&gt;CometD&lt;/strong&gt; (legacy). Worth knowing: Apex triggers, processes, and flows &lt;strong&gt;don't&lt;/strong&gt; count against the event delivery allocation - only CometD-style clients do, and for &lt;code&gt;empApi&lt;/code&gt; it's metered &lt;em&gt;per channel, per browser session&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durability - the bus is not an event store
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retention:&lt;/strong&gt; high-volume events live in the bus for &lt;strong&gt;72 hours&lt;/strong&gt;; the older standard-volume events (definable only before Spring '19, now being retired) for &lt;strong&gt;24 hours&lt;/strong&gt;. Then purged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max message size: 1 MB.&lt;/strong&gt; Fat events still have a ceiling - another reason the fat ones belong at the boundary, carrying a business fact, not a kitchen sink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SOQL/SOSL on events&lt;/strong&gt;, no reports, no list views. If you need to &lt;em&gt;see&lt;/em&gt; that an event fired, you persist a trace yourself. Events are shouted, not stored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the deepest reason §7's projections live &lt;em&gt;downstream&lt;/em&gt;: the platform deliberately doesn't durably store events, so your long-term, replayable, queryable copy belongs on a real log (Kafka/Redpanda) that is built for retention - outside Salesforce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling out: it's broadcast, not consumer groups
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The intuitive Kafka model: add consumers, partitions distribute, each message processed once - &lt;strong&gt;does not apply to external Salesforce subscribers.&lt;/strong&gt; Per Salesforce's own docs, if you start multiple Pub/Sub API subscriptions for the same org and topic, each stream is independent and &lt;strong&gt;the same events are delivered to every subscriber.&lt;/strong&gt; Scale a gRPC connector from one pod to three and you get every event three times, not a third each. The burden is &lt;strong&gt;deduplication&lt;/strong&gt; (on the &lt;code&gt;EventUuid&lt;/code&gt; / &lt;code&gt;id&lt;/code&gt; field), not partition assignment. To actually parallelise, consume once and fan the work onto a real log with real consumer groups.&lt;/li&gt;
&lt;li&gt;The one place you get competing-consumer semantics is &lt;strong&gt;on-platform&lt;/strong&gt;: Apex &lt;strong&gt;parallel subscribers&lt;/strong&gt;, where one trigger runs as multiple concurrent instances and Salesforce assigns each event to exactly one, partitioned by a key (default: a hash of &lt;code&gt;EventUUID&lt;/code&gt;, which randomises, and therefore does &lt;strong&gt;not&lt;/strong&gt; preserve order). Need ordering within an entity? Set the partition key to that entity's id: ordered within a key, parallel across keys, exactly like a Kafka partition key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Errors, retries, and the replay asymmetry
&lt;/h3&gt;

&lt;p&gt;Inside an Apex subscriber you get a real retry model: throw &lt;strong&gt;&lt;code&gt;EventBus.RetryableException&lt;/code&gt;&lt;/strong&gt; and the batch is resent after an increasing delay, in original &lt;code&gt;ReplayId&lt;/code&gt; order, up to &lt;strong&gt;10 total runs&lt;/strong&gt; (initial + 9 retries) before the subscription moves to an error state. Read the attempt count via &lt;code&gt;EventBus.TriggerContext.currentContext().retries&lt;/code&gt; so you cap your own retries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="n"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;OrderPlacedTrigger&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;OrderPlaced__e&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FulfillmentService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;New&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;TriggerContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RetryableException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;OrderPlaced handler exhausted retries'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For partially-processed batches, checkpoint with &lt;code&gt;EventBus.TriggerContext.currentContext().setResumeCheckpoint(replayId)&lt;/code&gt; so a retry resumes after the last success instead of redoing it - important because uncatchable &lt;strong&gt;limit exceptions&lt;/strong&gt; (DML/SOQL governor limits) can't be caught, and an un-checkpointed batch's unprocessed events aren't redelivered.&lt;/p&gt;

&lt;p&gt;The asymmetry to remember: &lt;strong&gt;there is no Apex API to replay an arbitrary historical event by &lt;code&gt;ReplayId&lt;/code&gt;.&lt;/strong&gt; From Apex you can resume a &lt;em&gt;suspended&lt;/em&gt; subscription (from earliest, or "from tip" to skip poison events), checkpoint within a batch, or publish a &lt;em&gt;new&lt;/em&gt; event - but arbitrary replay-by-id within the retention window is an &lt;strong&gt;external&lt;/strong&gt;-subscriber capability (Pub/Sub API and CometD let you subscribe from Earliest, Latest, or a custom &lt;code&gt;ReplayId&lt;/code&gt;). Once again: durable replay lives downstream, not in Apex.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. The one thing the integrated platform gives you that Kafka doesn't
&lt;/h2&gt;

&lt;p&gt;For all the constraints, Platform Events have a genuinely lovely property a raw Kafka setup lacks: &lt;strong&gt;one fact can drive backend logic and live UI at the same time, with almost no extra plumbing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend: an Apex trigger on the &lt;code&gt;__e&lt;/code&gt; reacts and runs domain logic.&lt;/li&gt;
&lt;li&gt;Frontend: an LWC subscribes to the same channel via &lt;code&gt;empApi&lt;/code&gt; and updates in real time.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lightning/empApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/event/OrderPlaced__e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;empApi error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;disconnectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single &lt;code&gt;OrderPlaced__e&lt;/code&gt;, published once, can drive backend logic (Apex), refresh an ops team's live dashboard (LWC), &lt;em&gt;and&lt;/em&gt; hit Kafka for the rest of the company (Pub/Sub API bridge). This is the producer-knows-no-consumers property from §3 made vivid: three subscribers across two completely different runtimes - Apex and JavaScript - and the publisher knows about none of them. Add a fourth and the publisher still doesn't change. One fact, N consumers, zero coupling between them - provided the fact is a fact and not &lt;code&gt;RefreshOrdersDashboard&lt;/code&gt; in disguise. Operationally, remember &lt;code&gt;empApi&lt;/code&gt; is a CometD client and meters per browser session, so a thousand open dashboards is a thousand subscriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. The contras, stated plainly
&lt;/h2&gt;

&lt;p&gt;This is not friction-free, and the JVM-world defaults don't all survive contact with the platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bus is not a store.&lt;/strong&gt; 72h / 24h retention, no SOQL, no native audit. Durability is your job - downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No arbitrary replay from Apex.&lt;/strong&gt; Suspend/resume and checkpoints only; true replay-by-id is external.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out, not consumer groups, for external subscribers.&lt;/strong&gt; Scaling out means deduping, not load balancing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governor and async limits constrain orchestration.&lt;/strong&gt; Local-engine sagas don't port one-to-one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 MB cap and limited field types.&lt;/strong&gt; Fat events have a ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish-immediate footgun.&lt;/strong&gt; Events can reference data that never committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability tax.&lt;/strong&gt; You trade the easy end-to-end call stack for emergent flows. Eventual consistency becomes something you engineer - idempotent handlers, lag monitoring, reconciliation for any fire-and-forget fan-out with an SLA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger action discipline.&lt;/strong&gt; Single-responsibility actions only pay off if the team enforces it. Without governance on &lt;code&gt;Trigger_Action__mdt&lt;/code&gt;, you end up with the same chaos as the old shared handler class, just in metadata form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Borders cost discipline. Bleed costs velocity. Pick your bill.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Finding the borders: event storming
&lt;/h2&gt;

&lt;p&gt;Everything so far assumes you already know where the boundaries are and which facts matter. In a mature org you usually don't - the knowledge is scattered across the heads of admins, developers, and the business people who actually understand the process. &lt;strong&gt;Event storming&lt;/strong&gt; is the workshop technique for getting it out of those heads and onto a wall before you write a line of Apex.&lt;/p&gt;

&lt;p&gt;The mechanics are deliberately low-tech: a long roll of paper (or a digital whiteboard) and sticky notes in a few colours. You get the people who know the business in one room and reconstruct what actually happens, using a fixed vocabulary of colours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orange: domain events (facts).&lt;/strong&gt; Something that happened, past tense: &lt;em&gt;Order Placed&lt;/em&gt;, &lt;em&gt;Shipment Dispatched&lt;/em&gt;, &lt;em&gt;Invoice Issued&lt;/em&gt;. You brainstorm these first and arrange them left-to-right in time. This is the raw material for your Platform Events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue: commands.&lt;/strong&gt; The action or decision that &lt;em&gt;caused&lt;/em&gt; an event: &lt;em&gt;Place Order&lt;/em&gt;, &lt;em&gt;Dispatch Shipment&lt;/em&gt;. Note the one-to-one feel - a command, when it succeeds, produces an event. This is exactly the fact-vs-command distinction from §8, discovered on the wall before it's encoded in a name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yellow: actors / roles.&lt;/strong&gt; Who issues the command - a sales rep, a warehouse operator, a system integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pink: external systems.&lt;/strong&gt; Anything outside the org that emits or receives - the payment gateway, the carrier's shipping API. These pink notes are where your &lt;em&gt;fat boundary events&lt;/em&gt; from §7 will live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purple: policies.&lt;/strong&gt; "Whenever &lt;em&gt;this&lt;/em&gt; event happens, &lt;em&gt;that&lt;/em&gt; command should fire." Purple notes are the reactive glue - and crucially, &lt;strong&gt;they reveal cross-domain reach-ins.&lt;/strong&gt; A policy that says "whenever Order Placed, create the Shipment" is the order-handoff anti-pattern from §5, caught on a sticky note.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The borders emerge on their own. As the orange events line up across the timeline, you'll see them cluster - a run of sales facts, then a run of fulfillment facts, then finance. &lt;strong&gt;The seams between the clusters are your bounded contexts.&lt;/strong&gt; You didn't impose them top-down; the language did. Where the business stops using one set of nouns and starts using another is exactly where one domain ends and the next begins. Draw a vertical line there and you've found a border to defend with an event.&lt;/p&gt;

&lt;p&gt;What event storming gives you, concretely, before any migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A candidate list of Platform Events&lt;/strong&gt; - every orange note is a fact you might publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bounded contexts&lt;/strong&gt; - the clusters become your &lt;code&gt;domain-*&lt;/code&gt; packages from §10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reach-ins to kill&lt;/strong&gt; - every purple policy that crosses a cluster line is a place a domain is reaching into another's territory. Those are your refactor targets, prioritised by how much pain they cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared language&lt;/strong&gt; - the admins and developers now describe the system the same way, which is half the battle in cutting domain bleed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't need a two-day formal workshop to start. Even a 90-minute "big picture" storm of one painful flow - the order handoff, say - will surface more hidden coupling than a week of reading code, because it pulls in the people who know what the system is &lt;em&gt;supposed&lt;/em&gt; to do, not just what it currently does.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. How to get there without a rewrite
&lt;/h2&gt;

&lt;p&gt;You don't refactor a twelve-year-old org in one go. Use &lt;strong&gt;branching by abstraction&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep internals as they are.&lt;/li&gt;
&lt;li&gt;Define a clean set of &lt;strong&gt;facts&lt;/strong&gt; at each context's edge and publish them - thin on-platform, and bridged (e.g. via Redpanda Connect) to a real log as fat boundary facts for the wider company.&lt;/li&gt;
&lt;li&gt;Move downstream consumers onto that durable, properly-partitioned stream instead of subscribing to raw Platform Events.&lt;/li&gt;
&lt;li&gt;Migrate trigger logic from shared handler classes to Trigger Actions, one object at a time - the metadata-driven composition means you can wire up new actions without touching the trigger file.&lt;/li&gt;
&lt;li&gt;Peel domains into their own packages behind those stable contracts, on your own schedule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The facts are the contract. Everything behind them is yours to modernise; everything in front sees clean borders - even while the engine room is mid-renovation.&lt;/p&gt;

&lt;p&gt;The hardest part isn't the platform mechanics - it's the cultural shift. Publishing a fact and trusting other domains to react is slower in the short term than reaching in and doing it yourself. The order-handoff anti-pattern from §5 exists because it &lt;em&gt;works&lt;/em&gt; - it's just not &lt;em&gt;owned&lt;/em&gt;. Inverting it requires accepting that a domain's records are that domain's responsibility, even when another domain is the trigger for creating them. That discipline is what makes independent deployability real.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat domain boundaries as if they were network boundaries.&lt;/strong&gt; Distributed systems engineers get this for free; on Salesforce you have to enforce it yourself. The discipline is identical - only the mechanism differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shared database is your advantage, not your problem.&lt;/strong&gt; Distributed systems copy data across service boundaries because they have no choice. You can re-query. Use it - keep events thin on-platform, fat only at the external boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There are two kinds of event-driven Salesforce.&lt;/strong&gt; Triggers are synchronous events; Platform Events are async. Both require the same ownership discipline; the difference is timing and failure boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger ownership is an architecture decision.&lt;/strong&gt; A trigger owned by everyone is owned by nobody. Trigger Actions with metadata-driven composition give you single responsibility, independent deploy, and explicit ordering - the same guarantees a service boundary gives a distributed system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain bleed is the core failure mode&lt;/strong&gt; - one domain reaching into another's objects, queries, and automation. Events are the membrane: facts cross boundaries, reach-ins don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facts, not commands.&lt;/strong&gt; If you can name the one subscriber meant to handle it, you've written a command. The coupling is still there, just on the bus now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin inside, fat at the external boundary.&lt;/strong&gt; Inside the org, thin notifications - the data is a query away. Outward, fat self-contained facts. That's the only place a read model projection belongs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the owner of every cross-domain flow on purpose.&lt;/strong&gt; Default to choreography; reach for orchestration when a process must complete end-to-end and someone needs to own whether it did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split your repo into domains.&lt;/strong&gt; Directory structure is the first step toward independent packages. Code isolation is the prerequisite for deployment isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know the mechanics cold&lt;/strong&gt; - publish behaviour, async publish + callbacks, retention, broadcast vs parallel subscribers, retry/checkpoint, no-arbitrary-replay-from-Apex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The prize is independent deployability.&lt;/strong&gt; Distributed systems get it from service boundaries. You get it from event contracts. Same outcome, different path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The platform is a tool, not an identity. Distributed systems engineers don't get credit for their hard boundaries - the network just forces them. The work on Salesforce is choosing to hold those same boundaries when nothing makes you.&lt;/p&gt;

&lt;p&gt;So don't wait for a rewrite, and don't try to redraw the whole org at once. On your next change, find one place where a domain reaches across a boundary it doesn't own - one trigger, one cross-domain write, one query reaching three objects deep - and replace just that reach-in with a published fact. Draw one border. That single event is the first place your org starts behaving like the distributed system it always secretly was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Glossary
&lt;/h2&gt;

&lt;p&gt;This article borrows vocabulary from distributed systems and domain-driven design. If you came up through the admin/declarative side of Salesforce, here's the plain-English version of each term, in roughly the order it appears.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain / subdomain.&lt;/strong&gt; A cohesive area of the business with its own data and rules - Sales, Fulfillment, Finance. On Salesforce, a domain is the set of objects, automation, and code one team owns. The opposite of "one big shared org where everyone edits everything."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded context.&lt;/strong&gt; The boundary around a domain inside which a word means exactly one thing. "Account" might mean a prospect in Sales and a billing entity in Finance; a bounded context is where you decide which. The seam between two contexts is exactly where you draw a border and put an event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain bleed.&lt;/strong&gt; This article's term for one domain's logic running inside another's territory - querying its objects, firing on its records, or writing its fields. The disease the whole article is about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coupling.&lt;/strong&gt; How much one piece of code depends on another. &lt;em&gt;Tight&lt;/em&gt; coupling means changing A forces you to change (and re-test) B. The goal is &lt;em&gt;loose&lt;/em&gt; coupling - domains that can change independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed monolith.&lt;/strong&gt; A system split into pieces that still have to deploy together because they're too entangled - all the cost of being distributed, none of the independence. The "non-distributed monolith" is the Salesforce mirror image: one org, but the same entanglement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact vs command.&lt;/strong&gt; A &lt;strong&gt;fact&lt;/strong&gt; (or domain event) states something that already happened, past tense: &lt;em&gt;OrderPlaced&lt;/em&gt;. A &lt;strong&gt;command&lt;/strong&gt; instructs a specific handler to do something: &lt;em&gt;CreateShipment&lt;/em&gt;. Both can ride on a Platform Event - the difference is naming and intent (see §8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Producer / consumer (publish / subscribe).&lt;/strong&gt; The &lt;strong&gt;producer&lt;/strong&gt; publishes a message and doesn't know who's listening. &lt;strong&gt;Consumers&lt;/strong&gt; subscribe to declare their own interest. This is what "the producer doesn't know its consumers" means - the dependency points from consumer to event, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event notification (thin) vs event-carried state transfer (fat).&lt;/strong&gt; A &lt;strong&gt;thin&lt;/strong&gt; event carries just an id and what changed; the subscriber re-queries for details. A &lt;strong&gt;fat&lt;/strong&gt; event carries the whole payload so the subscriber never has to call back. Thin inside the org, fat at the external boundary (see §7).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projection / read model.&lt;/strong&gt; A pre-built copy of data, shaped for fast reading, kept in sync by listening to events. On-platform you almost never need one (you can just SOQL the source). Downstream of Salesforce - in Kafka, a warehouse - you build them because re-querying across the boundary is expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CQRS (Command Query Responsibility Segregation).&lt;/strong&gt; The pattern of separating the write side (commands) from the read side (queries/projections). Mentioned mainly to say: it's a downstream concern, not something you build inside the org.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>salesforce</category>
      <category>architecture</category>
      <category>eventdriven</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Salesforce Debug Logs suck and how I fixed them</title>
      <dc:creator>René Knierim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 09:11:34 +0000</pubDate>
      <link>https://dev.to/oblitus/why-salesforce-debug-logs-suck-and-how-i-fixed-them-3ilh</link>
      <guid>https://dev.to/oblitus/why-salesforce-debug-logs-suck-and-how-i-fixed-them-3ilh</guid>
      <description>&lt;p&gt;If you are a Salesforce Developer, you know the drill: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Develop&lt;/li&gt;
&lt;li&gt;Deploy&lt;/li&gt;
&lt;li&gt;Enable Debug Logs for a User or open Developer Console&lt;/li&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;li&gt;Something breaks -&amp;gt; inspect the Debug log&lt;/li&gt;
&lt;li&gt;Download the Debug log and use it with the Apex Log Replayer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For me, this always felt a bit icky. Why do I have to go through a full release cycle, interact with the org, and find and download the log to even get the most basic information about what happened in the backend?&lt;/p&gt;

&lt;p&gt;And that's one of the keywords here. Something &lt;strong&gt;already happened&lt;/strong&gt;. I created an error on the Org that I might even share with other people and developers. Let alone if it is a production org that faces this issue. Every fix I deploy will affect other people and needs to go through a full release cycle, just to check again retrospectively if it is fixed.&lt;/p&gt;

&lt;p&gt;Apex again is the odd one out in the world of programming languages. Its historic strength to develop directly against the platform turns out to be a big time consumer on complex orgs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging at Runtime
&lt;/h2&gt;

&lt;p&gt;Imagine for a second you are not a Salesforce developer, but you develop in Java, Go, Kotlin, Swift or any other widely used programming language. There is no Org, no server you deploy to, nothing that generates Debug Logs for you during your dev cycle to debug issues. &lt;br&gt;
Instead, you can execute those languages on your machine. And because they execute on the machine, your IDE enables you to set breakpoints and inspect every single variable. All that without going through a release cycle to a Sandbox or a Scratch org. And all that without cluttering your machine with tons and tons of old debug logs that you forgot to delete.&lt;br&gt;
If this is something you have dealt with in the past and would like to experience the same comfort as developers in other languages, having a look into &lt;a href="https://testnimbus.dev" rel="noopener noreferrer"&gt;Nimbus&lt;/a&gt; might be worth your time&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gep8zey3mhf1izixkuv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gep8zey3mhf1izixkuv.png" alt=" " width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nimbus is a full local Apex Runtime and Debugger. It runs your Apex code against an embedded database as if you were on a Salesforce org, with the only difference that you don't need a Sandbox or a Scratch Org to verify what you have built&lt;/p&gt;

&lt;p&gt;Set up Nimbus in less than a minute: &lt;a href="https://github.com/nimbus-solution/nimbus" rel="noopener noreferrer"&gt;https://github.com/nimbus-solution/nimbus&lt;/a&gt; and go Pro for free&lt;/p&gt;

&lt;p&gt;Get the &lt;a href="https://marketplace.visualstudio.com/items?itemName=NimbusSolutions.testnimbus" rel="noopener noreferrer"&gt;VSCode&lt;/a&gt; and &lt;a href="https://plugins.jetbrains.com/plugin/32360-nimbus--local-apex-runtime/versions/stable/1083152" rel="noopener noreferrer"&gt;IntelliJ&lt;/a&gt; plugins and improve you development cycle today!&lt;/p&gt;

&lt;p&gt;Found something that doesn't work as expected? Leave us an &lt;a href="https://github.com/nimbus-solution/nimbus/issues" rel="noopener noreferrer"&gt;issue in GitHub &lt;/a&gt;or reach out to us directly in our &lt;a href="https://join.slack.com/t/nimbuslocalap-tj23081/shared_invite/zt-3x7fxoo38-AAgO9QHP7if53JPunBxOPQ" rel="noopener noreferrer"&gt;Slack community&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>tdd</category>
      <category>apex</category>
    </item>
  </channel>
</rss>
