Runway & burn-rate digest: Stripe → Notion → Slack
Stripe MRR + Notion spend & cash → weekly runway numbers posted straight to Slack.
Runway needs MRR, monthly spend, and cash — Stripe holds only the first. This pulls MRR from Billing Analytics (any Billing account), reads one hand-typed spend-and-cash row per month from Notion, computes burn and runway, and posts to Slack: a founder number board, not an investor narrative you edit before sending. Notion formulas cannot call Stripe, so the cross-source math lives nowhere else. Every Slack message labels cash as hand-entered, not bank-reconciled; at-or-below-MRR spend reports cash-flow positive instead of a nonsense runway.
How it flows
- 01
Monday 08:00 trigger
Weekly kickoff early enough for the founder board before the week starts. Wire it to whatever scheduler drives this workflow.
- 02
Stripe MRR pulled
Billing Analytics' "Subscription metrics summary" leads for the current MRR absolute. After Configure definition changes, Stripe says those can take 24–48 hours to apply — that is not a documented lag of the underlying MRR figures; treat the CSV as period-end analytics, not intra-day live balance. The paginated subscription-list fallback covers accounts without Analytics available.
- 03
Notion spend and cash balance read
One query against this month's single Monthly Opex row. Amount and Cash on hand are already hand-typed summaries — no cross-row summing for the job to get wrong.
- 04
Burn computed — zero and negative branches first
Burn = this month's spend − MRR. When spend is at or below revenue, burn is zero or negative: the job reports "cash flow positive — no runway to compute" and skips the division. Never post a negative or infinite runway month-count.
- 05
Runway computed from hand-typed cash
Only when burn is positive: runway months = Cash on hand ÷ burn. Cash is the Notion hand-entered figure from the prior step — not a bank feed, not Stripe Balance.
- 06
Digest posted to Slack
MRR, monthly burn, cash on hand, and runway in one Block Kit message — same template every week, including the permanent hand-entered-cash context footer. When the prior step reported cash-flow positive, put that phrase in the Runway field (do not invent a month-count) and still ship the footer; never drop the disclaimer on the profitable branch.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
Stripe
Current MRR — revenue side only
- 01
Create a restricted, read-only key
Developers → API keys → Create restricted key. Grant Read on Subscriptions and Invoices; leave everything else off, especially Balance, Payouts, Refunds, and any Write permission. A weekly read of a few endpoints sits far under Stripe's live rate limits (100 req/s global), so throttling is not a concern for this job.
- 02
Confirm Billing Analytics and export the MRR summary
Dashboard → Billing → Analytics → Configure. Note how MRR and "active" are computed (discounts, trials) and keep that as your baseline. Any Stripe Billing account already includes this panel and its three CSV exports — it is not a higher-tier add-on. Prefer the "Subscription metrics summary" CSV for the current MRR absolute. After you change Configure definitions (discounts / active-subscriber rules), Stripe says those configuration changes can take 24–48 hours to apply — that is not a documented 24–48h lag of the underlying MRR figures themselves; treat the CSV as period-end analytics, not intra-day live balance.
- 03
Fallback: sum MRR from the subscription list
When Billing Analytics is unavailable, list active subscriptions and normalise yourself: yearly price ÷ 12, monthly as-is, all in minor units. Paginate to the end — the default page is 10, and a truncated page silently looks like the company shrank. Auth is `Authorization: Basic` with the restricted key as the username and an empty password (`-u "$STRIPE_RESTRICTED_KEY:"`).
Active subscriptions, paginatedcurl -G https://api.stripe.com/v1/subscriptions \ -u "$STRIPE_RESTRICTED_KEY:" \ -d status=active \ -d limit=100 # yearly price / 12, monthly as-is, all in minor units; page to the end - 04
Know what Stripe cannot tell you
GET /v1/balance returns Stripe's own processing float — money collected but not yet paid out to your bank — not your company bank balance, and never payroll or rent. This workflow does not call it. Cash on hand comes from Notion, hand-typed, in the next section; keeping Stripe out of that number is what keeps this a revenue-side-only integration.
Notion
Monthly spend total and hand-typed cash balance
- 01
Create a "Monthly Opex" database — one row per month
One row per month, never one row per receipt. Properties: Month (Date), Category (Select: Payroll / Opex / One-time — label the dominant bucket, or leave blank), Amount (Number — that month's total spend, typed once as a summary), Cash on hand (Number — your bank balance as of today, typed once and updated when you re-check). Monthly totals, not line items, keep the weekly job from summing half-entered rows and understating burn.
- 02
Share the database with your integration
Create an Internal Integration Token, then open the database → ••• (top right) → Connections → Add connection → your integration. A database you can plainly see still 404s until this step runs. Default "Read content" is enough — do not grant Insert or Update; this job never writes back to Notion.
- 03
Retrieve the data source id once, then query this month
GET /v1/databases/{database_id} and keep the data_source_id it returns. The database query endpoint is deprecated as of API version 2025-09-03 — every read goes through POST /v1/data_sources/{data_source_id}/query with Notion-Version: 2026-03-11. Filter Month on_or_after the first of this month; expect one row, and read Amount and Cash on hand from it.
POST /v1/data_sources/{data_source_id}/query · Notion-Version: 2026-03-11{ "filter": { "property": "Month", "date": { "on_or_after": "2026-08-01" } } }
Slack
Where the number lands — no editing in between
- 01
Create the app with two scopes
api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → Bot Token Scopes. Add chat:write (post the digest) and channels:read (resolve the channel name to an id) — nothing else. This is a one-way broadcast: no channel history to read, no Slack user to map back to an external identity.
- 02
Install to the workspace and invite the bot
Install the app, then /invite it into #finance-ops (or another private finance channel). Stick to a channel id resolved with channels:read — a DM path needs extra scopes and an open-conversation step this workflow does not set up. Runway is a whole-company number that never routes to a specific person.
- 03
Build the message template with the cash disclaimer baked in
Use Block Kit: one section with four fields (MRR, monthly burn, cash on hand, runway) plus a permanent context footer. The cash-balance caveat is part of the template, not a setup note someone might trim later — every weekly post reuses this same payload, so the disclaimer ships with every message. Put a short fallback in text for the notification tray. On the cash-flow-positive branch, replace the Runway field value with "cash flow positive — no runway to compute" and keep the context footer unchanged.
POST https://slack.com/api/chat.postMessage{ "channel": "C0123FINOPS", "text": "Runway digest: 8.4 months left", "blocks": [ { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*MRR:*\n$42,100" }, { "type": "mrkdwn", "text": "*Monthly burn:*\n$18,300" }, { "type": "mrkdwn", "text": "*Cash on hand (Notion, manual):*\n$154,000" }, { "type": "mrkdwn", "text": "*Runway:*\n8.4 months" } ]}, { "type": "context", "elements": [ { "type": "mrkdwn", "text": "Cash balance is hand-entered in Notion — Stripe has no bank data. Treat this as an estimate, not a reconciled figure." } ]} ] }