WorkflowsMCP

Weekly investor update draft

Stripe metrics plus Notion highlights, assembled into a Gmail draft you edit and send.

@workflowsmcpFeb 2, 2026Verified Aug 2026foundersreportingmetricsai-drafting

Investor updates slip because assembling the numbers is the boring part. This workflow pulls the week's revenue movement from Stripe with a read-only key, collects highlights and lowlights from a Notion database, drafts the update in your own voice, and leaves it sitting in Gmail as a draft. It never sends — a machine-written note to your board going out unread is a failure mode worth designing against.

How it flows

  1. 01

    Monday 07:00 trigger

    Early enough that the draft is waiting before the week starts.

  2. 02

    Stripe metrics pulled

    MRR, new, expansion and churned, week over week, with a read-only restricted key.

  3. 03

    Highlights pulled from Notion

    Only rows flagged "Share with investors" from the last seven days.

  4. 04

    Draft assembled in your voice

    Numbers first, then wins, then lowlights, then a specific ask. Lowlights are never dropped.

  5. 05

    Gmail draft created

    Draft only. The job never calls the send endpoint — that, and not the scope, is what stops it.

  6. 06

    You edit and press send

    The reminder links straight to the draft. Nothing reaches investors unread.

Set up each app

Work through these in order — later apps usually need a token or an id from an earlier one.

Stripe

The revenue numbers, read-only

  1. 01

    Create a restricted, read-only key

    Developers → API keys → Create restricted key. Grant Read on Subscriptions, Charges, Customers and Invoices; leave every Write permission off. A reporting job has no business holding a key that can refund — and that is Stripe's own guidance, which recommends restricted keys over secret ones especially when the key is being handed to an AI agent.

  2. 02

    Take the numbers from Billing analytics first

    Stripe already computes what this update needs. Billing → Analytics decomposes MRR growth into New, Reactivation, Expansion, Contraction and Churn MRR, and it handles the edge cases hand-rolled normalisation gets wrong: trials, past_due, FX, discounts. Two exports do the job — "Customer MRR changes" is a log of every MRR change per customer, "Subscription metrics summary" is the MRR roll-forward. Pull those as CSV and you can skip both API steps below. State the one honest caveat in the note itself: the figures land 24 to 48 hours behind unless you turn on Faster Data Updates, so a Monday 07:00 job is reading through Saturday.

  3. 03

    Fallback: compute MRR from the subscription list

    For accounts without Billing analytics. List active subscriptions and normalise yearly plans yourself — Stripe's own definition is the yearly amount divided by 12, monthly as-is, all in minor units. Paginate: the default page is 10 and the silent truncation looks exactly like churn. There is nothing to expand here — the nested price on each item already carries unit_amount and recurring.interval in the default response.

    Active subscriptions
    curl -G https://api.stripe.com/v1/subscriptions \
      -u "$STRIPE_RESTRICTED_KEY:" \
      -d status=active \
      -d limit=100
    
    # normalise: yearly amount / 12, monthly amount as-is, all in minor units
  4. 04

    Fallback: get the week-over-week delta from events

    There is no canceled_at filter on the subscription list, and status=canceled returns every cancellation you have ever had, not a recent window. The documented way to ask "what changed in this window" is the events endpoint. Note what a created-plus-canceled diff can never tell you: an upgrade is neither, so expansion and contraction only surface in the updated event, by diffing previous_attributes. That matters, because "MRR went up" is not a board update and "up 4.2k, of which 3.1k is expansion" is.

    What changed in the last 7 days
    # types takes up to 20 event names; events are retained for 30 days.
    # created[gte] = 2026-03-04T00:00:00Z, the same window the Notion query uses.
    curl -G https://api.stripe.com/v1/events \
      -u "$STRIPE_RESTRICTED_KEY:" \
      -d "types[]=customer.subscription.created" \
      -d "types[]=customer.subscription.deleted" \
      -d "types[]=customer.subscription.updated" \
      -d "created[gte]=1772582400" \
      -d limit=100
    
    # created -> new MRR; deleted -> churned MRR;
    # updated + previous_attributes.items -> expansion / contraction.

Notion

The qualitative half of the update

  1. 01

    Keep a "Company Highlights" database

    Properties: Name (Title), Date (Date), Type (Select: Win / Lowlight / Hire / Ask), Detail (Text), Share with investors (Checkbox). The checkbox matters — not everything internal belongs in a board note.

  2. 02

    Query the last seven days

    Share the database with your internal connection first (••• → Connections → Add connection) and keep "read content" on, or this 404s on a database you can plainly see. Then POST /v1/data_sources/{data_source_id}/query with Notion-Version: 2026-03-11 (retrieve the database once for its data source id; /v1/databases/{id}/query is deprecated), filtering on the checkbox and the date window. Grouping by Type in your prompt keeps the draft from turning every item into a win.

    Data source query filter
    {
      "filter": {
        "and": [
          { "property": "Share with investors", "checkbox": { "equals": true } },
          { "property": "Date", "date": { "on_or_after": "2026-03-04" } }
        ]
      },
      "sorts": [{ "property": "Type", "direction": "ascending" }]
    }

Gmail

Holds the draft — and only the draft

  1. 01

    Authorise the compose scope, and know what it does not stop

    Use the gmail.compose scope (full value in the snippet). It is the narrowest scope that can create a draft, but be clear about what it is: Google documents it as "Manage drafts and send emails", so it permits sending. Nothing in the credential stops a send. This workflow never sends because the job never calls users.messages.send — that is a property of the code, not of the key, and there is no key that behaves otherwise, because drafting and sending share a scope. Second caveat: gmail.compose is a restricted scope. Against your own mailbox — an internal Workspace app, or an unpublished project with yourself as the test user — that is fine. Publishing it to outside users triggers OAuth app verification and, if you store or transmit the data, a CASA security assessment renewed every year.

    The scope value
    https://www.googleapis.com/auth/gmail.compose
  2. 02

    Create the draft

    The Gmail API takes a base64url-encoded RFC 2822 message in raw: the -_ alphabet, not +/. Most helpers do it for you — urlsafe_b64encode in Python, toString("base64url") in Node. Build the message itself with CRLF line breaks and keep the headers to 7-bit ASCII; a raw em dash in a Subject line needs RFC 2047 encoding, so use a plain hyphen unless you are ready to encode it.

    POST /gmail/v1/users/me/drafts
    {
      "message": {
        "raw": "VG86IGludmVzdG9yc0BhY21lLmNvbQ0KU3ViamVjdDogQWNtZSAtIHdlZWsgb2YgMTEgTWFyDQoNCk1SUiAkNDIuMWsgKCs0LjJrKSwgb2Ygd2hpY2ggJDMuMWsgaXMgZXhwYW5zaW9uLi4u"
      }
    }
  3. 03

    Nudge yourself, do not automate the send

    Have the job run Monday 07:00 and post yourself a Slack message, or an email, linking straight to the draft. The drafts create call returns an id rather than a URL, so build the browser link from it in the form below. The five minutes you spend editing is the entire value of the update.

    Link to the draft you just created
    https://mail.google.com/mail/u/0/#drafts?compose=r-1938440021785509377