<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://trailblazer.imswarnil.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://trailblazer.imswarnil.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-08-04T06:54:20+05:30</updated><id>https://trailblazer.imswarnil.com/feed.xml</id><title type="html">Trailblazer</title><subtitle>A fast, accessible Jekyll theme for Salesforce developers, architects and consultants — with a resume page, certification wall, project portfolio and a code-first blog.</subtitle><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><entry><title type="html">The field audit: deleting 178 fields without an incident</title><link href="https://trailblazer.imswarnil.com/blog/field-audit-part-two/" rel="alternate" type="text/html" title="The field audit: deleting 178 fields without an incident" /><published>2026-07-08T00:00:00+05:30</published><updated>2026-07-08T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/field-audit-part-two</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/field-audit-part-two/"><![CDATA[<p><a href="/blog/field-audit-part-one/">Part one</a> ended with 178
fields that passed every test. This part deletes them — slowly, in stages,
because “passed every test” and “safe to delete” are different claims, and
the gap between them is where incidents live.</p>

<h2 id="the-staging">The staging</h2>

<ol class="tb-steps"><li>
    <span class="tb-steps__title">Describe</span><span class="tb-u-fg-muted tb-u-text-sm">Export every candidate field’s metadata to the repo. This is the rollback.
</span>
  </li><li>
    <span class="tb-steps__title">Hide</span><span class="tb-u-fg-muted tb-u-text-sm">Remove from layouts and field-level security. Two weeks’ wait — anything that breaks now breaks loudly and reversibly.
</span>
  </li><li>
    <span class="tb-steps__title">Freeze</span><span class="tb-u-fg-muted tb-u-text-sm">Rename with a ZZZ_DEPRECATED_ prefix. Two more weeks. Formulas and integrations that reference it by API name keep working; humans notice the label.
</span>
  </li><li>
    <span class="tb-steps__title">Delete</span><span class="tb-u-fg-muted tb-u-text-sm">In batches of twenty, one object at a time, with the recycle bin’s fifteen-day window as the final net.
</span>
  </li></ol>

<h2 id="the-one-that-came-back">The one that came back</h2>

<p>Day nine of the hide stage: a regional team’s quarterly forecast broke. The
field had zero values in twelve months — because they populated it during
one week per quarter, and the audit window had landed between weeks. Test
one had lied, politely.</p>

<p>The fix took ten minutes because hiding is reversible. Had we deleted on day
one, it would have been a restore-from-describe, a data reload, and a
meeting with names in it. The staged process is slower by design: <strong>every
stage before the last one is a question, and only the last one is an
answer.</strong></p>

<h2 id="the-result">The result</h2>

<p>177 fields gone, one reinstated, zero incidents that survived a fortnight.
The org’s setup pages load noticeably faster, which nobody predicted and
everybody mentions.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Data" /><category term="Process" /><summary type="html"><![CDATA[Part two — the staged deletion: describe, hide, freeze, delete, and the one rollback we needed at step three.]]></summary></entry><entry><title type="html">The field audit: finding the 200 fields nobody uses</title><link href="https://trailblazer.imswarnil.com/blog/field-audit-part-one/" rel="alternate" type="text/html" title="The field audit: finding the 200 fields nobody uses" /><published>2026-07-01T00:00:00+05:30</published><updated>2026-07-01T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/field-audit-part-one</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/field-audit-part-one/"><![CDATA[<p>Every long-lived org accumulates fields the way a garage accumulates boxes:
each one made sense to someone, once, and nobody remembers which ones. This
series is the cleanup we ran on a nine-year-old org — this part is the audit,
part two is the deletion, and the order matters, because deleting a field
someone still uses is how cleanup projects get cancelled.</p>

<h2 id="what-unused-actually-means">What “unused” actually means</h2>

<p>A field is a candidate for deletion only when all four are true:</p>

<ol>
  <li><strong>No recent values.</strong> <code class="language-plaintext highlighter-rouge">SELECT COUNT() WHERE Field__c != null AND
LastModifiedDate = LAST_N_DAYS:365</code> is zero or near it.</li>
  <li><strong>No metadata references.</strong> The dependency API knows about formulas,
