# What You’ll Build#
This guide shows a production-ready approach to automated reporting with n8n: a weekly KPI digest that pulls data from GA4, Stripe, and Postgres, aggregates metrics, generates a concise narrative, and sends it to Slack and email on a schedule.
You’ll focus on the parts that make reporting workflows succeed long-term: stable metric definitions, data quality checks, retries, idempotency, and a structure that a teammate can safely change.
# Prerequisites and Assumptions#
You can follow along even if your exact KPIs differ, as long as you can fetch the underlying data.
| Requirement | Recommended | Notes |
|---|---|---|
| n8n | 1.40 or newer | Newer versions improve error handling and credentials UX |
| GA4 access | Analytics Data API enabled | Use OAuth2 or service account depending on org policy |
| Stripe access | Restricted API key | Prefer restricted keys over full secret keys |
| Postgres access | Read-only user for reporting | Add a separate schema for reporting tables if you persist aggregates |
| Slack | Incoming webhook or Slack credential | Webhook is simplest for a single channel |
| SMTP credential or provider node | Use a shared mailbox for business reporting |
ℹ️ Note: GA4 data often lands with a delay. Many teams run “weekly” digests on Tuesday morning for the prior Monday to Sunday window to avoid incomplete counts.
# Step 1: Design the KPI Contract Before You Touch n8n#
The fastest way to create unreliable reporting is to start wiring nodes without defining metrics. Define a KPI contract that answers three questions for every number.
- 1What is the exact definition and source of truth.
- 2What is the reporting window and timezone.
- 3What are the acceptable bounds and failure modes.
A good starting KPI set for a SaaS or subscription product looks like this:
| KPI | Source | Definition | Notes |
|---|---|---|---|
| Sessions | GA4 | Sessions in the reporting window | Ensure same timezone each run |
| Conversions | GA4 | Key event count or purchases | Use explicit event name list |
| New customers | Stripe | Count of customers created | Consider dedupe if you import customers |
| Gross revenue | Stripe | Sum of paid invoices or charges | Decide whether to include refunds |
| Refunds | Stripe | Sum of refunds created | Include partial refunds |
| Active users | Postgres | Users with activity event in window | Depends on your product schema |
| Trial to paid | Postgres plus Stripe | Trials started versus first payment | Often needs joining across systems |
🎯 Key Takeaway: Maintainability starts with stable metric definitions. Treat KPIs like an API: version them, document them, and change them intentionally.
# Step 2: Workflow Architecture for Maintainability#
A weekly digest is easier to maintain when the workflow is split into predictable stages.
Recommended stages#
| Stage | Purpose | Output |
|---|---|---|
| Schedule and window | Define start and end dates | A reportContext object |
| Fetch | Pull raw data from each system | Raw responses stored or logged |
| Normalize | Convert to a shared metric schema | Consistent numeric fields |
| Validate | Data quality checks and completeness | Pass or fail with clear reasons |
| Aggregate | Compute totals, deltas, and ratios | Final KPI object |
| Summarize | Generate a short narrative | Slack-ready text and email body |
| Deliver | Send to Slack and email | Message IDs, deliver status |
| Persist | Optional: store results in Postgres | Weekly snapshot for trends |
Suggested n8n node layout#
Use a single main workflow orchestrating sub-workflows, or keep it in one workflow with clearly labeled sections.
| Pattern | When to use | Pros | Cons |
|---|---|---|---|
| Single workflow | Early stage, fewer metrics | Easier to debug in one canvas | Can grow messy over months |
| Orchestrator plus sub-workflows | Multiple data sources, multiple teams | Reuse, separation of concerns | Needs discipline in inputs and outputs |
If your team expects reporting to expand, start with a template approach and reuse patterns. This complements n8n workflow templates so your automations stay consistent as you add more digests.
# Step 3: Create a Robust Weekly Reporting Window#
The reporting window should be deterministic and timezone-safe. Use ISO dates and compute last full week in a single place so every data source uses the same window.
Window logic#
A common choice is last full Monday to Sunday in the business timezone, then run on Tuesday 08:00.
| Parameter | Example | Why it matters |
|---|---|---|
| Business timezone | Europe Zagreb | Aligns with how stakeholders interpret “weekly” |
| Week start | Monday | Matches common EU reporting |
| Run time | Tuesday 08:00 | Allows for GA4 and Stripe processing lag |
| Window | Prior Mon 00:00 to Sun 23:59:59 | Prevents partial week data |
Here is a compact Code node snippet to compute the last full Monday to Sunday window. Keep it as one of the first nodes, and pass it forward.
// n8n Code node
const tz = 'Europe/Zagreb';
function toISODate(d) {
return d.toISOString().slice(0, 10);
}
// Use UTC dates to avoid local runtime differences, but keep business logic consistent.
const now = new Date();
// Find last Monday 00:00:00 for the previous full week.
const day = now.getUTCDay(); // Sun=0..Sat=6
const daysSinceMonday = (day + 6) % 7; // Mon=0..Sun=6
// Go to this week's Monday, then subtract 7 days to get last week's Monday.
const thisMonday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - daysSinceMonday));
const start = new Date(thisMonday.getTime() - 7 * 24 * 60 * 60 * 1000);
const end = new Date(thisMonday.getTime() - 1); // last millisecond of Sunday
return [
{
reportContext: {
timezone: tz,
windowStartISO: toISODate(start),
windowEndISO: toISODate(end),
windowStartUTC: start.toISOString(),
windowEndUTC: end.toISOString(),
weekLabel: `${toISODate(start)} to ${toISODate(end)}`,
},
},
];⚠️ Warning: Avoid “last 7 days” windows for weekly reports. They drift by day and make week over week comparisons misleading.
# Step 4: Fetch GA4 KPIs with Consistent Dimensions and Filters#
For GA4, use the Analytics Data API. Decide upfront whether you want property-wide metrics or filtered to specific hostnames, paths, or conversion events.
Example GA4 metrics to pull#
| Metric | GA4 metric name | Typical dimension | Notes |
|---|---|---|---|
| Sessions | sessions | date | Baseline traffic |
| Users | totalUsers | date | Useful to sanity-check sessions |
| Key events | eventCount | eventName | Filter to your key events |
| Purchases | purchases | date | If you use GA4 ecommerce |
Use an HTTP Request node to call the GA4 runReport endpoint. Keep the request and the mapping separate so changes to metrics do not break parsing.
# GA4 Analytics Data API endpoint
POST https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport{
"dateRanges": [{ "startDate": "2026-05-19", "endDate": "2026-05-25" }],
"metrics": [{ "name": "sessions" }, { "name": "totalUsers" }],
"dimensions": [{ "name": "date" }]
}Normalize GA4 response#
In a Code node, convert GA4’s string values to numbers and produce a single object.
// Input: GA4 API JSON
const rows = $json.rows || [];
let sessions = 0;
let users = 0;
for (const r of rows) {
const m = r.metricValues || [];
sessions += Number(m[0]?.value || 0);
users += Number(m[1]?.value || 0);
}
return [{
ga4: { sessions, users }
}];💡 Tip: Always log both the request payload and the raw GA4 response when a validation fails. It cuts debugging time significantly when GA4 returns sampled or empty data due to filters.
# Step 5: Fetch Stripe KPIs with Clear Revenue Definitions#
Stripe revenue can mean different things. Pick one definition and encode it as the KPI contract.
Revenue definition options#
| Option | What it measures | Best for | Common pitfall |
|---|---|---|---|
| Charges paid | Money captured | One-time payments | Excludes invoices and some subscription flows |
| Invoices paid | Subscription revenue | SaaS billing | Needs adjustments for credits and proration |
| Balance transactions | Net cash movement | Finance alignment | More complex to reconcile weekly |
For weekly KPI digests, “paid invoices” is often the best compromise for SaaS because it aligns with subscription lifecycle.
Fetch paid invoices within the date window, sum amount_paid, then pull refunds in the same window. Use auto-pagination to avoid missing records when volume grows.
A Stripe node is fine, but HTTP Request gives you explicit control. Keep each call small and bounded.
# List invoices paid during window
GET https://api.stripe.com/v1/invoices?limit=100&status=paid&created[gte]=START_TS&created[lte]=END_TSNormalize to your KPI schema:
const invoices = $json.data || [];
const gross = invoices.reduce((sum, inv) => sum + (inv.amount_paid || 0), 0);
return [{
stripe: {
invoiceCount: invoices.length,
grossRevenue: gross / 100
}
}];Stripe data quality checks#
Stripe is usually consistent, but you still want guardrails.
| Check | Rule | Why |
|---|---|---|
| Currency consistency | All invoices in same currency | Prevents summing EUR and USD |
| Amount sanity | grossRevenue is greater than or equal to 0 | Detects parsing bugs |
| Volume sanity | invoiceCount within expected range | Detects API pagination mistakes |
# Step 6: Pull Product KPIs from Postgres#
Postgres is where you can compute product usage and lifecycle metrics that do not exist in GA4 or Stripe, like weekly active users, signups by plan, or feature adoption.
Use the Postgres node with parameterized queries. Keep the SQL in dedicated nodes and add comments so changes are safe.
Example KPIs from a typical product schema#
| KPI | Example query target | Notes |
|---|---|---|
| Weekly active users | events table | Count distinct user IDs in window |
| New signups | users table | Filter by created_at |
| Trials started | subscriptions table | Filter by trial start |
| Activation rate | derived | Activated divided by signups |
Example query for weekly active users:
SELECT
COUNT(DISTINCT user_id) AS wau
FROM events
WHERE occurred_at >= $1
AND occurred_at <= $2;In n8n, pass windowStartUTC and windowEndUTC from reportContext.
ℹ️ Note: If your DB stores timestamps without timezone, standardize conversions at the query boundary. Reporting bugs often come from mixing UTC and local time in comparisons.
# Step 7: Normalize Everything into One KPI Object#
Once each source returns metrics, merge them into a single object with consistent naming and units.
A practical normalized schema:
| Field | Type | Example | Source |
|---|---|---|---|
weekLabel | string | 2026-05-19 to 2026-05-25 | Context |
sessions | number | 12450 | GA4 |
users | number | 8920 | GA4 |
grossRevenue | number | 18350.25 | Stripe |
refunds | number | 320.00 | Stripe |
wau | number | 1560 | Postgres |
signupCount | number | 210 | Postgres |
In n8n, use a Merge node in “merge by position” if you keep one item per branch, then a Code node to produce a final object.
const ctx = $json.reportContext;
const ga4 = $json.ga4 || {};
const stripe = $json.stripe || {};
const pg = $json.pg || {};
const kpis = {
weekLabel: ctx.weekLabel,
sessions: Number(ga4.sessions || 0),
users: Number(ga4.users || 0),
grossRevenue: Number(stripe.grossRevenue || 0),
invoiceCount: Number(stripe.invoiceCount || 0),
wau: Number(pg.wau || 0),
};
return [{ kpis, reportContext: ctx }];# Step 8: Add Data Quality Checks That Prevent Bad Reports#
Automated reporting fails quietly when upstream systems return partial data. Add explicit checks and stop the workflow before sending anything misleading.
Practical validation rules#
| Rule | Example threshold | Action if failed |
|---|---|---|
| Required fields present | sessions, grossRevenue, wau | Fail and alert |
| Non-negative metrics | all numeric KPIs greater than or equal to 0 | Fail |
| GA4 completeness | users greater than 0 on non-holiday weeks | Warn or fail |
| Stripe completeness | invoiceCount consistent with historical baseline | Warn |
| Spike detection | week over week change greater than 50 percent | Send with warning banner |
| Data freshness | window end is at least 24 hours behind now | Fail if too recent |
Implement validation in a Code node that emits either a status: ok or throws an error. Errors should go through your centralized retry and alerting logic.
const k = $json.kpis;
const errors = [];
if (k.sessions <= 0) errors.push('GA4 sessions is 0 or missing');
if (k.grossRevenue < 0) errors.push('Stripe grossRevenue is negative');
if (k.wau <= 0) errors.push('Postgres WAU is 0 or missing');
if (errors.length) {
throw new Error(`KPI validation failed: ${errors.join(' | ')}`);
}
return [{ ...$json, validation: { status: 'ok' } }];⚠️ Warning: Do not “send anyway” on validation failure. Stakeholders lose trust quickly when you distribute wrong numbers, and it is hard to rebuild confidence.
For a deeper pattern on retries and notifications, align this workflow with your standard approach to n8n error handling, retries, and alerting.
# Step 9: Add Retries and Idempotency so Weekly Runs Are Safe#
Weekly reporting is a perfect candidate for structured retry logic because failures are usually transient: GA4 API hiccups, Stripe rate limits, or a short Postgres maintenance window.
Recommended retry strategy by source#
| Source | Common failure | Retry approach | Backoff |
|---|---|---|---|
| GA4 API | 429 rate limit, 5xx | Retry 3 times | 30s, 2m, 5m |
| Stripe | 429, network timeouts | Retry 3 times | 30s, 2m, 5m |
| Postgres | connection timeout | Retry 2 times | 30s, 2m |
| Slack or email | transient network | Retry 2 times | 30s, 2m |
If you implement retries in n8n at node level, ensure you also track whether a report has already been delivered.
Idempotency pattern#
Store a weekly report record in Postgres with a unique key for the week. Only deliver if the record is not marked as delivered.
| Column | Type | Example |
|---|---|---|
week_start | date | 2026-05-19 |
week_end | date | 2026-05-25 |
payload | jsonb | normalized KPIs and summary |
delivered_at | timestamptz | null or timestamp |
delivery_channel | text | slack, email |
Then, the workflow can do this:
- 1Compute window.
- 2Check if record exists and delivered.
- 3If delivered, stop.
- 4If not delivered, compute KPIs and summary, upsert record.
- 5Deliver.
- 6Update
delivered_at.
This prevents duplicate Slack messages when you replay executions.
# Step 10: Generate a Concise Summary That Humans Actually Read#
Executives and product leads want the story, not a wall of numbers. Your summary should fit in one Slack message without scrolling on mobile.
Summary structure that works#
| Section | Content | Max length |
|---|---|---|
| Header | Week label, high-level outcome | 1 line |
| Growth | Week over week deltas for key KPIs | 3 to 5 bullets |
| Risks | Anomalies or missing data notes | 1 to 2 bullets |
| Next actions | One suggestion based on data | 1 bullet |
Compute deltas if you persist last week’s KPIs in Postgres. If you do not persist, you can still compute comparisons by pulling last week’s window in parallel, but storing aggregates is simpler and faster.
Example Code node to build Slack text:
const k = $json.kpis;
const week = k.weekLabel;
const lines = [];
lines.push(`Weekly KPI Digest: ${week}`);
lines.push(`Sessions: ${k.sessions.toLocaleString()} | Users: ${k.users.toLocaleString()}`);
lines.push(`Revenue: €${k.grossRevenue.toFixed(2)} from ${k.invoiceCount} invoices`);
lines.push(`WAU: ${k.wau.toLocaleString()}`);
lines.push(`Notes: Reply to this message to request a breakdown by channel or plan.`);
return [{ ...$json, message: { slackText: lines.join('\n') } }];💡 Tip: Keep a single “metric formatting” helper in one Code node so you do not duplicate formatting logic across Slack and email.
# Step 11: Send to Slack and Email on a Schedule#
Use a Schedule Trigger node for weekly runs. Then send messages through Slack and email nodes in parallel, but only after validations pass.
Slack delivery#
For Slack, prefer a single channel like #weekly-kpis and a consistent message format. This makes search and comparisons easy.
| Delivery detail | Recommendation | Why |
|---|---|---|
| Channel | one dedicated channel | reduces noise elsewhere |
| Threading | optional, start a thread for drilldowns | keeps channel clean |
| Attachments | avoid heavy formatting | reduces rendering issues |
| Links | include dashboard links | enables self-serve follow-up |
Email delivery#
Email is useful for leaders who do not live in Slack. Keep it short, with a link to dashboards or a longer report.
| Email field | Recommendation |
|---|---|
| From | ops or analytics mailbox |
| To | a group alias |
| Subject | Weekly KPI Digest: YYYY-MM-DD to YYYY-MM-DD |
| Body | same text as Slack plus links |
# Step 12: Observability and Maintenance Practices#
The difference between a demo workflow and a production workflow is observability: you should know when it fails, why it fails, and whether it sent duplicates.
Minimal operational checklist#
| Area | What to implement | Effort |
|---|---|---|
| Logs | store raw API responses for failed runs only | low |
| Alerting | send an error summary to an ops channel | low |
| Versioning | keep KPI definitions in one place | medium |
| Ownership | add an “owner” field and runbook link in workflow description | low |
| Secrets hygiene | restricted keys, rotate credentials | medium |
| Data drift | weekly anomaly checks | medium |
A good next step is standardizing reporting workflows with templates and conventions from n8n workflow templates, so each new digest follows the same stages and quality controls.
If you want this implemented end-to-end for your stack, including dashboards and governance, this is exactly what we deliver at Samioda Automation.
# Key Takeaways#
- Define a KPI contract first: source, window, timezone, and definition for every metric.
- Build the workflow in stages: fetch, normalize, validate, aggregate, summarize, deliver, and optionally persist.
- Add explicit data quality checks and fail fast instead of sending incomplete or misleading numbers.
- Implement retries with backoff per source and add idempotency to prevent duplicate deliveries.
- Keep summaries short and consistent: one Slack message, one email, with a link for deeper drilldowns.
- Improve maintainability by centralizing metric definitions, formatting helpers, and reusable workflow templates.
# Conclusion#
Automated reporting with n8n works best when you treat it like a production system: deterministic windows, stable KPI definitions, validation gates, and safe delivery with retries and idempotency. Once the weekly digest is reliable, you can extend the same pattern to daily anomaly alerts, cohort reporting, and pipeline monitoring without rebuilding from scratch.
If you want a maintainable reporting workflow tailored to your GA4, Stripe, and Postgres schema, we can design and implement it quickly, including error handling, alerting, and handover documentation. Reach out via Samioda Automation and we’ll help you ship reporting your team can trust.
FAQ
More in Business Automation
All →n8n + Supabase/Postgres Automation Patterns: Webhooks, RLS-Safe Writes, and Reliable Sync
A practical guide to n8n Supabase Postgres automation patterns: webhook ingestion, idempotency keys, upserts, RLS-safe writes, and reliable two-way sync for SaaS back-office workflows.
Building AI Agent Workflows in n8n: RAG, Tool Use, and Guardrails for Production
A practical end-to-end guide to an n8n AI agent RAG workflow: ingest documents, chunk and embed, store in a vector DB, query with an LLM, and ship safely with PII controls, prompt-injection defenses, cost limits, and human approvals.
n8n Web Scraping & Change Detection: Monitor Pages, Detect Updates, and Trigger Workflows Reliably
A practical 2026 guide to n8n web scraping change detection monitoring: fetch and parse HTML, normalize content, detect meaningful updates with hashing and diffing, avoid false positives, and route alerts to Slack or Email reliably.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
n8n Web Scraping & Change Detection: Monitor Pages, Detect Updates, and Trigger Workflows Reliably
A practical 2026 guide to n8n web scraping change detection monitoring: fetch and parse HTML, normalize content, detect meaningful updates with hashing and diffing, avoid false positives, and route alerts to Slack or Email reliably.
Building an n8n Approval Workflow in 2026: Slack or Teams, Email, and Audit Trails
Learn how to build a production-ready n8n approval workflow with human-in-the-loop approvals, timeouts, reminders, escalation paths, and audit logging to prevent duplicate decisions.
n8n + Supabase/Postgres Automation Patterns: Webhooks, RLS-Safe Writes, and Reliable Sync
A practical guide to n8n Supabase Postgres automation patterns: webhook ingestion, idempotency keys, upserts, RLS-safe writes, and reliable two-way sync for SaaS back-office workflows.