← All articles

GTM Engineering

Stop Building Dashboards. Nobody Opens Them.

My forecast dashboard got opened four times a month, three of them me. The same numbers in a Slack digest got replies inside the hour. Push beats pull. Here is the build order that turns 93 static rows into one decision.

· 13 min read

I built a forecast dashboard the whole team asked for. Beautiful. Real-time. It got opened, on average, four times a month, and three of those were me checking it still rendered. The behavior it was supposed to change did not change. Then I killed it, put the same numbers in a Slack message that arrived three times a day whether anyone asked or not, and reps started replying inside the hour.

Push beats pull. A dashboard is pull: it sits and waits for someone to remember it exists, log in, pick the right filters, and read it correctly. That is a stack of steps you are asking a busy person to take on their own initiative, every day, forever. They won’t, and the miss is not mine alone. Gartner has pegged business-intelligence and analytics adoption at roughly a third of the intended users for a decade, and that ceiling barely moves no matter how good the tool gets (Gartner, analytics adoption surveys). An alert inverts the work. It shows up in the channel the rep already has open, states the one thing that changed, and asks for a decision. The gap between those two models is the difference between a tool touched four times a month and one touched every day.

Watch what the inversion does to a single forecast. Ninety-three open rows, forty widgets, all of it rendered all the time, collapses down to the three rows that moved and the one decision a manager can act on today. The dissolve below is the whole argument before I explain a line of it.

From everything-all-the-time to one decisionThe dashboard dissolves into the alert that gets acted on
40 widgetsWidgets renderedA real-time dashboard paints every widget, every load, whether or not anything changed.
4 / month
Times the dashboard was opened, three of them me
under 1 hr
Reply time on the Slack digest that replaced it
47 vs 4
Digest replies vs dashboard opens, same month, same data

The report that changes behavior arrives unprompted

The mistake is thinking the artifact is the value. It isn’t. The value is a decision made at the moment it can still matter. A dashboard delivers information to people who go looking, almost nobody goes looking, and the ones who do go looking after the quarter is already decided. RevenueHero tested this cost in reverse on the inbound side: across 1,000 B2B sites in 2025, 63.5 percent never responded to a submitted lead at all, and the average first response took 29 hours (RevenueHero, 2025 speed-to-lead study). A decision delivered late is a decision not made. Pull builds latency into every step by design, because every step waits on a human remembering to take it.

So invert it. Do not render a chart and hope. Detect the thing worth acting on and send it to where the person already is. The whole architecture is one loop: snapshot, diff, filter to exceptions, deliver.

The inversion Pull dies at every step; push delivers a decision
Pull (dashboard)rememberlog infilterspot changePush (digest)snapshotdifffilterdecide
Every arrow on the pull path is a step a busy person has to take on their own initiative. The push path takes zero.

The forecast-movement digest, worked

Here is the one that replaced my dead dashboard, and it reconciles straight to the dissolve above.

A cron runs three times a day at fixed times: mid-morning, early afternoon, late afternoon. I picked fixed times over polling because a human’s day has a rhythm, and a message that lands at 9:40 every morning becomes a habit, while a message that lands at some random interval becomes noise you learn to ignore. Each run pulls the current forecast snapshot and diffs it against the prior stored snapshot.

The diff is a query, not magic. Pull today’s open pipeline with the fields that can move, store it, and next run compare:

-- Snapshot the fields that can change, keyed by opp
SELECT Id, Name, Account.Name, ForecastCategoryName,
       Amount, CloseDate, StageName
FROM Opportunity
WHERE IsClosed = false
  AND CloseDate = THIS_QUARTER

Then it sends, per manager, only the deltas. Not the whole pipeline. The message is short:

:rotating_light: Forecast movement, 9:40 AM
• Acme: Commit to Best Case, -$180K
• Two deals slipped close date past quarter end, $340K combined
• Beta Corp: advanced to Verbal, +$95K

Three lines, all movement, no static state. Nothing that didn’t change gets rendered, because a number that didn’t change is not news, and printing it trains people to skim past the ones that did.

The dashboard showed the same data. But the dashboard showed all of it, all the time, and the reader had to spot the three rows that moved among the ninety that didn’t. That is the middle stage of the dissolve, 93 rows the human has to scan unaided. The digest does the diffing for them and delivers only the exceptions, which is the third stage, three rows that survived. That is the whole trick.

Storing the snapshot is the part people skip, and it is what makes the diff possible. A dashboard is stateless: it renders whatever is true right now and has no memory of what was true this morning, so it can only tell you what is, never what changed. The digest is stateful. Every run writes the current state somewhere durable (a table, a small object, even a JSON blob), and the next run reads it back and subtracts. That subtraction is the entire product. If you find yourself wanting to add a trend line to the message, stop: a trend line is you rebuilding the dashboard because you did not trust the diff. Trust the diff. If a number is on the trend line but did not move since the last run, it is not news this run.

There is a subtle discipline in what you diff against. Diff against the last run of the same kind, not against an arbitrary baseline. If the 9:40 digest diffs against yesterday’s 9:40 and the 1:00 digest diffs against today’s 9:40, each message answers “what changed since you last heard from me,” which is the only question the reader is holding. Diff against a fixed quarter-start baseline instead and every message re-reports the same cumulative movement, a slow drift back toward the everything-all-the-time dashboard you were trying to kill.

Timing is inside the alert, the way freshness is inside a signal

