Skip to main content

Dot the data bot: how I built a daily growth report that thinks

Von Katie Hindson
Dot, the Morse data bot

A while back I wrote about how I made our AI understand Morse's data.

That post was about giving the bot a good foundation, but it felt like it was missing a follow-up example of how we actually used this.

So, I wanted to walk through how I built a daily growth monitoring report that thinks, using our data. The bot I built runs every morning, digs into whatever moved, forms hypotheses, chases them down, and posts its findings to Slack. It remembers what it said yesterday and anyone on the team can change how it works.

Its name is Dot (the data bot).

Dot replaced the 1–2 hours I used to spend every morning asking "why did this number move?" It feels like a reusable solution, so hopefully this inspires you to build a Dot of your own 🙂

Dot posts its daily growth report to Slack

The problem(s) I was actually trying to solve

There were two key problems I was trying to solve here:

First: the daily "why did this move?" question was eating my mornings. Every day someone would post in our data channel asking why signups dipped or adoption spiked, and I'd spend an hour or two digging through BigQuery to answer it. It was important work but it was also the same shape of work every single day which is exactly the kind of thing worth automating.

Second: people see trends visually, and I didn't want to give that up just because I was using Claude. I only ran into this problem because my first attempt at the morning question skipped dashboards entirely.

The thing is, a good chart tells you in half a second what a table of numbers takes a paragraph to explain. And while Claude is great at creating one-off charts when it's answering questions, it can't give you what a dashboard does: a curated set of charts, arranged to tell a clear data story, that you can glance at and monitor for changes. That still lives in our BI tool.

People still wanted dashboards and I needed to make dashboards work with Claude, not instead of them.

So I built up my solution to these two problems in layers, and each layer solved a specific pain from the one before.

Layer 1: just post something to Slack

The first version was simple (but it was a step in the right direction). It was a cron job that started by loading the data context from my last post:

Startup sequence (run once before answering anything): Query sling-data.dbt_prod.claude_context and parse the JSON. Apply all sections for the rest of the session.

That one query is what lets it dig into anything in our data because it knows what our tables mean, which filters to apply, and where to look. From there it ran a few key-metric queries I'd saved, wrote up what looked interesting, and posted to Slack. No memory, no deep dives, no hypotheses. Just "here's what the numbers did today."

Version one: a scheduled job that queried key metrics and posted a summary to Slack

It had no idea what it had said the day before, so it kept re-reporting the same trend as if it were brand new. And when it got something wrong (missed a cause, jumped to a conclusion) there was no way to correct it, so tomorrow it would just make the same mistake again.

Layer 2: give it a memory with a Notion database

The fix for "it forgets everything" was to give it somewhere to write things down.

I created a Notion database of daily reports. Every day's report gets saved as a page titled DD/MM/YY - Daily Growth Report, and (this is the important part) each one ends with a short SUMMARY section written specifically to be read by tomorrow's run.

A Notion database of dated daily growth reports, each ending with a summary written for the next day's run

So the workflow became: before doing anything today, fetch the last few days' summaries and use them as the baseline for comparison. The bot now knows it already flagged the iOS onboarding dip on Monday, so on Tuesday it follows up on it instead of "discovering" it again.

The Notion database does three jobs at once:

  • Memory. Yesterday's summary is tomorrow's starting context. This is a basic persistent memory, and it works really well because the summaries are short and high-signal.
  • A correctable record. If the bot reached a wrong conclusion, I (or anyone) can just edit the Notion doc. The next day's run reads the corrected version and moves on. Fixing the bot's understanding is as easy as fixing a typo.
  • A trend log. Because every day is a dated, structured page, the database itself becomes a queryable history of what's been happening to our growth metrics and what we thought was causing it. I can go back and ask "when did we first notice this?" and get a real answer.

Layer 3: guide it with the dashboard, then let it loop

Getting the prompt right here felt a bit like Goldilocks. If I left it too open ("why did adoption drop?") the bot would run with whatever query it thought of first, chase a dead end, and come back differently wrong every day. But locking it down to a fixed set of queries wasn't the answer either, because then it couldn't actually investigate anything, it just filled in a template. I wanted something in between: guide the start, then let it think.

I use dashboards to curate the queries that matter

I needed a way to curate the important queries to guide the bot in its analysis. And, I wanted these to be a maintained, trusted set.

Like I mentioned earlier, I tried to solve this without dashboards at the start and I wrote these queries as code in our codebase. There were three problems with this:

  1. Trying to curate a data story out of a folder of .sql files is terrible. There's no narrative, no ordering, and (call me old-fashioned, but) I couldn't see them. You can't glance at the folder of SQL queries and go "oh, adoption's decreasing."
  2. People wanted to "monitor" the trends themselves. They want to watch metrics through the day as we launch a product, publish an ad campaign, or work through an incident. They also often have context the bot doesn't, so they'll catch a trend or a correlation it misses, but only if they can regularly look at the charts.
  3. I am a data team of one. By making it easy for other people to help, I become much less of a bottleneck for all things data. Almost anyone at the company can update a chart on a dashboard but only a few can open and merge a PR against SQL in our codebase. So the moment the queries lived in code, I was the critical path for every change (no bueno).

