<?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>Sat, 31 Oct 2026 08:32:12 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[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 <strong><a href="https://ailstudio.gumroad.com/l/ipnphp?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=dist48-qkwkix">Subscription Audit Lite ($0+)</a></strong>. 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>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>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>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>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>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>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>=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 <strong><a href="https://ailstudio.gumroad.com/l/qkwkix?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=dist48-qkwkix">Subscription Tracker</a></strong> 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>RecordBridge by AIL Studio is not affiliated with Salesforce.</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.gumroad.com/l/qkwkix?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>