A report is worth the most the instant the thing it describes happens, and it decays from there. Woodpecker’s analysis of 26,000 outbound campaigns found a fresh trigger replies near 22 percent at 72 hours, falls to about 11 percent at 7 days, and drops to roughly 7 percent at 14 days (Woodpecker, 26,000-campaign study). The mechanism is the same for an internal alert. A deal that slipped this morning is a decision a manager can still influence at 9:40. The same fact surfaced in a dashboard someone opens next Thursday is an autopsy. Push wins on timing because it fires on the event, and pull loses on timing because it fires on someone remembering to look, which happens on nobody’s schedule.

This is why the fixed cadence matters more than it looks. Three sends a day is not about volume, it is about landing the change while a person can still act on it, and about building a rhythm the reader trusts enough to open. A digest that arrives at a predictable time, carries only movement, and stays silent when nothing moved earns a standing habit. A dashboard earns a bookmark that never gets clicked.

Exception-only, or you’ve rebuilt the dashboard in Slack

The failure mode here is real and I have hit it: you get excited about push, and you start pushing everything. A daily message with the full pipeline table in it is a dashboard someone pasted into Slack. It gets muted in a week.

The discipline is exception-only. Send the deal that slipped. Send the account that crossed a health threshold. Send the forecast category that changed. Do not send “here is the current state of everything,” because current state is exactly what nobody reads. If nothing crossed a threshold, the right message is no message. Silence is a signal too: it means nothing needs a decision right now, which is worth more than a green dashboard that says the same thing in forty widgets.

Line in the digestImplied verbKeep it?
”Acme slipped close date past quarter end”Re-engage or move it outYes
”Health score dropped below 40”Go look at the accountYes
”Deal advanced to Verbal, +$95K”Confirm and tighten close planYes
”Pipeline total is $4.2M”None, it is a standing numberNo
”12 deals in Commit”None, it did not changeNo

A good rule: every line in the alert should end in an implied verb, something the reader could do about it. If a line doesn’t imply an action, it is decoration, and decoration belongs in a dashboard nobody opens.

The threshold is where exception-only lives or dies. “Send the account that crossed a health threshold” is only useful if the threshold sits where a human would move to act. Set it too tight and every account crosses it every day, so you have rebuilt the noise. Set it too loose and the one account that mattered never trips it. I tune thresholds by looking backward: pull the last quarter of the events a manager should have acted on, find the value they crossed, and set the threshold just inside that. The test is not “is this technically a change.” The test is “would a good manager have wanted a nudge here.” Everything that passes ships. Everything that only passes “is technically a change” is the ninety static rows the dashboard already buried.

Monthly engagement: dashboard vs exception digest
Same numbers, same audience. The only thing that changed is who does the work of finding the exception.
View as table
ItemValue
Dashboard opens4
Digest replies47
Actions taken31

What each model costs the reader

The clean way to see why push wins is to price the work each model pushes onto the person, because attention is the scarce budget in a pipeline review, not data. The dashboard makes the reader do the detection. The digest does the detection and hands over the decision. The rows below hold the audience and the data constant so the delivery model is the only variable.

Task in the loopDashboard (pull)Exception digest (push)
Remember it existsReader, every daySystem, on a schedule
Get to the dataReader logs in, filtersLands in an open channel
Find what changedReader scans 93 rowsDiff returns 3
Decide what to doReader, if they got this farReader, first thing they see
Cost when nothing changedA full green screen to readNo message

Every row the dashboard column assigns to the reader is a place the loop can stall, and it stalls at the first one, remembering. The digest column assigns four of five steps to the machine and leaves the reader the single step no machine should own: the decision. That reallocation is the entire adoption gap, 47 replies against 4 opens, sitting in one table.

Before and after, on the same forecast

The dashboard The exception digest
Delivery Pull, user must go find it Push, lands where they already are
Content All 93 rows, all the time Only the rows that moved
Cadence Whenever someone remembers 3x/day at fixed times
When nothing changed Still a full green screen No message, which is itself a signal
Monthly engagement 4 opens, mostly me 47 replies, 31 actions
The data source is identical. The delivery model is the entire difference in adoption.

What to do Monday

Kill one dashboard, ship one digest
  1. 1

    Pick your most-loved, least-opened dashboard

    The one the team asked for and nobody uses. Check its open count first so you have the before number to beat.

  2. 2

    Name the one or two decisions it was supposed to drive

    Usually it is "a deal moved category" or "a number crossed a line." Everything else on the dashboard is context, not a decision.

  3. 3

    Write the snapshot query and store the result

    Pull only the fields that can move. Save each run so the next run has something to diff against. Stateless cannot diff.

  4. 4

    Diff against the last run of the same kind and filter to exceptions

    Emit only rows where a watched field changed or crossed a threshold. If zero rows changed, emit nothing.

  5. 5

    Send to the channel that team already lives in, 3x/day at fixed times

    Fixed times build the habit. Every line must end in an implied verb or it does not ship.

This is the same push-over-pull instinct behind a forecast digest that diffs snapshots and the routing-is-latency argument that a decision delivered late is a decision not made. It also pairs with the automation ledger, because a digest that reliably drives action is exactly the kind of saved-time you should be recording. The artifact was never the dashboard. It was the decision, and the decision only happens if it arrives before the person has to go looking.

Then watch the dashboard’s open count drop toward zero and stop caring, because the decisions it was supposed to drive are now getting made in Slack before lunch.

reporting slack adoption

Keep reading

One email. Every week.

One email a week: a system I built or broke, with the config, the numbers, and what I would change. No roundups, no theory, unsubscribe whenever it stops being useful.

The newsletter opens soon.

Connect a provider in src/config.ts