Dashboards solved these problems because it's easy to write/update a query as a chart, and a dashboard is curation you can actually see. That's where the important queries ended up living: as charts, grouped into tabs, in our BI tool.

But, Claude can't see a dashboard and it had no idea what a Metabase chart was or what SQL powered it. So I needed a way to bring our dashboards to the bots 🤖.

Bringing that curation to Claude

Once I had this nicely curated set of queries living in dashboards, I did the same thing as in the last post and turned it into a BigQuery table.

Every hour I scrape the Metabase API and dump the SQL behind every chart, across every dashboard, into a table:

SELECT tab_name, card_id, query
FROM `sling-data.dbt_prod.claude_dashboard_queries`
WHERE dashboard_id = 4159
  AND tab_name IN ('3697', '3698', '3699', '4291', '4060')

Why BigQuery again? Same reasoning as the context tables from the last post: any AI system that touches our data already has BigQuery access, so storing the dashboard queries there makes them automatically available to any bot or workflow.

Now my dashboards become a shared vocabulary between the team and the bot.

The dashboard becomes the guide

For the daily report, this means the bot starts by running the queries for the key metrics, then a curated set of deep-dive queries that I built for each one. It's the equivalent of me saying to a new analyst: "Adoption looks off? Start here. Run these. Now, what do you think?"

The report starts from a curated set of deep-dive queries pulled from our dashboards

This is the same idea as the priority system and the big-wide-tables from the last post. I'm not trying to make the model smart in a vacuum, instead I'm handing it the curation I've already done as charts, so it doesn't have to (badly) reinvent it.

Then it loops

The dashboard gets the bot pointed in the right direction but then the actual analytical work (the part that used to eat my mornings) is what happens after the obvious first few queries.

I initially tried the fixed-script approach: "for a drop in adoption, run these five follow-up queries." But this approach runs the same five queries whether or not the first one already answered the question, and it stops running, even when the real cause is the sixth query nobody wrote down. It can't respond to what it finds and that's the difference between single-pass-with-branches and a real loop: a real loop's next step depends on the last result so it has to actually iterate.

Instead of scripting the steps, I described the shape of the loop and made the model run it. For each of the top few trends, independently, the prompt tells it to:

  1. State the trend as a specific, quantified question. "Why did MX 7-day adoption drop from 9.1% to 5.3% between these two dates?"
  2. Run the matching deep-dive tab first. Mandatory, and only then form a first hypothesis.
  3. Form the single most likely hypothesis, then run the cheapest query that could kill it. This is the key instruction: it's told to try to disconfirm, not confirm. Branch out as needed into incidents, feature gates, releases, experiments, paid-spend tables, etc.
  4. Update its confidence and justify the number with real counts. Confidence has to come from actual user and event counts.
  5. Decide what to do next, from the result. Confident enough and backed by enough data? → stop and record it. Not yet, but still has query budget? → form the next hypothesis, explicitly discard the old one and say why it's dead. Out of budget (it gets ~6 queries per trend) and still unsure? → stop and label it INCONCLUSIVE, with the surviving hypotheses and the exact query that would resolve it.

This looping version reasons like an analyst whereas the fixed version reads like a form.

The looping version investigates each trend like an analyst, forming and killing hypotheses

The four things that actually made this work (spoiler: none of them are the model)

If you strip away the Morse-specific details (the tables, the dashboard IDs, the fact that it's fintech) the whole thing comes down to four principles. These are the parts I'd carry to any company, and the parts I'd tell you to steal:

  1. Give it a memory you can correct. The point of the Notion summaries isn't just that Dot remembers yesterday, it's that when it gets something wrong, I (or anyone) can fix the record and tomorrow's run believes the fix.
  2. Curate the starting points, don't script the investigation. Guide the bot to the right first queries (the dashboard), then let it loop and think for itself from there. Too open and it wanders but too scripted and it just fills in a form.
  3. Keep the context where the humans already are. The queries live in dashboards the team already reads. That means the bot and the team reason off the exact same source of truth, and it stays in sync for free because there's only one copy.
  4. Anyone can change anything, without writing code. The prompt lives in a Notion doc and the queries live in a dashboard. If someone on growth wants the report to lead with retention, needs to add a metric, or thinks the tone is too hedgy, they edit the doc or the dashboard, no PR and no me required. As a team of one, this is the one that matters most: if I'm the only person who can change it, it dies the first week I'm on holiday (and I live in France, so I holiday regularly).

The thread running through all four: none of them are about the model. The wins came from creating and curating good context. Which might sound familiar, because it's exactly where I landed at the end of the last post: the hard part of AI for data teams isn't the model, it's the context and the curation around it, and that's a skill data teams have been quietly building for years.

It could all be obsolete in six months when Claude can read a live dashboard directly. That's fine. Same as last time, I've tried to keep every layer loosely coupled and boring to replace. And maybe one day, we will finally see the death of dashboards. But, today is not that day.