Automation
Build Log: Real-Time KPI Dashboard via Webhook-Triggered Pipeline
A client was pulling from four data sources manually twice a day to update their reporting dashboard. Here's how I replaced that with a webhook-driven pipeline that updates the moment a conversion happens.
The client ran a B2B lead generation operation — paid ads, outbound sequences, and affiliate partners — across four separate platforms. Every morning and every afternoon, their ops manager opened all four dashboards, pulled numbers into a spreadsheet, and manually updated the reporting Google Sheet that the leadership team relied on for decisions.
Two pulls a day. Four platforms. One spreadsheet. One person spending 45 minutes on it, twice, every working day.
The brief: make it automatic and make it real-time.
Data Sources
- Facebook Ads Manager — campaign spend and conversion events
- GoHighLevel — contact creation, deal stage changes, and call logs
- Google Ads — keyword performance and conversion tracking
- Stripe — actual revenue events (completed payments)
The challenge: these four platforms have different APIs, different authentication patterns, different rate limits, and different data shapes. Unifying them into a single reporting schema is the core of the problem.
Architecture Decision: Push vs. Pull
The original manual process was a pull model — someone goes to each platform and retrieves data. The obvious automated equivalent is a scheduled pull: an n8n workflow that runs every hour and pulls from all four APIs.
I went with a hybrid instead:
- Stripe and GoHighLevel: webhook-driven (push). Both support outbound webhooks on key events. Stripe fires on payment completion; GHL fires on contact creation and deal stage changes.
- Facebook and Google Ads: scheduled pull (every 4 hours). Neither has real-time webhooks for the level of granularity we need. Hourly polling would have worked but created rate limit pressure.
The result: revenue and CRM events update within seconds of happening. Ad spend and performance metrics refresh every four hours. For the client's actual decision-making cadence, this was accurate enough.
The Normalisation Layer
The four data sources return very different shapes. Stripe gives you an amount in cents with a currency code. Facebook Ads gives you spend with a breakdown by ad set. GoHighLevel gives you a contact object with 30+ fields. Google Ads gives you keyword-level impressions, clicks, and conversions.
I built a normalisation workflow in n8n that sits between each data source and the central database. Each normaliser takes the raw payload from its source and outputs a standardised event record:
{
"event_type": "revenue|lead|conversion|spend",
"source": "stripe|ghl|facebook|google",
"amount": number | null,
"timestamp": ISO8601,
"metadata": {}
}The metadata field carries source-specific context — ad set name, contact ID, keyword, campaign — that matters for drill-down but doesn't need to be in the core schema.
This layer was the part that took the most iteration. Facebook's API returns spend aggregated by time window, not per-event, so I had to decide how to handle the delta between successive pulls — tracking last-seen totals per campaign and computing incremental changes.
The Database
Everything flows into a Postgres database with five tables:
events— the normalised event streamdaily_aggregates— pre-computed daily totals per source and event typecampaigns— Facebook and Google campaign metadatacontacts— GHL contact recordskpi_snapshots— point-in-time snapshots for trend analysis
The daily_aggregates table is recomputed every night by a scheduled n8n workflow. This pre-computation is what makes the dashboard fast — the frontend doesn't compute aggregates at query time.
The Dashboard
Rather than build a custom frontend, I pointed a Metabase instance at the Postgres database. The client's leadership team gets:
- Live KPI tiles — today's revenue (Stripe), leads created (GHL), ad spend (Facebook + Google), cost per lead
- Trend charts — 30-day rolling for each KPI with 7-day moving averages
- Campaign breakdown — spend vs. conversions per Facebook and Google campaign, updated every 4 hours
- Pipeline view — deal stage distribution in GHL, updated in real time
Metabase's question builder lets the ops manager create their own ad-hoc views without needing to touch the database. That was a specific request — the client wanted to reduce their dependency on me for routine reporting changes.
Error Handling
The webhook flows have a dead-letter queue: if the normalisation step fails (malformed payload, unexpected field names after an API update), the raw payload is stored with an error flag. A daily digest email shows any failed events so they can be manually reviewed and reprocessed.
The scheduled pulls have retry logic: if Facebook or Google Ads returns a rate limit error, the workflow waits 5 minutes and retries. After three retries, it alerts via Slack and marks the pull as failed.
Result
The ops manager's twice-daily manual process is gone. Revenue events appear on the dashboard within 5 seconds of Stripe processing the payment. Lead events appear within 10 seconds of a contact being created in GHL. Ad performance refreshes every 4 hours instead of twice a day.
The client estimates they recovered about 7.5 hours per week across the ops team (the pull itself, plus questions that would have required a manual check). The pipeline runs unattended and has handled two Facebook API version upgrades without breaking — both changes were non-breaking to the fields we use, and the normalisation layer absorbed the minor shape changes.
Related Reading
Build Log
Why AI Wrappers Won't Kill Your n8n Workflows (and When to Go Agentic)
A practical decision playbook for ops leaders facing the agentic coding vs. n8n debate.
Build Log
Full-Service AI Automation Is Now a Billable Engine: The Agency Owner's Playbook
How agency owners can package intake-to-follow-up AI automations like the n8n law firm build — and keep token costs low with state-tracking.