<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[AIL Studio]]></title><description><![CDATA[AIL Studio]]></description><link>https://ailstudio.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 05 Nov 2026 06:04:25 GMT</lastBuildDate><atom:link href="https://ailstudio.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Know your room left before every trade: a daily loss limit tracker in a spreadsheet]]></title><description><![CDATA[By Richard C, AIL Studio Most prop traders know their daily loss limit. Fewer know, at 1:30 PM after three trades, exactly how much of it is left, and…]]></description><link>https://ailstudio.hashnode.dev/know-your-room-left-before-every-trade-a-daily-loss-limit-tracker-in-a-spreadsheet</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/know-your-room-left-before-every-trade-a-daily-loss-limit-tracker-in-a-spreadsheet</guid><category><![CDATA[spreadsheets]]></category><category><![CDATA[#googlesheets]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[trading, ]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Wed, 04 Nov 2026 18:12:18 GMT</pubDate><content:encoded><![CDATA[<p>By Richard C, AIL Studio</p>
<p>Most prop traders know their daily loss limit. Fewer know, at 1:30 PM after three trades, exactly how much of it is left, and whether the stop on the next trade still fits inside it. That's the moment the limit gets broken: not on one huge loss, but on a normal-sized trade taken with too little room left.</p>
<p>This post builds a small log that answers two questions after every trade: how much room is left today, and can I take the next trade at its planned stop?</p>
<blockquote>
<p><strong>Example values only.</strong> The 2,000 limit below is made up. Firms measure daily loss differently (from start-of-day balance or equity, with or without open P&amp;L, with different reset times). Check your own firm's written rules and change the setup to match.</p>
</blockquote>
<h2>1. Set up the log</h2>
<p>One row per closed trade. Columns:</p>
<ul>
<li><strong>A Date</strong>, <strong>B Time</strong> (a real time value, not text)</li>
<li><strong>C Trade</strong>, <strong>D Realized P&amp;L</strong></li>
<li><strong>E Day P&amp;L so far</strong>, <strong>F Room left</strong>, <strong>H Status</strong> (formulas)</li>
<li><strong>G Next stop risk</strong>: what you'd lose if the next trade hits its stop</li>
</ul>
<p>Put the settings to the side: <strong>K1</strong> = daily loss limit (example: 2000), <strong>K2</strong> = caution level as a share of the limit (example: 25%).</p>
<h2>2. Running day P&amp;L</h2>
<p>In E2, then fill down:</p>
<pre><code class="language-plaintext">=SUMIFS($D$2:$D$100,$A$2:$A$100,A2,$B$2:$B$100,"&lt;="&amp;B2)
</code></pre>
<p>It adds every trade on the same date up to and including this one. It resets each new date, and the order of rows doesn't matter because it filters by time.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/80d72e1a-c855-43de-b7fd-ec764f464037.png" alt="Running day P&amp;L, E3 selected with the SUMIFS formula in the formula bar. Example data." style="display:block;margin:0 auto" />

<p>Keep B as real times. If you type times as text, the <code>"&lt;="&amp;B2</code> comparison stops working and every row shows 0.</p>
<h2>3. Room left</h2>
<p>In F2:</p>
<pre><code class="language-plaintext">=$K$1+E2
</code></pre>
<p>This assumes a fixed limit measured from start-of-day balance, so a winning trade adds room (2,350 after the first trade above). If your firm doesn't work that way, this is the cell to change.</p>
<h2>4. Status: can I take the next trade?</h2>
<p>In H2:</p>
<pre><code class="language-plaintext">=IF(F2&lt;=0,"STOP: limit hit",IF(G2&gt;F2,"Skip: stop too big",IF(F2&lt;$K$2*$K$1,"Caution","OK")))
</code></pre>
<p>In order:</p>
<ol>
<li>Room at or below zero: the limit is gone.</li>
<li>The next stop risk is bigger than the room left: a single stop-out would break the limit, so skip it or size down.</li>
<li>Room under 25% of the limit: caution.</li>
<li>Otherwise, OK.</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/b2b60808-5450-4977-b2df-77d09aefab4e.png" alt="Status column: &quot;Skip: stop too big&quot; when 400 of room is left and the planned stop risks 500; &quot;STOP: limit hit&quot; at -50. Example data." style="display:block;margin:0 auto" />

<p>In the example, day one ends with 400 of room left and a planned 500 stop. The sheet says skip, and that's the trade that would have broken the limit. On day two nobody checked, and the last trade takes room to -50.</p>
<p>Add conditional formatting so "Skip" and "STOP" stand out at a glance.</p>
<h2>5. One row per day</h2>
<p>On a second sheet, list each date once:</p>
<pre><code class="language-plaintext">=SORT(UNIQUE(FILTER(Log!A2:A100,Log!A2:A100&lt;&gt;"")))
</code></pre>
<p>Then, for each date in A2:</p>
<ul>
<li>Day P&amp;L: <code>=SUMIFS(Log!$D$2:$D$100,Log!$A$2:$A$100,A2)</code></li>
<li>Worst point: <code>=MINIFS(Log!$E$2:$E$100,Log!$A$2:$A$100,A2)</code></li>
<li>Lowest room: <code>=Log!$K$1+C2</code></li>
<li>Limit hit?: <code>=IF(D2&lt;=0,"Yes","No")</code></li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/5ae49eec-30de-492b-a1cc-53b1d5f65e35.png" alt="Per-day summary: worst point -2,050 and limit hit on 2026-11-03. Example data." style="display:block;margin:0 auto" />

