Skip to content

Founder-led. Limited engagements.Talk to Us →

Stop Logging Into 5 Dashboards: Building a One-Slack-Message KPI Bot

How we replaced five daily dashboard logins with one Slack message: a cron worker that polls Stripe, PostHog, and SQL, computes KPIs, and flags anomalies.

By CodeITronics8 min readInternal Tools · Slack Automation · KPI Reporting · Case Study
Stop Logging Into 5 Dashboards: Building a One-Slack-Message KPI Bot

Every morning, the founders we built this for opened five tools to answer one question: how is the business doing? Revenue in Stripe, product usage in PostHog, signups and activity in the database, and a couple of other tabs for good measure. Every one of those logins pulled attention away from real work, and the numbers still had to be stitched together in someone's head.

We replaced that ritual with a consolidated KPI reporting bot: a scheduled worker that polls Stripe, PostHog, and the SQL database, computes a fixed set of metrics, flags anything unusual, and posts one Slack message each morning. The result was zero daily dashboard logins. Here's how it's built and the decisions that made it trustworthy enough to replace the dashboards.

Why dashboards stop getting checked properly

Dashboards are built for exploration. They're great when you have a question and time to dig. They're poor at the daily check-in, which is a different job: "Is anything off today, and do I need to act?"

When the daily check-in requires five logins, three things happen. It takes too long. It gets skipped on busy days, which are exactly the days something breaks. And the comparison is done mentally: today's revenue in one tab, yesterday's signups remembered from another, and no consistent baseline to compare against.

The real requirement wasn't another dashboard. It was a short, reliable answer delivered where the founders already spend their day.

Architecture

   +-----------+   +-----------+   +------------------+
   |  Stripe   |   |  PostHog  |   |  Product SQL DB  |
   +-----------+   +-----------+   +------------------+
         \               |                 /
          \              |                /
           v             v               v
        +------------------------------------+
        |  Cron worker (daily, fixed time)   |
        |  1. fetch raw data per source      |
        |  2. compute metrics from defs      |
        |  3. store snapshot                 |
        |  4. compare to baselines           |
        |  5. build + post message           |
        +------------------------------------+
               |                     |
               v                     v
      Metrics store (SQL)      Slack (one message,
      daily snapshots,         thread for details)
      run log
ComponentResponsibility
Source connectorsRead-only access to Stripe, PostHog, and the SQL database
Metric definitionsOne place that says exactly how each number is computed
Cron workerRuns the pipeline on schedule, with retries and a run log
Snapshot storeKeeps daily values so comparisons don't depend on live APIs
Anomaly checksCompares today against recent history and flags outliers
Slack formatterTurns the numbers into a short, scannable message

There's no LLM in the critical path. This is a deliberately boring system: fetch, compute, compare, post. That's what makes it trustworthy.

Data sources

Stripe

Stripe is the source of truth for money: new subscriptions, cancellations, failed payments, and revenue. The connector uses read-only API keys and pulls events and objects for the reporting window. Revenue metrics are computed from Stripe data directly rather than from an internal copy, so the number in Slack matches what finance sees.

PostHog

PostHog provides product analytics: active users, key feature events, and funnel steps. The worker queries specific events and counts for the reporting day, rather than scraping a dashboard view, so the definitions are explicit and version-controlled.

The product database

Some numbers only exist in the application's own data: signups, accounts created, workspaces activated, records created. These come from read-only SQL queries against a replica or a read-only user, never from the primary with write access.

The scheduled worker

The worker is a single scheduled job that runs once a day before the founders start work. A few properties matter more than the scheduling mechanism itself:

  • Idempotent runs. Each run is keyed by date. Re-running for the same day overwrites that day's snapshot and edits the existing message rather than posting a duplicate.
  • Per-source isolation. If PostHog's API is slow or down, the Stripe and SQL metrics still compute. The message shows the missing section as unavailable instead of failing silently or not posting at all.
  • Retries with limits. Transient API errors get a few retries with backoff, then the run proceeds with what it has.
  • A run log. Every run records start time, per-source status, and any errors, so a wrong or missing number can be traced.
  • Fixed time windows. Each metric uses an explicit window in one time zone, usually the previous full day. Mixing UTC from one source with local time from another is the fastest way to produce numbers that don't match anything.

Metric definitions

The most valuable artifact in this project isn't code that calls APIs. It's the list of metric definitions.

"Active users" can mean five different things depending on who you ask. Before building, each metric was pinned down: what it counts, from which source, over what window, and what is excluded. Those definitions live in one config file the worker reads.

- key: new_mrr
  label: New MRR
  source: stripe
  window: previous_day
  definition: sum of first-period recurring amount for subscriptions created in window
  format: currency

- key: active_users
  label: Active users
  source: posthog
  window: previous_day
  definition: distinct users with at least one core_action event
  format: integer

- key: signups
  label: Signups
  source: sql
  window: previous_day
  definition: users created in window, excluding internal and test domains
  format: integer

Keeping definitions in one place does a few things. Everyone reads the same number the same way. Changing a definition is a reviewed change, not a silent edit. And adding a new metric means adding a definition, not writing a new integration.