Flows and layouts; it does not know about report filters, so those are a
separate pass.</li>
  <li><strong>No integration touches it.</strong> The field-level “where is this mapped”
question no tool answers — this is the week of the project, and it is
mostly reading integration configs and asking people.</li>
  <li><strong>Nobody claims it.</strong> A field with zero data can still be the one field
the CFO’s one report groups by, once a year, in January.</li>
</ol>

<h2 id="the-spreadsheet">The spreadsheet</h2>

<p>One row per custom field, four columns for the four tests, and a fifth for
the owner’s sign-off. Ours had 214 rows and took three weeks of background
effort. 178 passed all four tests — which is the number that makes
<a href="/blog/field-audit-part-two/">part two</a> worth doing.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Data" /><category term="Process" /><summary type="html"><![CDATA[Part one of cleaning up a nine-year-old org — how to prove a field is dead before you are allowed to delete it.]]></summary></entry><entry><title type="html">Scratch-org CI in ten minutes, from an empty repo</title><link href="https://trailblazer.imswarnil.com/blog/scratch-org-ci-in-ten-minutes/" rel="alternate" type="text/html" title="Scratch-org CI in ten minutes, from an empty repo" /><published>2026-06-20T00:00:00+05:30</published><updated>2026-06-20T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/scratch-org-ci-in-ten-minutes</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/scratch-org-ci-in-ten-minutes/"><![CDATA[<p>The screen recording of
<a href="/blog/scratch-orgs-per-pull-request/">the scratch-orgs-per-pull-request setup</a>,
done in one take. Two things go wrong on camera — the Dev Hub auth URL pasted
with a trailing newline, and the missing <code class="language-plaintext highlighter-rouge">always()</code> on the cleanup step — and
both stay in, because those are the two things that will go wrong for you too.</p>

<h2 id="chapters">Chapters</h2>

<ul>
  <li><strong>0:00</strong> — What we are building, and why the shared sandbox had to go</li>
  <li><strong>1:12</strong> — The Dev Hub auth URL, and where the newline hides</li>
  <li><strong>3:45</strong> — The workflow file, line by line</li>
  <li><strong>7:30</strong> — First red run, and reading the Actions log</li>
  <li><strong>9:58</strong> — Green, and what it costs per PR</li>
</ul>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="DevOps" /><category term="SFDX" /><summary type="html"><![CDATA[The whole pipeline from the write-up, built live — empty repository to a green pull-request check, with the two mistakes left in.]]></summary></entry><entry><title type="html">The anatomy of a deployment that goes well</title><link href="https://trailblazer.imswarnil.com/blog/anatomy-of-a-deployment/" rel="alternate" type="text/html" title="The anatomy of a deployment that goes well" /><published>2026-05-25T00:00:00+05:30</published><updated>2026-05-25T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/anatomy-of-a-deployment</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/anatomy-of-a-deployment/"><![CDATA[<p>This post doubles as a layout demo: <code class="language-plaintext highlighter-rouge">hero_split: true</code> in front matter puts
the cover beside the title instead of under it, which is the shape for posts
where the image is a hook rather than a figure.</p>

<h2 id="the-checklist-itself">The checklist itself</h2>

<p>A deployment that goes well is boring, and the boredom is manufactured in
the week before:</p>

<ol>
  <li><strong>The validation ran yesterday</strong>, not this morning — a validation that
just passed is a validation that had no time to rot.</li>
  <li><strong>The permission-set assignment is in the runbook</strong>, because deployed and
visible are different states.</li>
  <li><strong>The rollback is written down before it is needed</strong>, when everyone is
calm and nobody needs it.</li>
  <li><strong>Someone who did not build it reads the steps aloud</strong> — the step that
cannot be read aloud is the step that was never actually written.</li>
</ol>

<h2 id="the-part-people-skip">The part people skip</h2>