<p>"Worst point" matters more than the day's close. A day can dip below the limit and then recover, and some firms count the dip; check your firm's rules. This sheet only sees closed trades, though. If your firm counts open P&amp;L, a dip while a trade was open won't show up here.</p>
<h2>6. Use it before you click</h2>
<p>The routine is short:</p>
<ol>
<li>Log the trade the moment it closes.</li>
<li>Before the next entry, type its stop risk in G.</li>
<li>If H doesn't say OK, don't place the trade as planned.</li>
</ol>
<p>SORT, UNIQUE, FILTER and MINIFS need Excel 365 or 2021, or Google Sheets. Not tested there, so check them once in your own copy.</p>
<p>If you'd rather start from a ready-made log that tracks your trades and stats, the <a href="https://ailstudio.gumroad.com/l/qsnmk?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=daily-loss-room-left">Trader's Edge Journal</a> is ours.</p>
]]></content:encoded></item><item><title><![CDATA[Keep or cancel? A 10-minute monthly subscription review in a spreadsheet]]></title><description><![CDATA[By Richard C, AIL Studio Finding your subscriptions is the easy part. The hard part is deciding, every month, which ones still earn their place. "I might…]]></description><link>https://ailstudio.hashnode.dev/keep-or-cancel-a-10-minute-monthly-subscription-review-in-a-spreadsheet</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/keep-or-cancel-a-10-minute-monthly-subscription-review-in-a-spreadsheet</guid><category><![CDATA[spreadsheets]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[PersonalFinance]]></category><category><![CDATA[#googlesheets]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Wed, 04 Nov 2026 17:06:25 GMT</pubDate><content:encoded><![CDATA[<p>By Richard C, AIL Studio</p>
<p>Finding your subscriptions is the easy part. The hard part is deciding, every month, which ones still earn their place. "I might use it" keeps a lot of them alive. This post builds a small review sheet that turns that into a rule: cost per use, days since you last used it, and a Keep / Review / Cancel column that fills itself in.</p>
<p>It takes about 10 minutes a month. No add-ons, no scripts.</p>
<h2>1. One row per subscription</h2>
<p>You need a list of what you pay for. If you don't have one yet, your bank or card statement is the place to start. Then add two things you only know yourself:</p>
<ul>
<li><strong>Uses (30 days):</strong> a rough count. Opened the app, went to the gym, watched something. Guess honestly; it doesn't need to be exact.</li>
<li><strong>Last used:</strong> the date you last used it.</li>
</ul>
<p>Columns: Service (A), Monthly cost (B), Uses (C), Last used (D). Put your review date in J1 (the first of the month works) and your limit for "too expensive per use" in J2. I use 3.</p>
<h2>2. Days idle and cost per use</h2>
<p>Days idle in E2:</p>
<pre><code class="language-plaintext">=$J$1-D2
</code></pre>
<p>Cost per use in F2:</p>
<pre><code class="language-plaintext">=IF(C2=0,"",B2/C2)
</code></pre>
<p>The <code>IF</code> keeps a zero-use service from causing a divide-by-zero error; it just stays blank. Format E as a whole number and F as a number with two decimals. Then fill both columns down.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/07c1bfcf-de12-42ee-95ca-a5664cb24f07.png" alt="" style="display:block;margin:0 auto" />

<p><em>Example data: made-up services. One use of a 9.99 service costs 9.99; daily use of 2.99 storage costs about 10 cents a time.</em></p>
<p>Cost per use is the number that changes minds. A 44.00 gym visited three times is 14.67 a visit. That might still be worth it, but now it's a choice.</p>
<h2>3. Let a rule make the first call</h2>
<p>In G2:</p>
<pre><code class="language-plaintext">=IF(OR(C2=0,E2&gt;60),"Cancel",IF(F2&gt;$J$2,"Review","Keep"))
</code></pre>
<p>The rule reads:</p>
<ul>
<li><strong>Cancel</strong> if you didn't use it at all this month, or haven't touched it in over 60 days.</li>
<li><strong>Review</strong> if each use costs more than your limit in J2.</li>
<li><strong>Keep</strong> otherwise.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/4bd020f7-3243-48d9-a44b-2bd15d3e29e3.png" alt="" style="display:block;margin:0 auto" />

<p><em>Example data: the news site and the language app haven't been used in 79 and 122 days, so they come up as Cancel.</em></p>
<p>The rule is a first call, not the final word. "Review" means you decide: a pricey gym might be fine if you're going more next month. Change the 60 days and the limit in J2 until the results match what you'd decide yourself.</p>
<h2>4. Add up what's at stake</h2>
<p>Three summary cells to the right:</p>
<pre><code class="language-plaintext">J4: =SUMIFS(B2:B9,G2:G9,"Cancel")*12
J5: =COUNTIF(G2:G9,"Review")
J6: =SUM(B2:B9)
</code></pre>
<p>J4 is what cancelling the "Cancel" rows saves in a year, J5 is how many need a decision, and J6 is your monthly total today.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/24014a8b-c217-4ff7-9ef5-d7680e528caa.png" alt="" style="display:block;margin:0 auto" />

<p><em>Example data: cancelling two small, unused services saves 203.88 a year.</em></p>
<h2>5. Make it monthly</h2>
<p>On the first of each month:</p>
<ol>
<li>Change the review date in J1.</li>
<li>Update Uses and Last used for each row.</li>
<li>Act on every Cancel, and decide every Review.</li>
<li>Write down what you cancelled and when, so you can check the charge stops next month.</li>
</ol>
<p>Copy the sheet each month (Duplicate sheet, rename it "Nov 2026") if you want a history of what you dropped and how your monthly total moved.</p>
<h2>Honest limits</h2>
<ul>
<li><strong>Use counts are self-reported.</strong> The sheet is only as honest as your guesses.</li>
<li><strong>Some value isn't per use.</strong> Backup storage or insurance-like services earn their keep by existing. Mark those Keep by hand.</li>
<li><strong>Annual plans need dividing first.</strong> Put the monthly equivalent (yearly price / 12) in column B, and note the renewal date somewhere so cancelling happens before it renews.</li>
<li><strong>Cancel means actually cancelling.</strong> The sheet doesn't stop any charge; you still have to do that on each service's account page.</li>
</ul>
<hr />
<p>If you'd like the list side handled for you, with renewal dates and every billing cycle converted to monthly and yearly cost, I made a <a href="https://ailstudio.netlify.app/products/subscription-tracker/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=monthly-keep-or-cancel-review">Subscription Tracker for Google Sheets and Excel</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How to find missing and duplicate records after a Salesforce migration (match on an external key)]]></title><description><![CDATA[By Richard C, AIL Studio The migration finished and the record counts look about right: 9 Accounts in the old system, 8 in Salesforce. Close enough? Not…]]></description><link>https://ailstudio.hashnode.dev/how-to-find-missing-and-duplicate-records-after-a-salesforce-migration-match-on-an-external-key</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/how-to-find-missing-and-duplicate-records-after-a-salesforce-migration-match-on-an-external-key</guid><category><![CDATA[Salesforce]]></category><category><![CDATA[data]]></category><category><![CDATA[migration]]></category><category><![CDATA[spreadsheets]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Wed, 04 Nov 2026 14:20:24 GMT</pubDate><content:encoded><![CDATA[<p>By Richard C, AIL Studio</p>
<p>The migration finished and the record counts look about right: 9 Accounts in the old system, 8 in Salesforce. Close enough? Not quite. "About right" can hide a record that never arrived, one that arrived twice, and one that showed up from nowhere, all cancelling each other out. This post shows how to find each of those with two CSV exports and a spreadsheet.</p>
<h2>1. Why you can't match on record IDs</h2>
<p>Salesforce gives every record a new 15/18-character Id when it's inserted. The old system's IDs don't survive the move, so you can't line the two exports up by Id. You need a key that exists on both sides.</p>
<h2>2. Pick a stable external key</h2>
<p>Most migrations carry the old system's ID into a custom field, often marked as an External ID (for example <code>Legacy_ID__c</code>). That's the key. Export it on both sides:</p>
<ul>
<li><strong>Source:</strong> the old system's export, with its ID column.</li>
<li><strong>Target:</strong> a Salesforce report or Data Loader export with <code>Legacy_ID__c</code>, Name and Id.</li>
</ul>
<p>If there's no shared key, stop here. Matching on name or email is a different (and much fuzzier) job, and this method won't do it reliably.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/2aa5379f-f150-4fc5-b76a-ec9ea793f446.png" alt="Example data: the Source sheet in LibreOffice Calc with the status formula in the formula bar" />
<em>Example data: the Source sheet with the D4 status formula showing in the formula bar. Fake Accounts; across both sheets there are six problems. Calc's formula bar shows <code>$Target.A$2</code>; in Excel and Google Sheets the same reference is written <code>Target!A$2</code>.</em></p>
<p>Put the source on a sheet called <code>Source</code> and the target on a sheet called <code>Target</code>, each with the key in column A.</p>
<h2>3. Check for blank and duplicate keys first</h2>
<p>A blank or repeated key makes the missing/extra check lie, so count each key on its own sheet first. On <code>Source</code>, in C2:</p>
<pre><code>=SUMPRODUCT(--EXACT(A$2:A$10,A2))
</code></pre>
<p>Change 10 to your last row. Why not <code>COUNTIF</code>? <code>COUNTIF</code> ignores case, so <code>ACC-1007</code> and <code>acc-1007</code> would count as the same key. <code>EXACT</code> is case-sensitive, which is what an External ID comparison needs. It also doesn't trim spaces, which is deliberate: a key with a stray space really is a different key, and you want to see it, not have it silently fixed.</p>
<p>Anything above 1 is a duplicate key. An empty cell in column A is a blank key.</p>
<h2>4. Match both directions</h2>
<p>Now one status formula per row. On <code>Source</code>, D2:</p>
<pre><code>=IF(LEN(A2)=0,"Blank key",IF(C2&gt;1,"Duplicate key",IF(SUMPRODUCT(--EXACT(Target!A$2:A$9,A2))=0,"Missing in target","OK")))
</code></pre>
<p><img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/b23a8d19-ea01-4aa8-8f9b-cf128fdcad58.png" alt="Example data: Source sheet in LibreOffice Calc with key counts and status" />
<em>Example data: one record never arrived (ACC-1003), one looks missing because of case (ACC-1007), a duplicate key and a blank key.</em></p>
<p>On <code>Target</code>, put the same count in D2 (<code>=SUMPRODUCT(--EXACT(A$2:A$9,A2))</code>) and the mirror-image status in E2:</p>
<pre><code>=IF(LEN(A2)=0,"Blank key",IF(D2&gt;1,"Duplicate key",IF(SUMPRODUCT(--EXACT(Source!A$2:A$10,A2))=0,"Extra in target","OK")))
</code></pre>
<p><img src="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/ccf8638d-3bb6-4e3e-a7a9-3db3f307680e.png" alt="Example data: Target sheet in LibreOffice Calc with key counts and status" />
<em>Example data: ACC-1002 was loaded twice, acc-1007 doesn't match its source key, and ACC-1009 has no source record.</em></p>
<p>Reading the results:</p>
<ul>
<li><strong>Missing in target</strong> (ACC-1003): the record never loaded. Check the load's error file.</li>
<li><strong>Extra in target</strong> (ACC-1009): a record with no source. Maybe it was created by hand after the load, or came from another file.</li>
<li><strong>Duplicate key in target</strong> (ACC-1002): the load ran twice, or an insert was used where an upsert should have been. Note the source row says "OK", so you only catch it by checking both sides.</li>
<li><strong>Case mismatch</strong> (ACC-1007 vs acc-1007): the same record flagged on both sides. Fix the key, don't loosen the match.</li>
<li><strong>Blank and duplicate source keys</strong>: fix these in the source before trusting anything else.</li>
</ul>
<h2>5. Repeat per object, parents first</h2>
<p>Do Accounts first, then Contacts, then Opportunities. A missing Account usually explains missing Contacts and Opportunities underneath it, so fixing parents first saves you from chasing the same gap three times.</p>
<h2>What this doesn't check</h2>
<p>This only answers "did every record arrive exactly once?". It doesn't compare field values, and it doesn't check that a Contact sits under the right Account. For that, see <a href="https://ailstudio.hashnode.dev/how-to-catch-wrong-parent-contacts-after-a-salesforce-migration?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=missing-records-internal">How to catch wrong-parent Contacts after a Salesforce migration</a> and the SOQL version, <a href="https://ailstudio.hashnode.dev/how-to-audit-contact-account-parents-after-a-salesforce-migration-with-soql?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=missing-records-internal">How to audit Contact–Account parents after a Salesforce migration with SOQL</a>.</p>
<hr />
<p>If you'd rather load before-and-after CSVs in a desktop browser and get one findings report for missing records, changed fields and wrong parent Accounts, I made <a href="https://ailstudio.gumroad.com/l/ufgfjf?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=missing-records-external-key">RecordBridge</a> (US$49, one-time).</p>
]]></content:encoded></item><item><title><![CDATA[How to Audit Contact–Account Parents After a Salesforce Migration with SOQL]]></title><description><![CDATA[Find Contacts on the wrong Account after a Salesforce migration: SOQL queries that audit Contact-Account parents through a stable external ID.]]></description><link>https://ailstudio.hashnode.dev/how-to-audit-contact-account-parents-after-a-salesforce-migration-with-soql</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/how-to-audit-contact-account-parents-after-a-salesforce-migration-with-soql</guid><category><![CDATA[Salesforce]]></category><category><![CDATA[SOQL]]></category><category><![CDATA[data migration]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Mon, 02 Nov 2026 17:24:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/17e8c77f-748d-413c-a66d-1f16a26f5fe9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Richard C, AIL Studio · 7x certified Salesforce professional</p>
<p><strong>Disclosure:</strong> I make and sell RecordBridge, a browser tool that compares Salesforce CSV exports and flags Contacts or Opportunities on the wrong Account. This article teaches a native Salesforce check with SOQL, Data Loader, and a report. There are two optional links to my products: a free Guide and RecordBridge (US$49). I have a vested interest in both.</p>
<p>This is the Salesforce-native version of the wrong-parent check: everything runs in SOQL, Data Loader, and a standard report, so you never rebuild Account lookups from flat files. A post-migration audit that only checks row counts or runs basic CSV diff tools over Contact fields will pass a Contact that now hangs off the wrong Account, because the error lives in the <strong>parent link</strong>, not in the Contact's own values.</p>
<p>You will prove your External ID keys are unique, export each Contact with its parent Account's stable key in one child-to-parent relationship query, optionally mirror those columns in a Contacts &amp; Accounts report, then compare source and target on that key.</p>
<p>Prefer pandas or Google Sheets formulas? See <a href="https://ailstudio.hashnode.dev/how-to-catch-wrong-parent-contacts-after-a-salesforce-migration?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=soql-contact-parents">How to catch wrong-parent Contacts after a Salesforce migration</a>.</p>
<h2>What "wrong parent" means</h2>
<p>Record Ids almost always change between a source org and a target org. A Contact whose <code>AccountId</code> was <code>001...AAA</code> in the source may correctly become <code>001...ZZZ</code> in the target. Comparing Id strings tells you nothing.</p>
<p>You need a <strong>stable key</strong> that identifies the same Account in both orgs — typically an External ID custom field. Call it <code>External_Key__c</code> below; substitute your real API name.</p>
<p>The check:</p>
<ol>
<li>For each Contact in the source, resolve its Account's <code>External_Key__c</code>.</li>
<li>Find the same Contact in the target by the Contact's own stable key.</li>
<li>Resolve that Contact's Account's <code>External_Key__c</code> in the target.</li>
<li>If the two Account keys differ, the Contact has the wrong parent.</li>
</ol>
<p>Same Contact fields. Different company. That is what basic "did every cell match?" diff tools miss.</p>
<h2>Prerequisites</h2>
<ul>
<li>Read access to Account and Contact in <strong>both</strong> orgs (or in before/after sandboxes that still hold the data).</li>
<li>A stable key on Account <strong>and</strong> on Contact that is populated on both sides. No shared key? Stop here; fuzzy name matching is a different project.</li>
<li>Data Loader (or another SOQL export tool), or Salesforce Reports with CSV export.</li>
<li>Permission to create or edit a report in the target org if you use the report path.</li>
</ul>
<p>All sample names and keys below are <strong>example data</strong>. Emails use the reserved <code>example.invalid</code> domain.</p>
<h2>1. Confirm the keys are safe to look up</h2>
<p>Before you trust any join, prove the Account key is unique on each side. In Developer Console, VS Code with Salesforce Extensions, or Workbench, run:</p>
<pre><code class="language-plaintext">SELECT External_Key__c, COUNT(Id) cnt
FROM Account
WHERE External_Key__c != null
GROUP BY External_Key__c
HAVING COUNT(Id) &gt; 1
</code></pre>
<p>Zero rows means every non-blank Account key is unique. Repeat for Contact:</p>
<pre><code class="language-plaintext">SELECT External_Key__c, COUNT(Id) cnt
FROM Contact
WHERE External_Key__c != null
GROUP BY External_Key__c
HAVING COUNT(Id) &gt; 1
</code></pre>
<p>Also count blanks you must treat as unresolved, not as matches. These are two separate queries; run them one at a time:</p>
<pre><code class="language-plaintext">SELECT COUNT() FROM Account WHERE External_Key__c = null
SELECT COUNT() FROM Contact WHERE External_Key__c = null OR AccountId = null
</code></pre>
<p>If duplicates or blanks are widespread, fix the keys first. Parent checks on dirty keys invent findings.</p>
<p>Salesforce documents child-to-parent relationship queries in the <a href="https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_relationships_query_using.htm">SOQL relationship query guide</a>. You will use that pattern next.</p>
<h2>2. Export Contacts with the parent Account key in one SOQL</h2>
<p>Data Loader can export related parent fields with a child-to-parent relationship query. Official tip from the <a href="https://developer.salesforce.com/docs/atlas.en-us.dataLoader.meta/dataLoader/exporting_data.htm">Data Loader export guide</a>: relationship field names are <strong>case-sensitive</strong> (<code>Account.Name</code> works; <code>ACCOUNT.NAME</code> does not). Nested parent-to-child subqueries are not supported in Data Loader, so start from Contact, not Account.</p>
<p>Run this export in the <strong>source</strong> org (Export, object Contact):</p>
<pre><code class="language-plaintext">SELECT Id,
       External_Key__c,
       LastName,
       Email,
       AccountId,
       Account.Name,
       Account.External_Key__c