We kept the list short on purpose. A daily message with thirty numbers is a dashboard in disguise, and it gets skimmed the same way. The test for including a metric was simple: would a change in this number make a founder do something differently today?

Anomaly flags

A list of numbers still requires interpretation. The anomaly checks do the first pass of that interpretation, so the message can say "this one looks unusual" instead of leaving the reader to remember what normal looks like.

Each metric is compared against its recent history. A simple, explainable approach works well here:

  • Compare today's value to the same weekday over recent weeks, since many products have strong weekly patterns.
  • Flag the value if it falls outside a band around that baseline.
  • Use per-metric sensitivity. Failed payments might warrant a flag at a small change, while signups vary naturally and need a wider band.
  • Require a minimum absolute change too, so tiny numbers don't trigger large percentage swings.

We avoided complex models for this. When a flag appears, the founder needs to understand why in a second: "Signups are well below the usual Tuesday." An explainable rule beats a black box that's slightly more accurate.

Flags are also limited in number. If everything is flagged, nothing is. When there are many anomalies at once, that usually points to a data problem, like a broken tracking event or a failed source, and the message says so.

Designing the Slack message

The message is the product. If it's hard to read, people go back to the dashboards.

The structure we settled on:

Daily KPIs for Mon 12 May

Revenue     New MRR  ▲ vs last 4 Mondays
            Failed payments  ⚠ above usual range
Product     Active users  ▬ in usual range
            Key feature usage  ▲
Growth      Signups  ▼ below usual range

Flags (2): failed payments up, signups down
Details and source links in thread

The values themselves sit next to each label in the real message. The design rules:

  • One message, one screen. The top-level message fits without scrolling. Breakdowns and links to the underlying tools go in a thread reply.
  • Grouped by question. Revenue, product, and growth sections map to the questions founders actually ask.
  • Direction and context, not just values. Each metric shows the change against its baseline, so the reader doesn't need yesterday's number in their head.
  • Flags summarized at the bottom. A skim of the last lines tells you whether today needs attention.
  • Links for digging. When something looks off, the thread links straight to the relevant Stripe, PostHog, or query view. The dashboards still exist. They're just used for investigation, not for checking.

What needed care

Matching the source of truth

If the Slack number disagrees with Stripe, trust disappears immediately. Before the bot replaces anything, its outputs should be compared against the source tools for the same windows until they match, with any intentional differences, such as excluding test accounts, documented.

Silent failures

A bot that quietly stops posting is worse than no bot, because people assume no news is good news. The worker posts a short failure notice if a run fails, and a missing source is shown explicitly in the message.

Security

Every connector uses read-only credentials scoped to what the worker needs. Secrets live in the environment, not the code. The Slack app posts to one channel and doesn't read messages.

Results

The published result for this build is zero daily dashboard logins. The founders get one Slack message each morning with the metrics that matter, compared against a baseline, with anything unusual flagged. The dashboards are still there for investigation, but the daily check-in no longer requires opening them.

Key Takeaways

  • The daily check-in and deep analysis are different jobs. A message serves the first better than a dashboard.
  • Write metric definitions down in one place before building anything. They are the real product.
  • Keep the pipeline boring: fetch, compute, compare, post. Determinism builds trust.
  • Use simple, explainable anomaly rules, and limit how many flags appear.
  • Design for failure visibly. A missing number should say it's missing.

FAQs

Why not just use a BI tool with scheduled email reports?

That can work. The difference is control over definitions, anomaly logic, and message design, plus delivery in Slack where the team already works.

Does this use AI?

Not in the critical path. The numbers come from deterministic queries. An LLM could optionally add a one-line narrative, but it should never compute or alter the metrics.

How do you add a new data source?

Write a read-only connector that returns raw data for a window, then add metric definitions that use it. The scheduling, storage, comparison, and formatting stay the same.

What if a data source is down when the job runs?

The other sections still post, and the missing one is marked unavailable. The run log records the failure, and the job can be re-run for that date without duplicating the message.

How many metrics should the daily message have?

As few as possible. If a metric changing wouldn't change what someone does today, it belongs in the dashboard, not the message.

Working on Something Similar

If your team starts each day by logging into several tools to check the same handful of numbers, this is a small, high-leverage system to build. CodeITronics builds internal tools, integrations, and automation like this one. See more builds on our work page, or get in touch to talk through your reporting.

Keep reading

40% of Support Tickets, Zero Humans: Inside an AI Triage System

Inside our AI support triage system: knowledge-base grounding, intent detection, and escalation logic that auto-resolves 40% of tickets and routes the rest.

How an Autonomous Lead-Gen Agent Delivered 4x Outbound Volume

How our lead-gen pipeline with Puppeteer, AI agents, HubSpot, and Postgres removed manual SDR research and delivered a 4x increase in outbound volume.

Your Inbox Doesn't Need Another App — It Needs an Agent

How we built an AI inbox agent that classifies, summarizes, and routes email into tasks, with confidence thresholds and human review to keep it trustworthy.

Subscribe to the edition

One useful systems note a week from real Salesforce and automation builds. Unsubscribe in one click.

Letters

Letters to the editor

No letters yet. Questions and counterpoints are welcome.