<p>Item four. It costs fifteen minutes and it has caught something every single
time we have done it, which says less about the checklist and more about how
deployment plans get written: by the person who no longer sees the missing
step because they have done it by hand forty times.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Deployment" /><category term="Process" /><summary type="html"><![CDATA[A demonstration of the split header — the title and the image share the fold — wrapped around a real checklist.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" /><media:content medium="image" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Governor limits are a design tool</title><link href="https://trailblazer.imswarnil.com/blog/governor-limits-are-a-design-tool/" rel="alternate" type="text/html" title="Governor limits are a design tool" /><published>2026-05-18T00:00:00+05:30</published><updated>2026-05-18T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/governor-limits-are-a-design-tool</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/governor-limits-are-a-design-tool/"><![CDATA[<p>The first thing anyone learns about Apex is that it will stop you. 100 SOQL
queries. 150 DML statements. 10,000 rows. Six megabytes of heap. Break one and
the transaction dies with a message that reads like an accusation.</p>

<p>So the reflex is defensive. You learn the numbers, you keep under them, and you
think of the limits as a tax on getting work done.</p>

<p>That reflex is wrong, and it is expensive. The limits are not a tax. They are
the platform stating, in numbers, what shape multi-tenant code has to be.</p>

<h2 id="the-limits-describe-a-shape">The limits describe a shape</h2>

<p>Look at the three that bite most often:</p>

<table>
  <thead>
    <tr>
      <th>Limit</th>
      <th>What it actually says</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>100 SOQL queries</td>
      <td>Do not query per record</td>
    </tr>
    <tr>
      <td>150 DML statements</td>
      <td>Do not write per record</td>
    </tr>
    <tr>
      <td>50,000 rows returned</td>
      <td>Do not load a data set you cannot hold</td>
    </tr>
  </tbody>
</table>

<p>Every one of them is the same instruction wearing a different hat: <strong>work in
sets, not in loops</strong>. That is not a Salesforce rule. It is the rule for any
system where the database is over a network and the CPU is shared.</p>

<div class="tb-callout tb-callout--info"><svg class="tb-icon tb-icon--md" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
  <use href="#tb-info"></use>
</svg>

  <div class="tb-callout__body"><strong class="tb-callout__title">The tell</strong>
    <p>If you are counting queries to stay under 100, you have already lost. Correct bulkified code uses two or three, and the count does not move when the batch size does.
</p>
  </div>
</div>

<h2 id="what-that-looks-like">What that looks like</h2>

<p>The version that fails is the one everybody writes first:</p>