FROM Contact
WHERE External_Key__c != null
</code></pre>
<p>Save as <code>source-contacts-with-parent.csv</code>. Repeat the <strong>exact same query</strong> in the <strong>target</strong> org; save as <code>target-contacts-with-parent.csv</code>.</p>
<p>You now have, on every Contact row, the parent Account's stable key without a separate Account join in your spreadsheet. That is the main advantage of doing this inside Salesforce instead of rebuilding lookups from four flat files.</p>
<p><strong>Example data</strong> (two orgs, one Contact):</p>
<table>
<thead>
<tr>
<th>Org</th>
<th>Contact External_Key__c</th>
<th>LastName</th>
<th>Email</th>
<th><code>Account.Name</code></th>
<th>Account.External_Key__c</th>
</tr>
</thead>
<tbody><tr>
<td>Source</td>
<td>CT-001</td>
<td>Renée Example</td>
<td><a href="mailto:renee@example.invalid">renee@example.invalid</a></td>
<td>Aster Labs</td>
<td>AC-001</td>
</tr>
<tr>
<td>Target</td>
<td>CT-001</td>
<td>Renée Example</td>
<td><a href="mailto:renee@example.invalid">renee@example.invalid</a></td>
<td>Blue Orchard</td>
<td>AC-002</td>
</tr>
</tbody></table>
<p>LastName and Email match. Parent Account key does not (<code>AC-001</code> vs <code>AC-002</code>). Flag as <strong>WRONG_PARENT</strong>.</p>
<p>If you also need Account-only coverage (Accounts with no Contacts, or key hygiene), export Accounts separately:</p>
<pre><code class="language-plaintext">SELECT Id, Name, External_Key__c FROM Account
</code></pre>
<h2>3. Optional: the same columns from a Contacts &amp; Accounts report</h2>
<p>If your admins live in Reports more than in Data Loader, build the same view there.</p>
<ol>
<li>Create a report with the <strong>Contacts &amp; Accounts</strong> report type (or a custom report type that starts from Contact and includes Account).</li>
<li>Add columns: Contact External Key, Account External Key, Contact Name (or Last Name), Account Name, Email.</li>
<li>Filter out rows where Contact External Key or Account External Key is blank.</li>
<li>Run once in the source context (or from a source backup org) and once in the target. Export each run as CSV.</li>
</ol>
<p>You still compare outside Salesforce. The report only replaces the SOQL export. It does <strong>not</strong> compare two orgs by itself.</p>
<h2>4. Compare source and target on the Contact key</h2>
<p>With both CSVs open (Sheets, Excel, or a small script), for every Contact <code>External_Key__c</code> present on both sides:</p>
<table>
<thead>
<tr>
<th>Contact key</th>
<th>Source Account key</th>
<th>Target Account key</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>CT-001</td>
<td>AC-001</td>
<td>AC-002</td>
<td>WRONG_PARENT</td>
</tr>
<tr>
<td>CT-002</td>
<td>AC-003</td>
<td>AC-003</td>
<td>PARENT_MATCH</td>
</tr>
</tbody></table>
<p>Rules that keep the audit honest:</p>
<ul>
<li><strong>Match Contacts on the Contact stable key</strong>, never on Salesforce Id.</li>
<li><strong>Compare Account stable keys</strong>, never <code>AccountId</code>.</li>
<li><strong>Blank or missing parent key</strong> → <code>UNRESOLVED</code>, not a match. Do not hide <code>#N/A</code> with a default.</li>
<li><strong>Contact only on one side</strong> → missing/extra record, a separate finding from wrong-parent.</li>
<li>Normalize case and trim spaces on keys before comparing; tools differ on case sensitivity.</li>
</ul>
<p>Worked result on the example rows: <strong>1 wrong-parent Contact</strong>, <strong>1 parent match</strong>, <strong>0</strong> LastName/Email differences.</p>
<h2>5. Spot-check inside the target org with SOQL</h2>
<p>After you have a shortlist of suspect Contact keys, confirm them live in the target before you change data:</p>
<pre><code class="language-plaintext">SELECT Id,
       External_Key__c,
       LastName,
       Email,
       Account.Name,
       Account.External_Key__c
