Free Claude skill · Metrics and investor updates · v1.0
Investor update writing skill for Claude
Give Claude this period's numbers, last period's and your notes, and it drafts an investor update in a fixed order: a one-line TL;DR, a KPI table, highlights, lowlights, asks, cash and runway, and team. A script works out every change, net burn, runway and burn multiple, and lists what got worse or missed plan, so the bad news goes in the update and every figure can be traced back to your numbers.
By the StartupReader editorial desk · Updated 2026-11-08 · MIT licence · Works on claude.ai, Claude Code and the Claude API
Investor update writing
- sections in every update
- 7
- net burn measures compared
- 2
- network calls
- 0
- MIT licence
- Free
What it does
- Turns this period's and last period's numbers into a KPI table with the change and % change, percentage points for rates, and the gap to plan.
- Lists every metric that got worse or missed plan, so none of them is left out of the lowlights.
- Works out net burn from the change in cash and from expenses minus revenue, says when they disagree, and gives runway and burn multiple.
- Drafts the update in a fixed order: TL;DR, numbers, highlights, lowlights, asks, cash and runway, team.
- Keeps the tone numbers-first and free of hype, and checks every figure against the script output and your notes.
- Takes MRR, churn and retention straight from the SaaS metrics analysis skill when you have a customer-level MRR export.
Ask Claude things like
- “Write our October investor update from these numbers and notes.”
- “Here's last month's update and this month's figures. Same format, please.”
- “Draft a quarterly update for our angels. We missed our revenue plan: how do we say it?”
- “What's our runway and burn multiple for this month's update?”
What you give it
- Each KPI's value this period and last period, and your plan if you have one.
- Your last update, if there is one, so the KPIs, their order and their definitions stay the same.
- Cash at the end of each period and any money raised, and ideally revenue and operating expenses for the period.
- Notes on wins, problems, hires and departures, and what you need from investors.
What you get back
- A ready-to-edit update with a subject line, TL;DR, KPI table, highlights, lowlights, asks, cash and runway, and team.
- Net burn, runway and burn multiple from the script, with the method stated.
- A definitions line for every KPI, so next period's update compares like with like.
How changes are shown
Money and counts get an absolute change and a percentage change: MRR down $51, or 1.0%. Rates such as churn, retention and gross margin get a change in percentage points instead. Logo churn going from 2% to 3% is up 1 percentage point; calling it up 50% is arithmetically true and misleading, and investors comparing updates notice when the basis shifts from one month to the next.
The percentage change is left blank where it means nothing: for rates, and where last period's figure was zero or negative, such as a loss that narrowed. When you give a plan, the table adds the plan and the gap to it in the same way.
- Money and counts: change and % change, e.g. -$51 and -1.0%.
- Percentages: change in percentage points, e.g. -7.0 pp.
- Ratios and months: change and % change, e.g. +0.3x or -1.3 mo.
Net burn, runway and burn multiple
Net burn is the cash the company loses in a month, leaving out money raised. The script works it out two ways when it can: from the change in the bank balance, and from operating expenses minus revenue, the formula in StartupReader's burn rate and runway calculator. The two often differ because of when customers pay, annual prepayments or one-off bills, so it shows both and tells Claude to say which one the update uses.
Runway is cash divided by monthly net burn with burn held flat, the calculator's simple case. Burn multiple is net burn over the period divided by net new ARR (MRR times 12 if you report MRR). On one month it is noisy, so the script says so, and when ARR did not grow it reports the ratio as not meaningful rather than printing a negative number.
Why bad news goes near the top
Investors who find a missed plan or a lost customer in the numbers before the founder mentions it tend to read the rest of the update more sceptically. So the skill feeds the script's "Worse than last period" and "Behind plan" lines into the lowlights, and a key metric that fell or missed plan also goes in the TL;DR. Each lowlight says what happened, why if the founder knows, and what is being done.
The other rules serve the same goal. The KPIs, their order and their definitions stay the same from update to update, and a change is stated rather than slipped in. Every figure comes from the script or your notes. Asks name a type of person and an action, because "intros welcome" is easy to ignore and a specific request is easy to act on.
Worked example
Acme Desk, the made-up helpdesk startup from the SaaS metrics example, sends its angels a monthly update. October's MRR, customers, churn and retention come from that skill. The plan was $5,300 of MRR and 27 customers. Cash fell from $96,000 to $90,000, on $5,000 of revenue and $11,000 of expenses. The founder's notes: two new customers; a $299-a-month customer cancelled after moving support in-house; check-in calls start for every account paying $199 or more; a part-time support engineer starts on 3 November; they want intros to heads of support at 50 to 200 person online retailers.
The founder asks
Draft our October investor update. The numbers are in the metrics JSON. Notes: two new customers, lost a $299/month account that moved support in-house, starting check-ins with every account on $199+, part-time support engineer starts 3 Nov. Ask: intros to heads of support at 50-200 person online retailers.
Claude reads the skill, then runs its script with the figures from the documents. These are the script's real outputs:
$ python scripts/update_numbers.py assets/example-metrics.jsonAcme Desk: October 2026 vs September 2026 (1 month) | Metric | October 2026 | September 2026 | Change | % change | Plan | vs plan ||---|---|---|---|---|---|---|| MRR | $4,974 | $5,025 | -$51 | -1.0% | $5,300 | -$326 (-6.2%) || Paying customers | 26 | 25 | +1 | +4.0% | 27 | -1 (-3.7%) || Logo churn (monthly) | 4.0% | 4.0% | 0.0 pp | | | || Net revenue retention (monthly) | 94.0% | 101.0% | -7.0 pp | | | || Gross margin | 80.0% | 79.0% | +1.0 pp | | | | Worse than last period: MRR -$51 (-1.0%); Net revenue retention (monthly) -7.0 ppBehind plan: MRR -$326 (-6.2%); Paying customers -1 (-3.7%) Cash and runway Cash at the end of October 2026: $90,000 (September 2026: $96,000; raised in the period: $0) Net burn from the change in cash: $6,000 a month Net burn from expenses minus revenue: $11,000 - $5,000 = $6,000 a month Runway = $90,000 / $6,000 = 15.0 months (net burn from expenses minus revenue, held flat) Burn multiple: not meaningful, ARR (MRR x 12) did not grow this period Percentage metrics change in percentage points (pp). % change is blank where it is not meaningful (percentages, or a zero or negative base).
The answer, in the skill's format (abridged):
Subject: Acme Desk October 2026 update: $4,974 MRR, 15.0 months of runway
TL;DR: MRR fell 1.0% to $4,974, $326 behind plan, after a $299-a-month customer moved support in-house. Our ask: intros to heads of support at online retailers.
Numbers
| Metric | October 2026 | September 2026 | Change | % change | Plan | vs plan |
|---|---|---|---|---|---|---|
| MRR | $4,974 | $5,025 | -$51 | -1.0% | $5,300 | -$326 (-6.2%) |
| Paying customers | 26 | 25 | +1 | +4.0% | 27 | -1 (-3.7%) |
| Logo churn (monthly) | 4.0% | 4.0% | 0.0 pp | |||
| Net revenue retention (monthly) | 94.0% | 101.0% | -7.0 pp | |||
| Gross margin | 80.0% | 79.0% | +1.0 pp |
The one cancellation explains the drop in net revenue retention; logo churn held at 4.0%.
Highlights
- 2 new customers signed, taking paying customers to 26 (+1 net).
- Gross margin 80.0%, up 1.0 pp.
Lowlights
- MRR $4,974: down $51 (1.0%) on September and $326 (6.2%) behind plan. A $299-a-month customer moved support in-house, and the two new customers did not replace it.
- Net revenue retention 94.0% for the month, down 7.0 pp. We are starting check-in calls with every account paying $199 or more.
- Paying customers 1 behind plan (26 against 27).
Asks
- Intros to heads of support at 50 to 200 person online retailers.
Cash and runway
$90,000 in the bank at the end of October, down from $96,000, with nothing raised. Net burn was $6,000 a month on both measures, so runway is 15.0 months if burn stays flat.
Team
- A part-time support engineer starts on 3 November.
Definitions: MRR is monthly recurring revenue at month end. Logo churn is customers lost / customers at the start of the month. Net revenue retention is (starting MRR + expansion - contraction - churned) / starting MRR, for the month.
A made-up company and numbers. Your answer depends on your documents, and Claude's wording varies from run to run; the figures come from the script.
Install it
The skill is one folder. Download it once and add it wherever you use Claude; skills don't sync between claude.ai, Claude Code and the API, so each needs its own copy.
claude.ai and the Claude apps
- Download the ZIP and keep it zipped.
- In Claude, open Settings > Capabilities and turn on Code execution and file creation (called Create and edit files in the newer settings layout).
- Open Customize > Skills, click +, choose Create skill, then Upload a skill, and pick the ZIP.
- Leave the skill switched on. Claude uses it when your request matches its description; you don't have to name it.
On Team and Enterprise plans an owner first turns on code execution and skills under Organization settings > Plugins & skills. Anthropic's help centre has the details for each plan.
Claude Code
Unzip it into your personal skills folder to use it in every project, or into .claude/skills/ in a repository to share it with your team. Claude picks it up automatically; you can also run it by typing /investor-update-writing.
mkdir -p ~/.claude/skills
unzip investor-update-writing.zip -d ~/.claude/skills/Skills with a script need Python 3 on your computer.
Claude API
Upload the unzipped folder once; the response includes the skill's id. Uploaded skills are shared across your API workspace.
curl -X POST "https://api.anthropic.com/v1/skills" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-F "files[]=@investor-update-writing/SKILL.md;filename=investor-update-writing/SKILL.md" \
-F "files[]=@investor-update-writing/assets/example-metrics.json;filename=investor-update-writing/assets/example-metrics.json" \
-F "files[]=@investor-update-writing/assets/template.md;filename=investor-update-writing/assets/template.md" \
-F "files[]=@investor-update-writing/scripts/update_numbers.py;filename=investor-update-writing/scripts/update_numbers.py"Then list it in a request's container, with the code execution tool that skills run in (up to 20 skills per request):
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 4096,
"container": {
"skills": [{ "type": "custom", "skill_id": "YOUR_SKILL_ID", "version": "latest" }]
},
"tools": [{ "type": "code_execution_20250825", "name": "code_execution" }],
"messages": [{ "role": "user", "content": "..." }]
}'From Anthropic's skills API guide. Skills on the API run with no network access, which is why these skills' scripts use only Python's standard library.
What's in the skill
Every file, exactly as it is in the download. Claude reads SKILL.md when a request matches, opens the other files only when a step needs them, and runs scripts without loading their code. Read them before you install: a skill can direct Claude to run code, so only add skills you trust.
investor-update-writing/
SKILL.md
assets/example-metrics.json
assets/template.md
scripts/update_numbers.pySKILL.md90 lines
--- name: investor-update-writing description: Drafts monthly or quarterly investor updates, board updates and shareholder letters from a founder's metrics and notes, in a fixed structure (a one-line TL;DR, a KPI table with changes against last period and plan, highlights, lowlights, specific asks, cash and runway, and team). A bundled script works out every change, net burn, runway and burn multiple so no figure is calculated by hand, and the writing rules keep the update numbers-first and free of hype, with bad news early and metric definitions consistent from one update to the next. Use when a founder asks for an investor update, monthly update, quarterly update, board update, shareholder or stakeholder update, or wants to report MRR, ARR, revenue, churn, burn rate, runway, hiring or fundraising progress to investors, angels or a board. license: MIT compatibility: Works on claude.ai, Claude Code and the Claude API. The script needs Python 3.8+ (standard library only) and no network access. metadata: author: StartupReader version: "1.0" homepage: https://startupreader.com/claude-skills/investor-update-writing --- # Investor update writing Investors read updates quickly and compare them with the last one. An update earns trust when it reports the same numbers, measured the same way, every time, and when bad news arrives before anyone has to ask. Write it only from the founder's numbers and notes. ## Workflow Copy this checklist and tick it off as you go: ``` - [ ] 1. Collect this period's numbers and notes - [ ] 2. Write the metrics JSON (same KPIs and definitions as last time) - [ ] 3. Run scripts/update_numbers.py - [ ] 4. Draft the update from assets/template.md - [ ] 5. Check tone and traceability ``` ### 1. Collect the inputs Ask for everything missing in one message: - The period (month or quarter) and the previous update, if there is one. Reuse its KPIs, their order and their definitions. - Each KPI's value this period and last period, and the plan or target if the founder has one. - Cash at the end of this period and the last, and any money raised in the period. If they have them, revenue and operating expenses for the period. - Notes: wins, problems, hires and departures, and what they need from investors. Never fill a gap with an estimate. If a KPI was not measured, leave it out and say so in the update. **If there is a customer-level MRR export.** When the saas-metrics-analysis skill is available, run it on the export first and copy its figures exactly as printed: MRR, ARR, customers, logo churn, NRR and GRR for this month and last month from its table. For a quarterly update, take MRR, ARR and customers from the quarter-end months, add up the three months of new, expansion and churned MRR, and label any rate as monthly or as the quarter's average. Put its definitions in the update's definitions line. Without that skill, take MRR from the founder's billing tool and say which tool. ### 2. Write the metrics JSON Copy [assets/example-metrics.json](assets/example-metrics.json) and fill it in: - `kpis`: each with `name`, `unit` (`money`, `count`, `percent`, `ratio` or `months`), `this`, `last` and, if there is one, `plan`. - Percentages are fractions: `0.04` is 4%. - Names containing churn, burn, CAC, payback, cost or expense are treated as lower-is-better. For any other metric where lower is better, add `"higher_is_better": false`. - Name the recurring revenue KPI exactly `MRR` or `ARR` so the script can work out the burn multiple. - `cash`: `this`, `last` and `raised` (equity or debt received this period, which is not burn). `operating`: `revenue` and `expenses` as totals for the whole period. - `months_in_period`: 1 for monthly, 3 for quarterly. ### 3. Run the numbers ```bash python scripts/update_numbers.py metrics.json python scripts/update_numbers.py assets/example-metrics.json ``` The script prints the KPI table in Markdown, a "Worse than last period" line, a "Behind plan" line, and cash, net burn, runway and burn multiple. Runway is cash divided by monthly net burn, with burn held flat. If it reports an error, fix the input it names and run it again. Do not work out any figure yourself. ### 4. Draft the update Fill in [assets/template.md](assets/template.md), in this order: 1. **TL;DR**: one sentence with the most important number, the biggest problem and the top ask. 2. **Numbers**: the script's table, unchanged, then one or two sentences on what moved most and why. 3. **Highlights**: two to four bullets, each starting with a number or a named outcome. 4. **Lowlights**: everything on the "Worse than last period" and "Behind plan" lines that matters, plus problems from the notes. Say what happened, why (if known) and what the founder is doing about it. 5. **Asks**: one to three, each naming who and what. "Intros welcome" is not an ask; "an intro to a head of support at a 50 to 200 person online retailer, to test our escalation workflow" is. 6. **Cash and runway**: cash, net burn and which figure it is, and runway in months, from the script. 7. **Team**: joiners, leavers with their roles, and open roles. End with a definitions line for every KPI. Follow the tone rules below throughout. ### 5. Check before sending - Every number appears in the script output or the founder's notes. - If "Worse than last period" or "Behind plan" lists a key metric, it is in the TL;DR or the lowlights. - Each ask names a type of person or company and the action wanted. - KPI names and definitions match the last update, or the change is stated. - No hype words or exclamation marks. ## Tone rules - **Numbers first.** Start bullets with the figure: "$4,974 MRR, down 1.0%", not "MRR dipped slightly". - **No hype.** Leave out words like excited, thrilled, huge, massive, incredible, game-changing and crushing it, and exclamation marks. Let the numbers carry it. - **Bad news early and plainly.** If a key metric fell or missed plan, the TL;DR says so. Do not soften with "slight", "temporary" or "one-off" unless the founder's notes show why. - **Same metrics, same definitions.** Keep last update's KPIs and order. If a definition changed, say so in the update and give last period on the new basis, or say that it cannot be restated. - **Every number traceable.** Each figure comes from the script output or the founder's notes, quoted as printed. No forecasts unless the founder gave them, labelled as their plan. - **Short.** Investors skim. Cut anything that is not a number, a decision, a problem or an ask. - Updates get forwarded. Name customers, candidates or partners only if the founder's notes already do.
assets/example-metrics.json17 lines
{
"company": "Acme Desk",
"currency": "$",
"period": "October 2026",
"previous_period": "September 2026",
"months_in_period": 1,
"kpis": [
{ "name": "MRR", "unit": "money", "this": 4974, "last": 5025, "plan": 5300 },
{ "name": "Paying customers", "unit": "count", "this": 26, "last": 25, "plan": 27 },
{ "name": "Logo churn (monthly)", "unit": "percent", "this": 0.04, "last": 0.04 },
{ "name": "Net revenue retention (monthly)", "unit": "percent", "this": 0.94, "last": 1.01 },
{ "name": "Gross margin", "unit": "percent", "this": 0.80, "last": 0.79 }
],
"cash": { "this": 90000, "last": 96000, "raised": 0 },
"operating": { "revenue": 5000, "expenses": 11000 }
}
assets/template.md40 lines
Subject: [Company] [period] update: [headline KPI and value], [runway] months of runway Hi all, **TL;DR:** [One sentence: the number that matters most, the biggest problem, and the top ask.] ## Numbers [The KPI table from the script, pasted unchanged.] [One or two sentences on what moved most and why.] ## Highlights - [Number first, then what happened and why it matters.] - [Two to four bullets. Each one has a figure or a named outcome.] ## Lowlights - [What went wrong or is behind plan, with the number, the cause if known, and what you are doing about it.] - [Everything on the script's "Worse than last period" and "Behind plan" lines that matters belongs here.] ## Asks 1. [Who exactly, and what you want them to do. Example: an intro to a head of support at a 50 to 200 person online retailer, for a 30-minute call to test our escalation workflow.] 2. [One to three asks: intros, candidates for a named role, or advice on a specific decision.] ## Cash and runway [Cash at the end of the period, net burn a month and which figure it is (change in cash, or expenses minus revenue), runway in months with burn held flat. Fundraising plans or timing, if any.] ## Team - [Joiners and leavers with their roles, open roles, and anything investors can help fill.] [Name] --- Definitions: [How each KPI is calculated, the same as last update unless stated. Say what changed, if anything.]
scripts/update_numbers.py285 lines
#!/usr/bin/env python3
"""Numbers for an investor update: KPI table, net burn, runway and burn multiple.
Reads a small JSON file with this period's and last period's figures and prints
a Markdown KPI table with absolute and % changes (percentage points for
percentages), the comparison with plan, the metrics that got worse or are
behind plan, and cash, net burn, runway and burn multiple. Claude pastes these
figures into the update instead of doing any arithmetic itself.
Standard library only and no network, so it runs in the Claude API sandbox, on
claude.ai and in Claude Code.
Usage:
python scripts/update_numbers.py metrics.json
JSON fields (see assets/example-metrics.json):
company optional, for the heading
currency optional symbol or code for money, e.g. "$", "₹", "EUR"
period label of this period, e.g. "October 2026" or "Q3 2026"
previous_period label of the period compared with
months_in_period 1 for a monthly update (default), 3 for a quarterly one
kpis list of {name, unit, this, last, plan?, higher_is_better?, decimals?}
unit: money | count | percent | ratio | months
percent values are fractions: 0.04 means 4%
higher_is_better defaults to false for names containing
churn, burn, CAC, payback, cost or expense (but not LTV:CAC),
and to true otherwise.
A KPI named "MRR" or "ARR" is used for the burn multiple.
cash optional {this, last?, raised?}: cash at the end of each
period and money raised from investors or lenders in this one
operating optional {revenue, expenses}: totals for this period
Net burn per month = (expenses - revenue) / months_in_period, or, without
"operating", (cash last - cash this + raised) / months_in_period.
Runway (months) = cash this / net burn per month, with burn held flat.
Burn multiple = net burn over the period / net new ARR over the period.
"""
import json
import re
import sys
UNITS = ("money", "count", "percent", "ratio", "months")
DEFAULT_DECIMALS = {"money": 0, "count": 0, "percent": 1, "ratio": 1, "months": 1}
LOWER_IS_BETTER = re.compile(r"churn|burn|\bcac\b|payback|cost|expense", re.IGNORECASE)
HIGHER_ANYWAY = re.compile(r"\bltv\b", re.IGNORECASE) # LTV:CAC contains "CAC" but higher is better
# Churn and margins are shares of a whole, so as fractions they cannot pass 1.
SHARE_OF_WHOLE = re.compile(r"churn|margin", re.IGNORECASE)
# A percent fraction above this (1,000%) is almost certainly a whole-number percentage.
MAX_PERCENT_FRACTION = 10
MONTHS_PER_YEAR = 12
def fail(message):
print(f"Error: {message}", file=sys.stderr)
sys.exit(2)
def signed(x, decimals):
"""Format with an explicit + or -, and no sign (and no -0) when it rounds to zero."""
if round(x, decimals) == 0:
return f"{0:,.{decimals}f}"
return f"{x:+,.{decimals}f}"
def number(value, where, minimum=None):
if isinstance(value, bool) or not isinstance(value, (int, float)):
fail(f"{where} must be a number, not {json.dumps(value)}")
if minimum is not None and value < minimum:
fail(f"{where} cannot be below {minimum:g} (is {value:g})")
return float(value)
class Formatter:
def __init__(self, currency):
self.currency = currency or ""
def money(self, x, decimals, signed=False):
if round(x, decimals) == 0:
x = 0.0
sign = "-" if x < 0 else ("+" if signed and x > 0 else "")
body = f"{abs(x):,.{decimals}f}"
cur = self.currency
if len(cur) > 1 and cur.isalpha():
cur += " "
return f"{sign}{cur}{body}"
def value(self, kpi, x, sign=False):
unit, d = kpi["unit"], kpi["decimals"]
if unit == "money":
return self.money(x, d, sign)
if unit == "percent":
return (signed(x * 100, d) if sign else f"{x * 100:,.{d}f}") + "%"
text = signed(x, d) if sign else f"{x:,.{d}f}"
if unit == "ratio":
return text + "x"
if unit == "months":
return text + " mo"
return text
def change(self, kpi, new, old):
"""Absolute change (pp for percentages) and % change, or '' where % is not meaningful."""
if kpi["unit"] == "percent":
return f"{signed((new - old) * 100, kpi['decimals'])} pp", ""
diff = self.value(kpi, new - old, sign=True)
rel = f"{signed((new - old) / old * 100, 1)}%" if old > 0 else ""
return diff, rel
def load(path):
try:
with open(path, encoding="utf-8-sig") as f:
data = json.load(f)
except OSError as e:
fail(f"cannot read {path}: {e.strerror}")
except json.JSONDecodeError as e:
fail(f"{path} is not valid JSON: {e.msg} at line {e.lineno}, column {e.colno}")
if not isinstance(data, dict):
fail("the JSON must be an object with period, previous_period and kpis")
for key in ("period", "previous_period"):
if not isinstance(data.get(key), str) or not data[key].strip():
fail(f'"{key}" is missing: give a label such as "October 2026"')
months = data.get("months_in_period", 1)
if isinstance(months, bool) or not isinstance(months, int) or months < 1:
fail('"months_in_period" must be a whole number of months: 1 for monthly, 3 for quarterly')
data["months_in_period"] = months
kpis = data.get("kpis")
if not isinstance(kpis, list) or not kpis:
fail('"kpis" must be a non-empty list of {name, unit, this, last}')
seen = set()
for i, k in enumerate(kpis, 1):
if not isinstance(k, dict):
fail(f"kpis[{i}] must be an object")
name = k.get("name")
if not isinstance(name, str) or not name.strip():
fail(f"kpis[{i}] needs a name")
where = f'KPI "{name}"'
if name.lower() in seen:
fail(f"{where} appears twice")
seen.add(name.lower())
if k.get("unit") not in UNITS:
fail(f"{where}: unit must be one of {', '.join(UNITS)} (is {json.dumps(k.get('unit'))})")
for field in ("this", "last"):
if field not in k:
fail(f'{where} needs "{field}". If last period was not measured, leave the KPI out and say so in the update')
k[field] = number(k[field], f"{where} {field}")
if k.get("plan") is not None:
k["plan"] = number(k["plan"], f"{where} plan")
if k["unit"] == "percent":
for field in ("this", "last", "plan"):
v = k.get(field)
if v is None:
continue
if abs(v) > MAX_PERCENT_FRACTION or (SHARE_OF_WHOLE.search(name) and v > 1):
fail(f"{where} {field} is {v:g}: percentages are fractions, e.g. 0.04 for 4%")
hib = k.get("higher_is_better")
if hib is None:
k["higher_is_better"] = bool(HIGHER_ANYWAY.search(name)) or not LOWER_IS_BETTER.search(name)
elif not isinstance(hib, bool):
fail(f"{where}: higher_is_better must be true or false")
d = k.get("decimals", DEFAULT_DECIMALS[k["unit"]])
if isinstance(d, bool) or not isinstance(d, int) or not 0 <= d <= 4:
fail(f"{where}: decimals must be a whole number from 0 to 4")
k["decimals"] = d
cash = data.get("cash")
if cash is not None:
if not isinstance(cash, dict) or "this" not in cash:
fail('"cash" must be an object with "this" (cash at the end of this period)')
for field in ("this", "last", "raised"):
if cash.get(field) is not None:
cash[field] = number(cash[field], f"cash.{field}", minimum=0)
op = data.get("operating")
if op is not None:
if not isinstance(op, dict) or "revenue" not in op or "expenses" not in op:
fail('"operating" must be an object with "revenue" and "expenses" for this period')
for field in ("revenue", "expenses"):
op[field] = number(op[field], f"operating.{field}", minimum=0)
return data
def worse(k, new, old):
return new < old if k["higher_is_better"] else new > old
def main():
try:
sys.stdout.reconfigure(encoding="utf-8") # currency symbols such as the rupee sign on any console
except (AttributeError, ValueError):
pass
if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
print(__doc__)
sys.exit(0)
if len(sys.argv) != 2:
fail("give one metrics JSON file, e.g. python scripts/update_numbers.py assets/example-metrics.json")
data = load(sys.argv[1])
fmt = Formatter(data.get("currency"))
kpis = data["kpis"]
months = data["months_in_period"]
this_label, last_label = data["period"].strip(), data["previous_period"].strip()
has_plan = any(k.get("plan") is not None for k in kpis)
heading = f"{data['company']}: " if data.get("company") else ""
print(f"{heading}{this_label} vs {last_label} ({months} month{'s' if months > 1 else ''})")
print()
head = ["Metric", this_label, last_label, "Change", "% change"] + (["Plan", "vs plan"] if has_plan else [])
print("| " + " | ".join(head) + " |")
print("|" + "---|" * len(head))
worse_lines, behind_lines = [], []
for k in kpis:
diff, rel = fmt.change(k, k["this"], k["last"])
cells = [k["name"], fmt.value(k, k["this"]), fmt.value(k, k["last"]), diff, rel]
if worse(k, k["this"], k["last"]):
worse_lines.append(f"{k['name']} {diff}" + (f" ({rel})" if rel else ""))
if has_plan:
plan = k.get("plan")
if plan is None:
cells += ["", ""]
else:
pdiff, prel = fmt.change(k, k["this"], plan)
cells += [fmt.value(k, plan), pdiff + (f" ({prel})" if prel else "")]
if worse(k, k["this"], plan):
behind_lines.append(f"{k['name']} {pdiff}" + (f" ({prel})" if prel else ""))
print("| " + " | ".join(cells) + " |")
print()
print("Worse than last period: " + ("; ".join(worse_lines) if worse_lines else "none"))
if has_plan:
print("Behind plan: " + ("; ".join(behind_lines) if behind_lines else "none"))
cash, op = data.get("cash"), data.get("operating")
money = lambda x: fmt.money(x, 0)
print()
print("Cash and runway")
if cash is None and op is None:
print(' Not computed: add "cash" (and "operating" or cash.last) to get net burn and runway')
burn_cash = burn_op = None
if cash is not None:
raised = cash.get("raised") or 0.0
line = f" Cash at the end of {this_label}: {money(cash['this'])}"
if cash.get("last") is not None:
line += f" ({last_label}: {money(cash['last'])}; raised in the period: {money(raised)})"
burn_cash = (cash["last"] - cash["this"] + raised) / months
print(line)
if burn_cash is not None:
print(f" Net burn from the change in cash: {money(burn_cash)} a month")
if op is not None:
burn_op = (op["expenses"] - op["revenue"]) / months
sums = f"{money(op['expenses'])} - {money(op['revenue'])}"
if months > 1:
sums = f"({sums}) / {months}"
print(f" Net burn from expenses minus revenue: {sums} = {money(burn_op)} a month")
burn = burn_op if burn_op is not None else burn_cash
basis = "expenses minus revenue" if burn_op is not None else "the change in cash"
if burn_op is not None and burn_cash is not None and round(burn_op) != round(burn_cash):
print(f" The two burn figures differ by {money(abs(burn_op - burn_cash))} a month, usually from the timing of"
" collections, annual prepayments or one-off payments. Say which one the update uses.")
if burn is not None and cash is not None:
if burn > 0:
print(f" Runway = {money(cash['this'])} / {money(burn)} = {cash['this'] / burn:.1f} months"
f" (net burn from {basis}, held flat)")
else:
print(f" Not burning cash this period (net burn {money(burn)} a month): runway is not limited by burn")
elif burn is not None:
print(' Runway needs "cash": {"this": ...}')
arr_kpi = next((k for k in kpis if re.match(r"^(mrr|arr)\b", k["name"].strip(), re.IGNORECASE)
and k["unit"] == "money"), None)
if arr_kpi is not None and burn is not None and burn > 0:
factor = MONTHS_PER_YEAR if arr_kpi["name"].strip().lower().startswith("mrr") else 1
net_new_arr = (arr_kpi["this"] - arr_kpi["last"]) * factor
label = "ARR (MRR x 12)" if factor > 1 else "ARR"
if net_new_arr > 0:
period_burn = burn * months
note = "" if months >= 3 else "; one month is noisy, a quarter is the usual basis"
print(f" Burn multiple = {money(period_burn)} net burn / {money(net_new_arr)} net new {label}"
f" = {period_burn / net_new_arr:.1f}x{note}")
else:
print(f" Burn multiple: not meaningful, {label} did not grow this period")
print()
print("Percentage metrics change in percentage points (pp). % change is blank where it is not meaningful"
" (percentages, or a zero or negative base).")
if __name__ == "__main__":
main()
Limits
- It writes from the numbers and notes you give it. It can't check them against your accounts or bank, so the update is only as accurate as your inputs.
- Runway assumes burn stays flat. To see what revenue or cost growth does to it, use the burn rate and runway calculator.
- Burn multiple on one month swings with the timing of deals and payments. The script says so; quarterly updates give a steadier figure.
- It won't invent reasons. If a metric fell and your notes don't say why, the draft says the cause isn't known yet, and Claude asks.
- It drafts the update; it doesn't send it or keep your investor list.
Questions
Monthly or quarterly?
The skill handles both: set months_in_period to 1 or 3 and it divides burn accordingly. Whichever you choose, keep to it. Updates that arrive on schedule with the same metrics are easier for investors to compare than occasional long ones.
What happens when a number got worse?
The script lists every metric that fell or missed plan, and the skill puts the important ones in the TL;DR and the lowlights, with what happened and what you are doing about it. It won't soften a miss with words like "slight" or "temporary" unless your notes say why.
Can it use the numbers from the SaaS metrics skill?
Yes. If you have an MRR export by customer, Claude runs the SaaS metrics analysis skill on it first and copies MRR, ARR, customers, churn and retention for this period and last into the metrics file exactly as printed, along with that skill's definitions.
Why are churn and retention changes in percentage points?
Because a change in a rate is clearer as a difference than as a percentage of a percentage. Churn moving from 2% to 3% is 1 percentage point; as a percentage change it would be 50%, which sounds alarming and can't be compared with other months.
Does the skill send my numbers anywhere?
The skill is instructions, a template and a Python script with no network calls, so it sends nothing anywhere. What you paste or upload is handled under your Claude plan's normal data terms.
Learn what the skill checks
The skill is built from the same rules and formulas as these StartupReader pages, so you can follow its reasoning step by step.
- ExplainerBurn multiple: how to calculate it and what good looks like →Burn multiple is net burn ÷ net new ARR: how much cash a startup spends to add each rupee of recurring revenue. Formula, worked example and rough benchmarks.
- ExplainerARR vs MRR: what counts as recurring revenue, with examples →MRR is monthly recurring revenue; ARR is the same on a yearly basis. What counts, run-rate vs contracted ARR, and how net new MRR builds up, with an example.
- ExplainerUnit economics and the LTV:CAC ratio, with a calculator →Unit economics show whether each customer makes money. How to calculate customer lifetime value, acquisition cost, the LTV:CAC ratio and payback, with a free calculator.
- CalculatorBurn rate & runway calculator →See your gross and net burn, how many months of cash you have, and when it runs out, with revenue and cost growth.
- CalculatorARR & MRR calculator →Build a month's MRR bridge from new, expansion, contraction and churned revenue to get MRR, ARR, growth and net retention.
- CalculatorChurn rate calculator →Work out monthly customer churn, what it compounds to over a year, average customer lifetime and gross revenue churn.
Pairs well with
- Claude skill · Metrics and investor updatesSaaS metrics analysis →A free Claude skill that turns an MRR export into the monthly MRR bridge, churn, NRR, GRR and ARPA, then LTV:CAC, CAC payback, runway and burn multiple.
- Claude skill · FundraisingPitch deck review →A free Claude skill that reviews your investor deck slide by slide, rebuilds your market size bottom-up and checks that every number agrees.
- Claude skill · FundraisingDue diligence prep →A free Claude skill that builds your data room index from file names, maps your documents to a due diligence checklist and flags the gaps and red flags.
- Claude skill · FundraisingTerm sheet review →A free Claude skill that reads a VC term sheet, works out the real valuation, preference payouts and anti-dilution, and rates every clause.
- Claude skill · Equity and cap tablesCap table modeling →A free Claude skill that converts your SAFEs and convertible notes, tops up the option pool and shows who owns what after your next priced round.
- Claude skill · Equity and cap tablesFounder equity split →A free Claude skill that suggests a co-founder equity split from weighted scores, then models vesting, a founder leaving and dilution from funding rounds.
The weekly brief
More on investor update writing, once a week
Who raised, who launched and the founder terms worth knowing, in one short email a week.
The StartupReader app
Startup news in 60-second swipes
Funding, launches and founder programs, each on one card with the key numbers. Free, with read-aloud and auto-scroll.
Building something? List your startup free.
Written and tested by the StartupReader editorial desk with AI assistance; the example outputs are the skill's scripts run on the made-up figures shown. StartupReader is not affiliated with Anthropic. The skill gives general information, not legal, tax or financial advice. Spotted something wrong? Tell us.