<div class="language-apex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Wrong: one query per record. Works on 1 record, dies on 201.</span>
<span class="k">for</span> <span class="p">(</span><span class="n">Account</span> <span class="n">a</span> <span class="p">:</span> <span class="n">Trigger</span><span class="o">.</span><span class="py">new</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">List</span><span class="o">&lt;</span><span class="n">Contact</span><span class="o">&gt;</span> <span class="n">kids</span> <span class="o">=</span> <span class="p">[</span><span class="k">SELECT</span> <span class="n">Id</span> <span class="k">FROM</span> <span class="n">Contact</span> <span class="k">WHERE</span> <span class="n">AccountId</span> <span class="o">=</span> <span class="p">:</span><span class="n">a</span><span class="o">.</span><span class="py">Id</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">kids</span><span class="o">.</span><span class="nf">isEmpty</span><span class="p">())</span> <span class="p">{</span>
        <span class="n">a</span><span class="o">.</span><span class="nf">addError</span><span class="p">(</span><span class="s1">'</span><span class="s2">Every account needs a contact.'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The version that passes is not more complicated. It is the same logic with the
query lifted out:</p>

<div class="language-apex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Right: one query, whatever the batch size.</span>
<span class="n">Map</span><span class="o">&lt;</span><span class="n">Id</span><span class="p">,</span> <span class="n">Account</span><span class="o">&gt;</span> <span class="n">byId</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Map</span><span class="o">&lt;</span><span class="n">Id</span><span class="p">,</span> <span class="n">Account</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Trigger</span><span class="o">.</span><span class="py">new</span><span class="p">);</span>

<span class="n">Set</span><span class="o">&lt;</span><span class="n">Id</span><span class="o">&gt;</span> <span class="n">withContacts</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Set</span><span class="o">&lt;</span><span class="n">Id</span><span class="o">&gt;</span><span class="p">();</span>
<span class="k">for</span> <span class="p">(</span><span class="n">Contact</span> <span class="n">c</span> <span class="p">:</span> <span class="p">[</span><span class="k">SELECT</span> <span class="n">AccountId</span> <span class="k">FROM</span> <span class="n">Contact</span> <span class="k">WHERE</span> <span class="n">AccountId</span> <span class="n">IN</span> <span class="p">:</span><span class="n">byId</span><span class="o">.</span><span class="nf">keySet</span><span class="p">()])</span> <span class="p">{</span>
    <span class="n">withContacts</span><span class="o">.</span><span class="nf">add</span><span class="p">(</span><span class="n">c</span><span class="o">.</span><span class="py">AccountId</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">for</span> <span class="p">(</span><span class="n">Account</span> <span class="n">a</span> <span class="p">:</span> <span class="n">Trigger</span><span class="o">.</span><span class="py">new</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">withContacts</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="py">Id</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">a</span><span class="o">.</span><span class="nf">addError</span><span class="p">(</span><span class="s1">'</span><span class="s2">Every account needs a contact.'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Two loops instead of one, and a set in between. It is longer. It is also the
version that does not care whether it is handed one record or two hundred, which
is the only property that matters in a trigger.</p>

<h2 id="read-them-forwards">Read them forwards</h2>

<p>Once you stop reading limits as obstacles, they start answering design
questions before you have to ask them.</p>

<ul>
  <li><strong>Heap is 6 MB.</strong> So a job that has to touch a million records is Batch Apex,
and you knew that before you wrote a line.</li>
  <li><strong>CPU time is 10 seconds.</strong> So the formula field that recalculates a rollup
across a hierarchy is not going in a trigger.</li>
  <li><strong>Callouts are limited to 100, and none after DML.</strong> So the integration is
asynchronous, and you knew <em>that</em> before the first design meeting.</li>
</ul>

<p>Each of those is a decision the platform has already made. Arguing with it costs
a sprint; reading it costs an afternoon.</p>

<h2 id="the-one-exception">The one exception</h2>

<p>None of this makes the limits <em>comfortable</em>. The 10-second CPU limit in
particular punishes code that is correct but naive, and the error it throws
tells you nothing about which line was slow.</p>

<p>But that is a complaint about the diagnostics, not about the constraint. The
constraint is doing its job: it is stopping you from writing something that
would have fallen over in production anyway, six months later, when the data
volume caught up with you.</p>

<p>Better to find out in the sandbox.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Apex" /><category term="Architecture" /><summary type="html"><![CDATA[Everyone treats governor limits as something to survive. They are better read as a spec — the platform telling you what shape your code should be.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" /><media:content medium="image" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Naming things in a Salesforce org</title><link href="https://trailblazer.imswarnil.com/blog/naming-things-in-an-org/" rel="alternate" type="text/html" title="Naming things in a Salesforce org" /><published>2026-05-10T00:00:00+05:30</published><updated>2026-05-10T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/naming-things-in-an-org</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/naming-things-in-an-org/"><![CDATA[<p>This post doubles as a layout demo: <code class="language-plaintext highlighter-rouge">sidebar: left</code> in front matter mirrors
the article shell, rail on the left. Everything else about the page is
unchanged, which is the point — the shell is a knob, not a fork.</p>

<h2 id="the-argument">The argument</h2>

<p>Every org contains a field called something like <code class="language-plaintext highlighter-rouge">Amount2__c</code>, and every
<code class="language-plaintext highlighter-rouge">Amount2__c</code> tells the same story: someone needed a second amount, the first
one was taken, and the deadline won. The name is a confession that the data
model stopped being designed at some point and started being appended to.</p>

<p>Names are the cheapest architecture there is. Three rules cover most of it:</p>

<ul>
  <li><strong>Name the meaning, not the mechanics.</strong> <code class="language-plaintext highlighter-rouge">Renewal_Amount__c</code>, not
<code class="language-plaintext highlighter-rouge">Amount2__c</code>; <code class="language-plaintext highlighter-rouge">Is_Billing_Synced__c</code>, not <code class="language-plaintext highlighter-rouge">Flag3__c</code>.</li>
  <li><strong>Put the unit in the name</strong> when the type does not carry it —
<code class="language-plaintext highlighter-rouge">Term_Months__c</code> has prevented more bugs than most validation rules.</li>
  <li><strong>A prefix is a namespace.</strong> <code class="language-plaintext highlighter-rouge">INT_</code> for integration-owned fields tells an
admin “changing this breaks something you cannot see” at a glance, which
is the entire job of a naming convention: making danger legible.</li>
</ul>

<h2 id="the-uncomfortable-corollary">The uncomfortable corollary</h2>

<p>You cannot rename your way out of a bad model — a field named beautifully
for the wrong concept is worse than an ugly one, because it looks
deliberate. Naming is the finish, not the structure; the structure is the
<a href="/blog/field-audit-part-one/">field audit</a>.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Process" /><category term="Architecture" /><summary type="html"><![CDATA[A demonstration of the left-sidebar layout — and a real argument about why Amount2__c is a confession, not a name.]]></summary></entry><entry><title type="html">A letter to my first org</title><link href="https://trailblazer.imswarnil.com/blog/a-letter-to-my-first-org/" rel="alternate" type="text/html" title="A letter to my first org" /><published>2026-04-20T00:00:00+05:30</published><updated>2026-04-20T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/a-letter-to-my-first-org</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/a-letter-to-my-first-org/"><![CDATA[<p>This post doubles as a layout demo: <code class="language-plaintext highlighter-rouge">sidebar: false</code> drops the rail and
centres the column at reading width, which is the shape for writing that is
not reference material — nobody needs a table of contents for a letter.</p>

<hr />

<p>Dear org 00D4x0000012345,</p>

<p>I am sorry about the trigger. All four of them, on Account, each written in
a different year by a different version of me, each convinced it was the
last one anyone would need. I know now why the platform people talk about
one trigger per object the way they do — they had met me before I arrived.</p>

<p>I am sorry about <code class="language-plaintext highlighter-rouge">Amount2__c</code>. You know what you did, and so do I.</p>

<p>Thank you for the governor limits. I called them many things in 2017, none
printable, all wrong. They were the only code review I had that year — the
only voice in the building saying <em>this will not scale</em> to someone who did
not yet know the word “scale” could apply to him.</p>

<p>You taught me that every org is an archaeology site, that every weird field
had a reasonable Tuesday behind it, and that the person whose mess I was
cursing was usually me with less sleep. I have cursed gently ever since, in
your honour.</p>

<p>The new org says hello. It has one trigger per object. You would hate it.</p>

<p>— A.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Craft" /><summary type="html"><![CDATA[A demonstration of the bare narrow layout — no sidebar, no cover, just a reading column — carrying the one non-technical post every site needs.]]></summary></entry><entry><title type="html">Migrating forty Aura components without a freeze</title><link href="https://trailblazer.imswarnil.com/blog/migrating-forty-aura-components/" rel="alternate" type="text/html" title="Migrating forty Aura components without a freeze" /><published>2026-04-02T00:00:00+05:30</published><updated>2026-04-02T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/migrating-forty-aura-components</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/migrating-forty-aura-components/"><![CDATA[<p>The console had forty-one Aura components, six years of accumulated behaviour,
and four hundred agents using it every day. The ask was to move it to Lightning
Web Components without a feature freeze, because the roadmap did not have room
for one.</p>

<p>It took eleven weeks. Here is what worked.</p>

<h2 id="do-not-start-with-the-hard-one">Do not start with the hard one</h2>

<p>The instinct is to prove the approach on the worst component — if the gnarliest
one converts, the rest are easy. That instinct produces three weeks of work with
nothing shipped and a team that has stopped believing the project will finish.</p>

<p>Start with the most <em>boring</em> one instead. Ours was a badge that displayed a case
status. Two hours, shipped the same day, and suddenly the migration was a thing
that happened rather than a thing that was planned.</p>

<h2 id="the-order-that-worked">The order that worked</h2>

<ol class="tb-steps"><li>
    <span class="tb-steps__title">Leaf components first</span><span class="tb-u-fg-muted tb-u-text-sm">Anything with no children and no events. Roughly half the list, and each one is an afternoon.
</span>
  </li><li>
    <span class="tb-steps__title">Then shared utilities</span><span class="tb-u-fg-muted tb-u-text-sm">The date formatter, the toast wrapper, the field-level-security check. Convert once, and every later component gets easier.
</span>
  </li><li>
    <span class="tb-steps__title">Then containers, bottom-up</span><span class="tb-u-fg-muted tb-u-text-sm">A container can hold LWC children while it is still Aura. The reverse is not true — which is what dictates the whole order.
</span>
  </li><li>
    <span class="tb-steps__title">The application event bus last</span><span class="tb-u-fg-muted tb-u-text-sm">It was the only genuinely hard piece, and by then it was the only piece left.
</span>
  </li></ol>

<p>That third point is the whole strategy. <strong>Aura can contain LWC. LWC cannot
contain Aura.</strong> Every migration order that ignores that fact ends up with a
component that cannot be converted until its parent is, and a parent that cannot
be converted until its children are.</p>

<div class="tb-callout tb-callout--warning"><svg class="tb-icon tb-icon--md" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
  <use href="#tb-alert"></use>
</svg>

  <div class="tb-callout__body"><strong class="tb-callout__title">The one that caught us</strong>
    <p>An Aura parent passing an object into an LWC child gets a read-only proxy. Code that mutated the incoming object worked in Aura and silently failed in LWC — no error, just a value that did not change.
</p>
  </div>
</div>

<h2 id="what-it-cost">What it cost</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Before</th>
      <th>After</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Console load</td>
      <td>4.1s</td>
      <td>1.3s</td>
    </tr>
    <tr>
      <td>Components</td>
      <td>41 Aura</td>
      <td>38 LWC</td>
    </tr>
    <tr>
      <td>Bundle size</td>
      <td>890 KB</td>
      <td>310 KB</td>
    </tr>
    <tr>
      <td>Test coverage</td>
      <td>71%</td>
      <td>88%</td>
    </tr>
  </tbody>
</table>

<p>Thirty-eight, not forty-one: three components turned out to be doing something
the platform now does natively, and the migration was the first time anyone had
looked at them closely enough to notice. That is a real and under-advertised
benefit of this kind of work.</p>

<h2 id="what-i-would-do-differently">What I would do differently</h2>

<p>Convert the event bus <strong>third</strong>, not last. We spent the final fortnight with a
compatibility shim translating application events in both directions, and every
bug in that period turned out to be the shim. Doing it early would have meant a
harder week three and a much calmer week ten.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="LWC" /><category term="Migration" /><summary type="html"><![CDATA[We moved a Service Cloud console from Aura to LWC over eleven weeks, with the team still shipping features. Here is the order we did it in and what it cost.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" /><media:content medium="image" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A scratch org per pull request</title><link href="https://trailblazer.imswarnil.com/blog/scratch-orgs-per-pull-request/" rel="alternate" type="text/html" title="A scratch org per pull request" /><published>2026-02-11T00:00:00+05:30</published><updated>2026-02-11T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/scratch-orgs-per-pull-request</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/scratch-orgs-per-pull-request/"><![CDATA[<p>Before: someone deployed to the shared integration org, discovered a conflict,
and the next four people were blocked while it was untangled. Release day was a
war room.</p>

<p>After: every pull request spins up its own scratch org, deploys into it, runs
the full test suite, and destroys the org when the PR closes. Nobody blocks
anybody.</p>

<h2 id="the-workflow">The workflow</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Validate PR</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">scratch</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install the CLI</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">npm install --global @salesforce/cli</span>

      <span class="c1"># The Dev Hub auth URL lives in a repository secret. It is the only</span>
      <span class="c1"># credential in the whole pipeline, and it never touches the log.</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Authenticate to the Dev Hub</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "$" &gt; auth.txt</span>
          <span class="s">sf org login sfdx-url --sfdx-url-file auth.txt --set-default-dev-hub</span>
          <span class="s">rm auth.txt</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create the scratch org</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">sf org create scratch -f config/project-scratch-def.json -a ci -d 1 -y </span><span class="m">1</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Push and test</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">sf project deploy start -o ci</span>
          <span class="s">sf apex run test -o ci --code-coverage --result-format human --wait 20</span>

      <span class="c1"># `always()` matters: without it a failed test leaves the org alive and</span>
      <span class="c1"># the daily scratch-org limit is gone by Thursday.</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Delete the scratch org</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">always()</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">sf org delete scratch -o ci --no-prompt</span>
</code></pre></div></div>

<h2 id="the-parts-that-are-not-obvious">The parts that are not obvious</h2>

<p><strong>Scratch org limits are the real constraint.</strong> A Dev Hub has a daily and an
active allocation, and a busy repository will hit both. Setting the duration to
one day rather than the default seven is most of the fix; the <code class="language-plaintext highlighter-rouge">always()</code> cleanup
step is the rest.</p>

<div class="tb-callout tb-callout--warning"><svg class="tb-icon tb-icon--md" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
  <use href="#tb-alert"></use>
</svg>

  <div class="tb-callout__body"><strong class="tb-callout__title">Do not skip the cleanup</strong>
    <p>A workflow that deletes the org only on success will, within a week, be a workflow that cannot create orgs at all — because every failed run has left one behind.
</p>
  </div>
</div>

<p><strong>Test data has to come from somewhere.</strong> A scratch org is genuinely empty.
Either every test builds its own data with a factory, or you maintain a seed
plan under <code class="language-plaintext highlighter-rouge">data/</code>. We started with the seed plan, then moved to factories
because the plan drifted from the schema and nobody noticed until it broke.</p>

<p><strong>It is slower than you want.</strong> Ours takes eleven minutes end to end, most of
it org creation. That is fine for a PR gate and useless as an inner-loop tool —
developers still work in a long-lived personal scratch org and only feel this
when they push.</p>

<h2 id="was-it-worth-it">Was it worth it</h2>

<p>Two days to build, and release-day deployment failures went from routine to
rare. The number I did not expect: the review queue moved faster, because a
reviewer who can see a green run on a real org spends less time reading the
diff for deployment problems and more time reading it for actual problems.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="DevOps" /><category term="SFDX" /><summary type="html"><![CDATA[Every PR gets a fresh org, deploys into it, runs the tests and destroys it. The setup took two days and removed an entire category of release-day panic.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" /><media:content medium="image" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Platform Events instead of polling</title><link href="https://trailblazer.imswarnil.com/blog/platform-events-instead-of-polling/" rel="alternate" type="text/html" title="Platform Events instead of polling" /><published>2025-12-05T00:00:00+05:30</published><updated>2025-12-05T00:00:00+05:30</updated><id>https://trailblazer.imswarnil.com/blog/platform-events-instead-of-polling</id><content type="html" xml:base="https://trailblazer.imswarnil.com/blog/platform-events-instead-of-polling/"><![CDATA[<p>The billing system needed to know when an opportunity closed. The existing
answer was a scheduled job every fifteen minutes that queried everything
modified since the last run and posted it onwards.</p>

<p>It worked. It also meant an average of eight minutes between “the deal closed”
and “billing knows”, a query that got slower every quarter, and 96 API calls a
day spent mostly discovering that nothing had happened.</p>

<h2 id="what-we-changed">What we changed</h2>

<p>A Platform Event published from the trigger, and a subscriber on the other side.</p>

<div class="language-apex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The event is a fact about the past, not a command. Named accordingly —</span>
<span class="c1">// OpportunityClosed, not CloseOpportunity — because a subscriber that reads</span>
<span class="c1">// it as an instruction will eventually do something we did not ask for.</span>
<span class="n">List</span><span class="o">&lt;</span><span class="n">Opportunity_Closed__e</span><span class="o">&gt;</span> <span class="n">events</span> <span class="o">=</span> <span class="k">new</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Opportunity_Closed__e</span><span class="o">&gt;</span><span class="p">();</span>

<span class="k">for</span> <span class="p">(</span><span class="n">Opportunity</span> <span class="n">o</span> <span class="p">:</span> <span class="n">Trigger</span><span class="o">.</span><span class="py">new</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Opportunity</span> <span class="n">old</span> <span class="o">=</span> <span class="n">Trigger</span><span class="o">.</span><span class="py">oldMap</span><span class="o">.</span><span class="nf">get</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="py">Id</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="py">IsWon</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">old</span><span class="o">.</span><span class="py">IsWon</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">events</span><span class="o">.</span><span class="nf">add</span><span class="p">(</span><span class="k">new</span> <span class="nf">Opportunity_Closed__e</span><span class="p">(</span>
            <span class="n">Opportunity_Id__c</span> <span class="o">=</span> <span class="n">o</span><span class="o">.</span><span class="py">Id</span><span class="p">,</span>
            <span class="n">Amount__c</span>         <span class="o">=</span> <span class="n">o</span><span class="o">.</span><span class="py">Amount</span><span class="p">,</span>
            <span class="n">Closed_At__c</span>      <span class="o">=</span> <span class="n">System</span><span class="o">.</span><span class="nf">now</span><span class="p">()</span>
        <span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// publish() returns a result per event and does NOT throw. Ignoring the</span>
<span class="c1">// return value is the single most common way to lose an event silently.</span>
<span class="k">for</span> <span class="p">(</span><span class="n">Database</span><span class="o">.</span><span class="py">SaveResult</span> <span class="n">sr</span> <span class="p">:</span> <span class="n">EventBus</span><span class="o">.</span><span class="nf">publish</span><span class="p">(</span><span class="n">events</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">sr</span><span class="o">.</span><span class="nf">isSuccess</span><span class="p">())</span> <span class="p">{</span>
        <span class="n">Logger</span><span class="o">.</span><span class="nf">error</span><span class="p">(</span><span class="s1">'</span><span class="s2">Failed to publish OpportunityClosed'</span><span class="p">,</span> <span class="n">sr</span><span class="o">.</span><span class="nf">getErrors</span><span class="p">());</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-four-things-worth-knowing">The four things worth knowing</h2>

<p><strong>Publishing is not transactional in the way you expect.</strong> With the default
<code class="language-plaintext highlighter-rouge">PublishAfterCommit</code> behaviour, the event goes out only if the transaction
commits — which is what you want. With <code class="language-plaintext highlighter-rouge">PublishImmediately</code>, it goes out even if
the transaction rolls back, and you have told a downstream system about
something that did not happen.</p>

<p><strong>Delivery is at-least-once.</strong> A subscriber will occasionally see the same event
twice. Every handler needs to be idempotent, and the cheapest way to get there
is a unique external id on whatever the subscriber writes.</p>

<p><strong>The event bus retains for 72 hours.</strong> That is your entire replay window. A
subscriber that is down over a long weekend does not catch up.</p>

<div class="tb-callout tb-callout--warning"><svg class="tb-icon tb-icon--md" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
  <use href="#tb-alert"></use>
</svg>

  <div class="tb-callout__body"><strong class="tb-callout__title">Store the replay id</strong>
    <p>A CometD subscriber that reconnects without a stored replay id resumes from ‘now’ and silently drops everything it missed while disconnected. Persist it after every batch.
</p>
  </div>
</div>

<p><strong>High-volume events have their own allocation</strong>, and it is consumed by
delivery, not by publication. One event delivered to four subscribers is four
against the limit.</p>

<h2 id="the-numbers">The numbers</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Polling</th>
      <th>Platform Events</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Average latency</td>
      <td>8 min</td>
      <td>1.4 s</td>
    </tr>
    <tr>
      <td>API calls/day</td>
      <td>96</td>
      <td>~40 (actual closures)</td>
    </tr>
    <tr>
      <td>Code</td>
      <td>1 scheduled class, 1 batch</td>
      <td>1 trigger, 1 subscriber</td>
    </tr>
  </tbody>
</table>

<p>The API graph flattening was the part that got the change approved. The latency
was the part that mattered.</p>]]></content><author><name>Ava Rodriguez</name><email>hello@example.com</email></author><category term="Integration" /><category term="Platform Events" /><summary type="html"><![CDATA[We replaced a fifteen-minute polling job with Platform Events. Latency went from eight minutes average to under two seconds, and the API usage graph went flat.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" /><media:content medium="image" url="https://trailblazer.imswarnil.com/assets/img/covers/blog.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>