FROM Contact
WHERE External_Key__c IN ('CT-001')
</code></pre>
<p>Open the Contact, open the Account, and verify the business expectation (which company should own this person). The export flags the mismatch; a human decides the fix. This article stops at detection. It is not a mass-update script.</p>
<p>The same parent pattern works for Opportunity → Account if your migration moved deals too. Swap <code>Contact</code> for <code>Opportunity</code> and use the Opportunity stable key. Data Loader still starts from the child object when you want <code>Account.External_Key__c</code> on the row.</p>
<h2>Honest limits</h2>
<ul>
<li>No stable External ID (or other shared key) on both sides → this method does not apply.</li>
<li>Comparing <code>Account.Name</code> alone is fragile (renames, duplicates, punctuation). Prefer the External ID.</li>
<li>Data Loader does not support nested parent-to-child subqueries, and polymorphic relationship queries (for example <code>TYPEOF</code> on fields like <code>WhoId</code>) may not export cleanly depending on your tool and API version; keeping exports flat with parent-dot fields avoids both.</li>
<li>A clean parent check on Contacts does not certify the whole migration. Field value drifts, sharing, and record types are separate passes.</li>
<li>Example figures and the Renée / Aster / Blue Orchard rows are labeled example data.</li>
</ul>
<h2>Wrap-up</h2>
<p>You can catch wrong-parent Contacts with tools you already have: a uniqueness query on the External ID, a Contact export that includes <code>Account.External_Key__c</code>, an optional Contacts &amp; Accounts report, and a key-to-key compare between source and target. The relationship is the thing to test; matching names and emails is not enough.</p>
<p>If you want a worked example to compare against, the free RecordBridge Guide has four small sample CSVs and a README that walk through spotting a wrong-parent Contact with spreadsheet lookups. <a href="https://ailstudio.gumroad.com/l/rjvcjs?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=rjvcjs">Free RecordBridge Guide</a></p>
<p>If you would rather load before-and-after Account, Contact, and Opportunity CSVs in a desktop browser and get a findings CSV — including wrong-parent links, missing/extra rows, and field diffs — <a href="https://ailstudio.gumroad.com/l/recordbridge?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=soql-contact-parents"><strong>RecordBridge</strong></a> is US$49 one-time. Nothing installs on Salesforce. Nothing connects to your org. Comparisons stay in the browser page's memory. It needs the same shared stable key this article uses; if you do not have one, do not buy it for that case. Built-in sample files (example data) produce 7 findings with all three objects selected, including one wrong-parent Contact.</p>
<p><strong>Disclosure (again):</strong> I am the maker of RecordBridge at AIL Studio. The SOQL and report steps above stand on their own; the product link is optional.</p>
<p>RecordBridge by AIL Studio is not affiliated with Salesforce. Salesforce is a trademark of Salesforce, Inc.</p>
]]></content:encoded></item><item><title><![CDATA[Audit Lite finds what you pay — adding a renewal countdown in Google Sheets]]></title><description><![CDATA[By Richard C · AIL Studio A short subscription list answers "what am I paying?" A renewal countdown answers "what is due soon?" This post starts from a…]]></description><link>https://ailstudio.hashnode.dev/audit-lite-finds-what-you-pay-adding-a-renewal-countdown-in-google-sheets</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/audit-lite-finds-what-you-pay-adding-a-renewal-countdown-in-google-sheets</guid><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Tue, 27 Oct 2026 18:06:03 GMT</pubDate><content:encoded><![CDATA[<p>By Richard C · AIL Studio</p>
<p>A short subscription list answers "what am I paying?" A renewal countdown answers "what is due soon?" This post starts from a finished audit list, then adds one Days left column and simple yellow / red highlighting so upcoming renewals stand out.</p>
<p>No scripts. The same formulas work in Excel.</p>
<h2>1. Start with a clean list</h2>
<p>If you do not have a list yet, build one first: name, amount, billing cycle and next renewal date. For a free starter file with Keep / Cancel / Review dropdowns and monthly / yearly totals, use <a href="https://ailstudio.gumroad.com/l/ipnphp?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=dist48-qkwkix"><strong>Subscription Audit Lite ($0+)</strong></a>. It is a small .xlsx for Google Sheets or Excel.</p>
<p>This article assumes you already have (or just built) columns like these:</p>
<pre><code class="language-plaintext">A        B       C              D
Service  Amount  Billing cycle  Next renewal
</code></pre>
<p><strong>Example data</strong> (made-up subscriptions for illustration, not real charges):</p>
<pre><code class="language-plaintext">Row  A: Service           B: Amount  C: Cycle   D: Next renewal
2    Streaming service    15.99      Monthly    2026-11-03
3    Cloud storage        9.99       Monthly    2026-11-12
4    News site            79.00      Yearly     2026-12-01
5    Music app            10.99      Monthly    2026-10-30
6    Design tools         54.00      Monthly    2026-11-20
</code></pre>
<p>You type A–D. Everything below is a formula. Put <code>Example data (not real charges)</code> above the table so screenshots stay honest.</p>
<h2>2. Days left until renewal (column E)</h2>
<pre><code class="language-plaintext">E2: =IF(D2="","",D2-TODAY())
</code></pre>
<p>Fill down. Format column E as a number. Positive means the renewal is still ahead. Zero means today. Negative means the date is already past — update Next renewal after the charge posts, or mark the row Cancelled.</p>
<p>With the example rows, if today were <strong>2026-10-27</strong>, column E would read about:</p>
<pre><code class="language-plaintext">Row  Days left (example, as of 2026-10-27)
2    7
3    16
4    35
5    3
6    24
</code></pre>
<p>Those day counts are <strong>example data</strong> tied to the made-up renewal dates above. Your live sheet uses <code>TODAY()</code>, so the numbers move each morning.</p>
<h2>3. A plain-language status (column F)</h2>
<pre><code class="language-plaintext">F2: =IF(E2="","",IF(E2&lt;0,"Past due",IF(E2&lt;=7,"Due in 7 days",IF(E2&lt;=30,"Due in 30 days","Later"))))
</code></pre>
<p>Fill down. You get four labels without scanning every date.</p>
<h2>4. Conditional formatting</h2>
<p>Select <code>A2:F100</code> (or just E and F) and add two rules:</p>
<ul>
<li>Yellow fill when <code>=$E2&lt;=30</code> and <code>=$E2&gt;7</code> (or when status is "Due in 30 days").</li>
<li>Red fill when <code>=$E2&lt;=7</code> (covers both the 7-day window and past-due rows).</li>
</ul>
<p>In Google Sheets: <strong>Format → Conditional formatting → Custom formula is</strong>. In Excel the same idea lives under Conditional Formatting → New Rule → Use a formula.</p>
<p>You will see near renewals without opening every merchant email.</p>
<h2>5. Totals worth a glance</h2>
<p>Pick empty cells under the list:</p>
<pre><code class="language-plaintext">Due in 7 days (count):   =COUNTIF(F2:F6,"Due in 7 days")
Due in 30 days (count):  =COUNTIF(F2:F6,"Due in 30 days")
Past due (count):        =COUNTIF(F2:F6,"Past due")
</code></pre>
<p>Optional: sum amounts for rows due within 7 days (only useful if Amount is already a monthly-normalized figure, or if you only care about the next charge as billed):</p>
<pre><code class="language-plaintext">=SUMIF(F2:F6,"Due in 7 days",B2:B6)
</code></pre>
<p>If cycles differ (weekly vs yearly), normalize Amount to monthly in a separate column first — otherwise a yearly $79 and a weekly $12 are not comparable.</p>
<h2>6. Keep the list honest</h2>
<ul>
<li>Update <strong>Next renewal</strong> after each charge posts, or the countdown goes stale.</li>
<li>Annual plans often need a cancel several days before the renewal date; check the merchant page, not only this sheet.</li>
<li>Past-due does not always mean you were charged twice — sometimes the date just was not rolled forward.</li>
</ul>
<h2>Wrap-up</h2>
<p>Audit Lite answers what you pay. One Days left column and two formatting rules answer what is due soon. Build the countdown from this post in about 10 minutes on top of any clean list.</p>
<p>If you want a finished file that already normalizes six billing cycles, keeps room for 100 subscriptions, flags renewals (yellow at 30 days, red at 7) and rolls totals onto a dashboard, the <a href="https://ailstudio.netlify.app/products/subscription-tracker/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=dist48-qkwkix"><strong>Subscription Tracker</strong></a> is $1+ (pay what you want) for Excel and Google Sheets. Delete the sample rows and put your own in.</p>
<p>By Richard C · AIL Studio</p>
]]></content:encoded></item><item><title><![CDATA[How to calculate a trailing drawdown in Google Sheets for a prop firm challenge]]></title><description><![CDATA[By Richard C · AIL Studio Not financial advice. This is a spreadsheet tutorial for record-keeping. Prop firm rules change, and firms define trailing…]]></description><link>https://ailstudio.hashnode.dev/how-to-calculate-a-trailing-drawdown-in-google-sheets-for-a-prop-firm-challenge</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/how-to-calculate-a-trailing-drawdown-in-google-sheets-for-a-prop-firm-challenge</guid><category><![CDATA[#googlesheets]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[spreadsheets]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Tue, 27 Oct 2026 04:22:01 GMT</pubDate><content:encoded><![CDATA[<p>By Richard C · AIL Studio</p>
<blockquote>
<p><strong>Not financial advice.</strong> This is a spreadsheet tutorial for record-keeping. Prop firm rules change, and firms define trailing drawdown differently. Verify with your firm's current rules page before you buy or trade. Nothing here is a trading recommendation.</p>
</blockquote>
<p><a href="https://ailstudio.gumroad.com/l/cbjdeq?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=trailing-drawdown"><strong>Prop Firm Rules Cheat Sheet (Free, Oct 2026)</strong></a>: a free .xlsx that puts drawdown type, daily loss, profit target, minimum days, payout split and price for 10 prop firms side by side (checked Oct 2026 against each firm's own site).</p>
<p>A trailing drawdown is the rule that ends many prop firm accounts. Unlike a static drawdown, the floor can move up after you make money. Two accounts with the same starting balance and the same dollar limit can fail on different days, depending on whether the firm trails at end of day or intraday, and whether the floor locks at the starting balance.</p>
<p>This article builds only that check in Google Sheets: end-of-day trailing, intraday trailing, the lock, and the mistakes that hide a breach. For a full challenge tracker (daily loss, profit target, consistency and the rest), see <a href="https://dev.to/richardc_ailstudio/how-to-track-prop-firm-challenge-rules-in-google-sheets-daily-loss-max-drawdown-consistency-4c4n">How to track prop firm challenge rules in Google Sheets</a>.</p>
<h2>What "trailing" means (in plain words)</h2>
<p><strong>Static drawdown:</strong> the floor is fixed. On a $50,000 account with a $2,000 max drawdown, you fail at $48,000 no matter how high you climbed first.</p>
<p><strong>Trailing drawdown:</strong> the floor follows your highest balance upward. After you grow the account, you can fail above the original static floor.</p>
<p><strong>End-of-day (EOD) trailing:</strong> the peak (and usually the floor) updates from the balance at the close of the trading day. Intraday open profit that disappears before the close often does not raise the floor.</p>
<p><strong>Intraday trailing:</strong> the peak updates from equity while trades are open. A winner that reverses can raise the floor, then leave you too close to it.</p>
<p><strong>Lock at starting balance:</strong> many firms stop trailing once the floor reaches the starting balance. After that, the floor does not rise further. Some firms do not lock. Check your plan.</p>
<p>Examples from the Oct 2026 cheat sheet (still verify on the firm's site): end-of-day trailing appears on plans from Topstep, Take Profit Trader and My Funded Futures; intraday trailing appears on Apex's intraday-trail evaluations; static max loss appears on plans from FTMO and FundedNext. Those are illustrations of <em>types</em>, not a promise that any one plan still works that way today.</p>
<h2>1. Settings</h2>
<table>
<thead>
<tr>
<th>Cell</th>
<th>Label (column A)</th>
<th>Value (column B)</th>
</tr>
</thead>
<tbody><tr>
<td>B1</td>
<td>Starting balance</td>
<td>50000</td>
</tr>
<tr>
<td>B2</td>
<td>Max drawdown ($)</td>
<td>2000</td>
</tr>
</tbody></table>
<p>These are <strong>example data</strong> for a $50K-style account. Replace them with your firm's numbers. Whether the floor locks at the starting balance is handled in the floor formula in section 2, not in a separate settings cell.</p>
<p>Static floor for comparison:</p>
<pre><code class="language-plaintext">B4: =B1-B2      → 48000
</code></pre>
<h2>2. End-of-day trailing log</h2>
<p>Headers in row 9: <strong>Date, Daily P&amp;L, Running balance, Peak EOD balance, Trailing floor, Trailing check, Static check</strong> (A–G). Put <code>Example data (not real trades)</code> in A8.</p>
<p><strong>Example data</strong> (made-up numbers for illustration, not real trades):</p>
<table>
<thead>
<tr>
<th>Row</th>
<th>A: Date</th>
<th>B: Daily P&amp;L</th>
</tr>
</thead>
<tbody><tr>
<td>10</td>
<td>2026-10-01</td>
<td>2500</td>
</tr>
<tr>
<td>11</td>
<td>2026-10-02</td>
<td>-1500</td>
</tr>
<tr>
<td>12</td>
<td>2026-10-03</td>
<td>800</td>
</tr>
<tr>
<td>13</td>
<td>2026-10-06</td>
<td>-2100</td>
</tr>
</tbody></table>
<p>You only type columns A and B.</p>
<h3>Running balance (column C)</h3>
<pre><code class="language-plaintext">C10: =$B$1+B10
C11: =C10+B11      (fill down to C13)
</code></pre>
<h3>Peak EOD balance (column D)</h3>
<pre><code class="language-plaintext">D10: =MAX($B$1,C10)
D11: =MAX(D10,C11)      (fill down to D13)
</code></pre>
<h3>Trailing floor with lock (column E)</h3>
<pre><code class="language-plaintext">E10: =MIN(D10-$B$2,$B$1)      (fill down to E13)
</code></pre>
<p><code>MIN(..., $B$1)</code> stops the floor rising above the starting balance. If your firm does <strong>not</strong> lock, use:</p>
<pre><code class="language-plaintext">E10: =D10-$B$2      (fill down)
</code></pre>
<h3>Checks (columns F and G)</h3>
<pre><code class="language-plaintext">F10: =IF(C10&lt;=E10,"BREACH","OK")      (fill down)
G10: =IF(C10&lt;=$B$4,"BREACH","OK")     (fill down)
</code></pre>
<h2>Results with the example data</h2>
<table>
<thead>
<tr>
<th>Row</th>
<th>Balance</th>
<th>Peak EOD</th>
<th>Trailing floor (locked)</th>
<th>Trailing</th>
<th>Static</th>
</tr>
</thead>
<tbody><tr>
<td>10</td>
<td>52500</td>
<td>52500</td>
<td>50000</td>
<td>OK</td>
<td>OK</td>
</tr>
<tr>
<td>11</td>
<td>51000</td>
<td>52500</td>
<td>50000</td>
<td>OK</td>
<td>OK</td>
</tr>
<tr>
<td>12</td>
<td>51800</td>
<td>52500</td>
<td>50000</td>
<td>OK</td>
<td>OK</td>
</tr>
<tr>
<td>13</td>
<td>49700</td>
<td>52500</td>
<td>50000</td>
<td><strong>BREACH</strong></td>
<td>OK</td>
</tr>
</tbody></table>
<p>I checked these formulas against the example table in Python (same arithmetic Google Sheets uses for <code>MAX</code>, <code>MIN</code> and running sums). Row 13 is the point: the account is still above the static floor of 48000, but the trailing floor has already locked at 50000 after the Day 1 gain, so −2100 fails the trailing rule.</p>
<p>Once a row shows <strong>BREACH</strong>, the firm has usually failed the account. Later rows that look OK do not undo that.</p>
<p>Without the lock, the same peak of 52500 would set a floor of 50500, and row 13 would still breach (49700 ≤ 50500). With a smaller early gain, lock vs no-lock can disagree, so match your firm's wording.</p>
<h2>3. Intraday trailing (one day of equity snapshots)</h2>
<p>EOD formulas cannot see open profit. To approximate an intraday trail, log equity during the day, including open P&amp;L.</p>
<p>Headers in row 20: <strong>Time, Equity (incl. open P&amp;L), Peak equity, Trailing floor, Check</strong> (A–E). Label A19 <code>Example data (not real trades)</code>.</p>
<p><strong>Example data</strong> for a single day starting at 50000 with a $2,000 trail and a lock at the starting balance:</p>
<table>
<thead>
<tr>
<th>Row</th>
<th>A: Time</th>
<th>B: Equity</th>
</tr>
</thead>
<tbody><tr>
<td>21</td>
<td>09:35</td>
<td>50000</td>
</tr>
<tr>
<td>22</td>
<td>10:10</td>
<td>52200</td>
</tr>
<tr>
<td>23</td>
<td>11:40</td>
<td>49950</td>
</tr>
<tr>
<td>24</td>
<td>15:50</td>
<td>50800</td>
</tr>
</tbody></table>
<pre><code class="language-plaintext">C21: =MAX($B$1,B21)
C22: =MAX(C21,B22)           (fill down)
D21: =MIN(C21-$B$2,$B$1)     (fill down; drop $B$1 if no lock)
E21: =IF(B21&lt;=D21,"BREACH","OK")  (fill down)
</code></pre>
<table>
<thead>
<tr>
<th>Time</th>
<th>Equity</th>
<th>Peak</th>
<th>Floor</th>
<th>Check</th>
</tr>
</thead>
<tbody><tr>
<td>09:35</td>
<td>50000</td>
<td>50000</td>
<td>48000</td>
<td>OK</td>
</tr>
<tr>
<td>10:10</td>
<td>52200</td>
<td>52200</td>
<td>50000</td>
<td>OK</td>
</tr>
<tr>
<td>11:40</td>
<td>49950</td>
<td>52200</td>
<td>50000</td>
<td><strong>BREACH</strong></td>
</tr>
<tr>
<td>15:50</td>
<td>50800</td>
<td>52200</td>
<td>50000</td>
<td>OK</td>
</tr>
</tbody></table>
<p>Same arithmetic check as above. At 11:40 the account is already failed under an intraday trail. The 15:50 close of 50800 would look fine under an <strong>EOD-only</strong> rule for that day (peak 50800, locked floor 48800, close OK). That gap is why mixing up EOD and intraday is expensive.</p>
<p>A sheet like this is a record. The firm's platform is the official clock and the official equity.</p>
<h2>4. Common mistakes</h2>
<ol>
<li><strong>Using only the close for an intraday firm.</strong> Your EOD log can show OK on a day the live platform already failed you.</li>
<li><strong>Forgetting the lock.</strong> If the firm locks at the starting balance and you keep trailing above it, your sheet's floor is too high (stricter than the firm) or, if you never trail at all, too low (looser than the firm).</li>
<li><strong>Comparing to starting balance instead of the floor.</strong> After a winning streak, "I'm still above 50K" is not the trailing test.</li>
<li><strong>Logging deposits, resets or payouts in the same balance column.</strong> Trailing peaks are about trading equity under the plan rules. Keep admin adjustments in a separate column so they do not inflate the peak by accident.</li>
<li><strong>Treating the sheet as live risk control.</strong> Daily and intraday limits on many platforms include open trades and update faster than a manual sheet. Use the sheet to understand the rule and to review after the close.</li>
</ol>
<h2>5. Conditional formatting</h2>
<p>Select the check columns, then <strong>Format → Conditional formatting → Text is exactly</strong> <code>BREACH</code>, with a red fill. That makes the first failing day hard to miss when you scroll.</p>
<h2>When to use a ready-made sheet</h2>
<p>Building the formulas once is useful for learning. If you also want a trade log with automatic P&amp;L, win rate, R-multiple, review tabs and a Prop Firm Tracker in one file, <a href="https://ailstudio.gumroad.com/l/qsnmk?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=trailing-drawdown"><strong>Trader's Edge Journal ($14)</strong></a> is a Google Sheets / Excel template from AIL Studio. You get an .xlsx with 15 sample trades and a short PDF quick-start. It is a record-keeping tool: it does not tell you what to trade and does not guarantee results.</p>
<p>Related reading:</p>
<ul>
<li><a href="https://dev.to/richardc_ailstudio/how-to-track-prop-firm-challenge-rules-in-google-sheets-daily-loss-max-drawdown-consistency-4c4n">How to track prop firm challenge rules in Google Sheets (daily loss, max drawdown, consistency)</a></li>
<li><a href="https://dev.to/richardc_ailstudio/prop-firm-rules-compared-oct-2026-2g4f">Prop firm rules compared (Oct 2026)</a></li>
</ul>
<p><em>Not financial advice. Rules change, verify with the firm before buying.</em></p>
<p>By Richard C · AIL Studio</p>
]]></content:encoded></item><item><title><![CDATA[How to catch wrong-parent Contacts after a Salesforce migration]]></title><description><![CDATA[By Richard C · AIL Studio · 7x certified Salesforce professional A Contact can keep its name and email after a Salesforce migration and still sit under the…]]></description><link>https://ailstudio.hashnode.dev/how-to-catch-wrong-parent-contacts-after-a-salesforce-migration</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/how-to-catch-wrong-parent-contacts-after-a-salesforce-migration</guid><category><![CDATA[Salesforce]]></category><category><![CDATA[data migration]]></category><category><![CDATA[crm]]></category><category><![CDATA[data cleaning ]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Tue, 27 Oct 2026 02:27:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adf221c756725187a8c3778/3b65088e-aaf2-4af2-a75f-5fc3e7974dab.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Richard C · AIL Studio · 7x certified Salesforce professional</p>
<p>A Contact can keep its name and email after a Salesforce migration and still sit under the wrong Account. Free CSV compare tools miss that case, because the field values still match — you have to check the parent relationship.</p>
<p>This post walks through that check on a tiny example, first with pandas and then with spreadsheet formulas you can use in Google Sheets.</p>
<h2>Why you can't just compare AccountId</h2>
<p>Record IDs usually change between the source and target org. A Contact's <code>AccountId</code> of <code>SA-01</code> in the source might legitimately become <code>TA-91</code> in the target. So "the IDs are different" tells you nothing.</p>
<p>What you need is a <strong>stable key</strong>: a value that identifies the same record in both exports, such as an external ID field. The check is then:</p>
<ol>
<li>Source Contact → its source AccountId → that Account's <strong>stable key</strong>.</li>
<li>Target Contact (matched by the Contact's stable key) → its target AccountId → that Account's <strong>stable key</strong>.</li>
<li>If the two Account keys differ, the Contact has the <strong>wrong parent</strong>.</li>
</ol>
<h2>The example data</h2>
<p><strong>Example data:</strong> four small, made-up CSVs. All records are invented, and the email uses the reserved <code>example.invalid</code> domain.</p>
<p><code>source-accounts.csv</code></p>
<table>
<thead>
<tr>
<th>ExternalKey</th>
<th>Id</th>
<th>Name</th>
<th>Industry</th>
</tr>
</thead>
<tbody><tr>
<td>AC-001</td>
<td>SA-01</td>
<td>Aster, Labs</td>
<td>Science</td>
</tr>
<tr>
<td>AC-002</td>
<td>SA-02</td>
<td>Blue Orchard</td>
<td>Retail</td>
</tr>
</tbody></table>
<p><code>target-accounts.csv</code></p>
<table>
<thead>
<tr>
<th>ExternalKey</th>
<th>Id</th>
<th>Name</th>
<th>Industry</th>
</tr>
</thead>
<tbody><tr>
<td>AC-001</td>
<td>TA-91</td>
<td>Aster, Labs</td>
<td>Science</td>
</tr>
<tr>
<td>AC-002</td>
<td>TA-92</td>
<td>Blue Orchard</td>
<td>Retail</td>
</tr>
</tbody></table>
<p><code>source-contacts.csv</code></p>
<table>
<thead>
<tr>
<th>ExternalKey</th>
<th>Id</th>
<th>AccountId</th>
<th>LastName</th>
<th>Email</th>
</tr>
</thead>
<tbody><tr>
<td>CT-001</td>
<td>SC-01</td>
<td>SA-01</td>
<td>Renée, Example</td>
<td><a href="mailto:renee@example.invalid">renee@example.invalid</a></td>
</tr>
</tbody></table>
<p><code>target-contacts.csv</code></p>
<table>
<thead>
<tr>
<th>ExternalKey</th>
<th>Id</th>
<th>AccountId</th>
<th>LastName</th>
<th>Email</th>
</tr>
</thead>
<tbody><tr>
<td>CT-001</td>
<td>TC-91</td>
<td>TA-92</td>
<td>Renée, Example</td>
<td><a href="mailto:renee@example.invalid">renee@example.invalid</a></td>
</tr>
</tbody></table>
<p>Look at it by hand first. Source CT-001 points to SA-01, which is AC-001. In the target, AC-001 is TA-91, so that's where CT-001 <em>should</em> point. It actually points to TA-92, which is AC-002. Same name, same email, wrong Account.</p>
<h2>Check 1: pandas</h2>
<pre><code class="language-python">import pandas as pd

def load(name):
    # keep IDs as text; the files have a UTF-8 BOM
    return pd.read_csv(f"{name}.csv", dtype=str, encoding="utf-8-sig", keep_default_na=False)

sa, ta = load("source-accounts"), load("target-accounts")
sc, tc = load("source-contacts"), load("target-contacts")

# lookups only work if keys and IDs are unique on each side
for df in (sa, ta):
    assert df["Id"].is_unique and df["ExternalKey"].is_unique

src_key = dict(zip(sa["Id"], sa["ExternalKey"]))   # SA-01 -&gt; AC-001
tgt_key = dict(zip(ta["Id"], ta["ExternalKey"]))   # TA-92 -&gt; AC-002
tgt_id  = dict(zip(ta["ExternalKey"], ta["Id"]))   # AC-001 -&gt; TA-91

m = sc.merge(tc, on="ExternalKey", how="left", suffixes=("_src", "_tgt"))
m["expected_parent_key"] = m["AccountId_src"].map(src_key)
m["actual_parent_key"]   = m["AccountId_tgt"].map(tgt_key)
m["expected_target_id"]  = m["expected_parent_key"].map(tgt_id)
m["result"] = (m["expected_parent_key"] == m["actual_parent_key"]).map(
    {True: "PARENT_MATCH", False: "WRONG_PARENT"})

print(m[["ExternalKey", "expected_target_id", "AccountId_tgt", "result"]])
print("wrong parents:", (m["result"] == "WRONG_PARENT").sum())
</code></pre>
<p>Output on the example data:</p>
<pre><code class="language-plaintext">  ExternalKey expected_target_id AccountId_tgt        result
0      CT-001              TA-91         TA-92  WRONG_PARENT
wrong parents: 1
</code></pre>
<p>So there is <strong>1 wrong-parent Contact</strong> (CT-001: expected TA-91, actual TA-92), and <strong>0 differences</strong> in LastName or Email. On real data, also check for Contacts whose parent doesn't resolve at all (a <code>NaN</code> key here). Report those as unresolved, not as matches.</p>
<h2>Check 2: Google Sheets formulas</h2>
<p>Import each CSV into its own tab named <strong>SourceAccounts</strong>, <strong>TargetAccounts</strong>, <strong>SourceContacts</strong> and <strong>TargetContacts</strong>, with headers in row 1. Keep IDs as text. "Aster, Labs" and "Renée, Example" each belong in one cell.</p>
<p>First, confirm the lookups are safe. Each Account <code>Id</code> and <code>ExternalKey</code> must be unique:</p>
<pre><code class="language-plaintext">=SUMPRODUCT((COUNTIF(SourceAccounts!$B$2:$B$3,SourceAccounts!$B$2:$B$3)=1)*1)   → 2 (both IDs unique)
=SUMPRODUCT((COUNTIF(TargetAccounts!$A$2:$A$3,TargetAccounts!$A$2:$A$3)=1)*1)   → 2 (both keys unique)
</code></pre>
<p>Then add a <strong>Check</strong> tab. Put the labels in row 1 and these formulas in row 2:</p>
<pre><code class="language-plaintext">A2  Contact key:                =SourceContacts!A2
B2  Source Account ID:          =SourceContacts!C2
C2  Source Account key:         =INDEX(SourceAccounts!$A$2:$A$3,MATCH(B2,SourceAccounts!$B$2:$B$3,0))
D2  Expected target Account ID: =INDEX(TargetAccounts!$B$2:$B$3,MATCH(C2,TargetAccounts!$A$2:$A$3,0))
E2  Actual target Account ID:   =INDEX(TargetContacts!$C$2:$C$3,MATCH(A2,TargetContacts!$A$2:$A$3,0))
F2  Actual target Account key:  =INDEX(TargetAccounts!$A$2:$A$3,MATCH(E2,TargetAccounts!$B$2:$B$3,0))
G2  Relationship check:         =IF(EXACT(C2,F2),"PARENT_MATCH","WRONG_PARENT")
H2  Source LastName:            =SourceContacts!D2
I2  Target LastName:            =INDEX(TargetContacts!$D$2:$D$3,MATCH(A2,TargetContacts!$A$2:$A$3,0))
J2  LastName check:             =IF(EXACT(H2,I2),"MATCH","DIFFERENCE")
K2  Source Email:               =SourceContacts!E2
L2  Target Email:               =INDEX(TargetContacts!$E$2:$E$3,MATCH(A2,TargetContacts!$A$2:$A$3,0))
M2  Email check:                =IF(EXACT(K2,L2),"MATCH","DIFFERENCE")
B4  Wrong-parent count:         =COUNTIF(G2:G2,"WRONG_PARENT")
</code></pre>
<p>Expected results:</p>
<table>
<thead>
<tr>
<th>Cell</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>A2</td>
<td>CT-001</td>
</tr>
<tr>
<td>B2</td>
<td>SA-01</td>
</tr>
<tr>
<td>C2</td>
<td>AC-001</td>
</tr>
<tr>
<td>D2</td>
<td>TA-91</td>
</tr>
<tr>
<td>E2</td>
<td>TA-92</td>
</tr>
<tr>
<td>F2</td>
<td>AC-002</td>
</tr>
<tr>
<td>G2</td>
<td><strong>WRONG_PARENT</strong></td>
</tr>
<tr>
<td>J2</td>
<td>MATCH</td>
</tr>
<tr>
<td>M2</td>
<td>MATCH</td>
</tr>
<tr>
<td>B4</td>
<td>1</td>
</tr>
</tbody></table>
<p>I recalculated this exact workbook and got these values, matching the pandas result. As a sanity check, change <code>TargetContacts!C2</code> to <code>TA-91</code>. F2 becomes AC-001 and G2 becomes PARENT_MATCH.</p>
<p>A few notes for real exports:</p>
<ul>
<li><strong>MATCH is case-insensitive</strong>, so <code>EXACT</code> does the final comparisons. Keep keys consistently cased.</li>
<li><strong>Extend ranges deliberately.</strong> For more Contacts, fill row 2 down and widen every range (<code>$A$2:$A$5000</code> and so on), then change B4 to cover the whole G column range.</li>
<li><strong>A lookup error (#N/A) means unresolved.</strong> Investigate it. Don't treat it as a match or hide it with IFERROR.</li>
<li>This checks one relationship on the files you load. It doesn't certify a whole migration.</li>
</ul>
<h2>If you'd rather skip the setup</h2>
<p>I packaged these four example CSVs, the README and the exact Check-tab formulas into a free guide ($0). It includes the expected output, so you can confirm your sheet: <a href="https://ailstudio.gumroad.com/l/rjvcjs?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=recordbridge48">RecordBridge Free Guide: CSV Example</a>.</p>
<p>For full exports, <a href="https://ailstudio.gumroad.com/l/ufgfjf?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=recordbridge48">RecordBridge</a> (US$49, one-time) runs the same check across Accounts, Contacts and Opportunities.</p>
<p>More Salesforce tools from AIL Studio: <a href="https://ailstudio.gumroad.com/?sort=page_layout&amp;tags=salesforce&amp;utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=salesforce-set">browse the Salesforce set</a></p>
<p>Next step: if you have SOQL access, here is how to run the same audit straight from the org: <a href="https://ailstudio.hashnode.dev/how-to-audit-contact-account-parents-after-a-salesforce-migration-with-soql">Audit Contact parents with SOQL</a></p>
<p>RecordBridge by AIL Studio is not affiliated with Salesforce.</p>
<p>Next step: <a href="https://ailstudio.hashnode.dev/how-to-find-missing-and-duplicate-records-after-a-salesforce-migration-match-on-an-external-key?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=wrongparent-next-missing-records">find missing and duplicate records by matching on an external key</a>.</p>
]]></content:encoded></item><item><title><![CDATA[5 ChatGPT Prompts for Small Business Owners That Do Real Work]]></title><description><![CDATA[Copy-paste prompts for clarifying your offer, chasing quotes, answering bad reviews, planning your week, and checking your prices]]></description><link>https://ailstudio.hashnode.dev/5-chatgpt-prompts-for-small-business-owners-that-do-real-work</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/5-chatgpt-prompts-for-small-business-owners-that-do-real-work</guid><category><![CDATA[chatgpt]]></category><category><![CDATA[AI]]></category><category><![CDATA[Small business]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Prompt Engineering]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Mon, 26 Oct 2026 09:55:15 GMT</pubDate><content:encoded><![CDATA[<p>Most "ChatGPT prompts for small business" lists are one-liners like "write a marketing plan for my business." You get back something generic, because the AI knows nothing about you.</p>
<p>The prompts below work better for two reasons. They ask for a specific output in a specific format, and they leave clear [BRACKETS] for your own details. They work in ChatGPT, Claude, Gemini, Grok, or any other AI chat tool.</p>
<p>Replace everything in [BRACKETS]. The more context you give, the better the answer. If the first answer misses, follow up with "Make it shorter," "Make it sound more like me: [paste a sample of your writing]," or "Give me 3 alternatives."</p>
<h2>1. Clarify your offer</h2>
<pre><code class="language-text">I run [BUSINESS] serving [CUSTOMER TYPE] in [LOCATION]. Ask me 8 questions, one at a time, to uncover what makes my business different, then write a one-sentence value proposition and three short taglines based on my answers.
</code></pre>
<p><strong>When to use it:</strong> Before you write a website, a Google Business Profile, or an ad. If you can't say in one sentence why someone should pick you, every other piece of marketing is harder.</p>
<p><strong>How to tweak it:</strong> The "one at a time" part matters. It makes the AI interview you instead of guessing. If the questions feel too broad, add "Focus on why customers choose us over [COMPETITOR TYPE]." Answer honestly and in detail. Your answers are the raw material, so short answers get you generic taglines.</p>
<h2>2. Follow up on a quote that went quiet</h2>
<pre><code class="language-text">I sent a quote for [JOB] to [CLIENT] [X] days ago with no reply. Write a polite follow-up that offers to answer questions, restates the main benefit, and makes it easy to say no. Then write a second, final follow-up for a week later.
</code></pre>
<p><strong>When to use it:</strong> A few days after sending a quote you haven't heard back on. Plenty of owners never follow up because it feels pushy, and that leaves work on the table.</p>
<p><strong>How to tweak it:</strong> "Makes it easy to say no" is deliberate. A reply of "no thanks" is still better than silence, because then you can stop waiting. Add details about the job, such as the client's deadline or the problem they described, so the follow-up sounds like you remember them. If you usually text clients, ask for "a text message under 300 characters" instead.</p>
<h2>3. Reply to a negative review</h2>
<pre><code class="language-text">Here's a negative review: [PASTE]. Write a public reply that thanks them, acknowledges the specific issue without making excuses, explains what we're doing about it, and invites them to contact [CONTACT] offline. Under 100 words.
</code></pre>
<p><strong>When to use it:</strong> When a bad review comes in and your first draft of a reply is defensive. Keep in mind that the reply is written for future customers reading it as much as for the reviewer.</p>
<p><strong>How to tweak it:</strong> Tell the AI what actually happened and what you've really changed, for example "We've since added a second person on Saturdays." Don't let it promise fixes you haven't made. If the review is unfair or factually wrong, add "Politely correct this factual error: [FACT]" and keep the tone calm.</p>
<h2>4. Pick this week's priorities</h2>
<pre><code class="language-text">Here's everything on my plate this week: [LIST]. Help me pick the 3 that matter most for revenue or customers, identify what can be delegated or dropped, and lay out a realistic day-by-day plan.
</code></pre>
<p><strong>When to use it:</strong> Monday morning, or any time your to-do list is longer than the week.</p>
<p><strong>How to tweak it:</strong> Paste the messy list exactly as it is, with no cleanup needed. Add your constraints: "I'm on job sites Tuesday and Wednesday," "I only have 2 hours a day for admin." If the plan still looks too full, follow up with "Cut this to what's realistic in [X] hours total."</p>
<h2>5. Check your pricing</h2>
<pre><code class="language-text">I sell [PRODUCT/SERVICE] for [PRICE]. My costs are [COSTS] and I spend about [HOURS] per job. Calculate my effective margin and hourly rate, and suggest 3 pricing options (including a tiered package) with pros and cons.
</code></pre>
<p><strong>When to use it:</strong> When you're busy but cash still feels tight, or before you raise prices. Seeing your effective hourly rate written out can be eye-opening.</p>
<p><strong>How to tweak it:</strong> Include all of your costs, including materials, travel, software, and the unpaid time spent quoting and doing admin. Check the math yourself, because AI tools can make arithmetic mistakes. If you want the tiers to make sense to customers, add "Describe each tier the way a customer would see it."</p>
<h2>A couple of ground rules</h2>
<ul>
<li>Don't paste passwords, full card numbers, or sensitive customer data into AI tools.</li>
<li>Read everything before it goes to a customer. Treat the AI's output as a first draft, not a finished one.</li>
</ul>
<hr />
<p>These five come from a larger set of 50 prompts covering marketing, email, customer service, and operations. If you'd like the rest, the full pack is here: <a href="https://ailstudio.gumroad.com/l/ineoz?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=5-chatgpt-prompts-for-small-business-owners-that-do-real-work">50 AI Prompts for Busy Small Business Owners</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Guessing What Your Subscriptions Cost: A 20-Minute Spreadsheet Audit]]></title><description><![CDATA[Use one lookup table to turn weekly, monthly, and yearly charges into numbers you can compare, then add renewal alerts]]></description><link>https://ailstudio.hashnode.dev/stop-guessing-what-your-subscriptions-cost-a-20-minute-spreadsheet-audit</link><guid isPermaLink="true">https://ailstudio.hashnode.dev/stop-guessing-what-your-subscriptions-cost-a-20-minute-spreadsheet-audit</guid><category><![CDATA[Productivity]]></category><category><![CDATA[spreadsheets]]></category><category><![CDATA[google sheets]]></category><category><![CDATA[excel]]></category><category><![CDATA[personal finance]]></category><dc:creator><![CDATA[Richard C]]></dc:creator><pubDate>Mon, 26 Oct 2026 09:50:54 GMT</pubDate><content:encoded><![CDATA[<p>Most of us can say roughly what we pay for Netflix. Very few can give the total for everything: the annual domain renewal, the $9/week meal kit, the quarterly cloud backup, the free trial that quietly became paid. The trouble isn't the size of any one charge. It's that they all bill on different schedules, so you can't add them up in your head.</p>
<p><a href="https://ailstudio.gumroad.com/l/ipnphp?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=stop-guessing-what-your-subscriptions-cost-a-20-minute-spreadsheet-audit">Free download: Subscription Audit Lite</a></p>
<blockquote>
<p><strong>Not sure what your subscriptions really cost?</strong> Start with Subscription Audit Lite, a free 30-row spreadsheet that totals every recurring charge and shows what cancelling would save: <a href="https://ailstudio.gumroad.com/l/ipnphp?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=stop-guessing-what-your-subscriptions-cost-a-20-minute-spreadsheet-audit">get Audit Lite (free)</a>.</p>
</blockquote>
<p>This post walks through a small spreadsheet that fixes that. It works the same way in Google Sheets and Excel, and it doesn't need any scripts.</p>
<h2>Step 1: List everything (the hard part)</h2>
<p>Spend 10 minutes going through:</p>
<ul>
<li>Bank and card statements for the last 12 months (search for "recurring" or scan for repeat amounts)</li>
<li>App Store / Google Play → Subscriptions</li>
<li>Your email, searching for <code>receipt</code>, <code>renewal</code>, <code>your plan</code>, <code>trial ending</code></li>
<li>PayPal → Automatic payments</li>
</ul>
<p>Every hit gets a row.</p>
<h2>Step 2: The sheet layout</h2>
<p>A <code>Subscriptions</code> tab with these columns:</p>
<p><code>Service | Category | Cost | Billing cycle | Monthly | Yearly | Next renewal | Days left | Status</code></p>
<p>You only type into Cost, Billing cycle, Next renewal and Status. Everything else is calculated.</p>
<h2>Step 3: Normalize billing cycles with a lookup, not nested IFs</h2>
<p>Make a <code>Lists</code> tab:</p>
<table>
<thead>
<tr>
<th>Cycle</th>
<th>Payments/year</th>
</tr>
</thead>
<tbody><tr>
<td>Weekly</td>
<td>52</td>
</tr>
<tr>
<td>Monthly</td>
<td>12</td>
</tr>
<tr>
<td>Quarterly</td>
<td>4</td>
</tr>
<tr>
<td>Every 6 months</td>
<td>2</td>
</tr>
<tr>
<td>Yearly</td>
<td>1</td>
</tr>
</tbody></table>
<p>Then the Monthly column (assuming Cost in C, Cycle in D, Status in I):</p>
<pre><code class="language-plaintext">=IF(C2="","",IF(I2="Cancelled",0,
  C2*IFERROR(VLOOKUP(D2,Lists!$A$2:$B$6,2,FALSE),0)/12))
</code></pre>
<p>Every cycle goes through the same rule: <strong>cost × payments per year ÷ 12</strong>. If you add a "Every 2 years" cycle later, it's one new row in <code>Lists</code> (value <code>0.5</code>) and no formulas change. Put the cycle column behind a dropdown that reads from <code>Lists!A2:A6</code>, so a typo can't break the lookup.</p>
<p>Yearly is just <code>=IF(E2="","",E2*12)</code>. This is the column people find uncomfortable to read, because $14/month looks a lot heavier as $168/year.</p>
<h2>Step 4: Renewal countdown + color alerts</h2>
<pre><code class="language-plaintext">=IF(G2="","",G2-TODAY())
</code></pre>
<p>Then add two conditional-formatting rules on the Days-left column:</p>
<ul>
<li>Red: <code>=AND($H2&gt;=0,$H2&lt;=7)</code></li>
<li>Yellow: <code>=AND($H2&gt;7,$H2&lt;=30)</code></li>
</ul>
<p>Now you'll see an annual renewal coming early enough to cancel it.</p>
<h2>Step 5: The "Review" status</h2>
<p>Status options: <code>Active</code>, <code>Trial</code>, <code>Review</code>, <code>Cancelled</code>.</p>
<p><strong>Review</strong> is the useful one. It means "I'm not sure I still want this." On a summary tab:</p>
<pre><code class="language-plaintext">Monthly total:      =SUM(Subscriptions!E2:E101)
Yearly total:       =SUM(Subscriptions!F2:F101)
Up for review/mo:   =SUMIF(Subscriptions!I2:I101,"Review",Subscriptions!E2:E101)
Renewing ≤30 days:  =COUNTIFS(Subscriptions!H2:H101,"&gt;=0",Subscriptions!H2:H101,"&lt;=30")
</code></pre>
<p>The "Up for review" number shows how much you'd save each month by cancelling the things you're unsure about. Seeing that number tends to get people to actually cancel.</p>
<p>For a spend-by-category breakdown, use <code>=SUMIF(Subscriptions!B2:B101,K2,Subscriptions!E2:E101)</code> next to a list of categories, then turn it into a pie chart.</p>
<h2>Step 6: Keep it alive</h2>
<ul>
<li>After each renewal, move the date forward. The <code>&gt;=0</code> check keeps past dates out of the "upcoming" counts.</li>
<li>Don't delete cancelled rows. They count as $0 and show what you've already cut.</li>
<li>Do a 5-minute check once a quarter.</li>
</ul>
<h2>Summary</h2>
<p>The whole setup is one lookup table, three formula columns, two formatting rules and a summary tab. You can build it from this post in about 20 minutes.</p>
<p>If you'd rather not build it by hand, I made a finished template built on the same formulas, with the billing-cycle normalizer, days-left column, yellow/red renewal warnings and a dashboard, for Excel and Google Sheets. It's $1+ (pay what you want): <a href="https://ailstudio.netlify.app/products/subscription-tracker/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=stop-guessing-what-your-subscriptions-cost-a-20-minute-spreadsheet-audit">get the Subscription Tracker</a>.</p>
<p><a href="https://ailstudio.gumroad.com/l/ipnphp?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=stop-guessing-what-your-subscriptions-cost-a-20-minute-spreadsheet-audit">Free download: Subscription Audit Lite</a></p>
]]></content:encoded></item></channel></rss>