Business Automation
n8nAutomationReportingGA4StripePostgreSQLSlackEmailData Quality

Automated Reporting with n8n: Build Weekly KPI Digests from GA4, Stripe, and Postgres

AO
Adrijan Omićević
·17 min read

# 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.

RequirementRecommendedNotes
n8n1.40 or newerNewer versions improve error handling and credentials UX
GA4 accessAnalytics Data API enabledUse OAuth2 or service account depending on org policy
Stripe accessRestricted API keyPrefer restricted keys over full secret keys
Postgres accessRead-only user for reportingAdd a separate schema for reporting tables if you persist aggregates
SlackIncoming webhook or Slack credentialWebhook is simplest for a single channel
EmailSMTP credential or provider nodeUse 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.

  1. 1
    What is the exact definition and source of truth.
  2. 2
    What is the reporting window and timezone.
  3. 3
    What are the acceptable bounds and failure modes.

A good starting KPI set for a SaaS or subscription product looks like this:

KPISourceDefinitionNotes
SessionsGA4Sessions in the reporting windowEnsure same timezone each run
ConversionsGA4Key event count or purchasesUse explicit event name list
New customersStripeCount of customers createdConsider dedupe if you import customers
Gross revenueStripeSum of paid invoices or chargesDecide whether to include refunds
RefundsStripeSum of refunds createdInclude partial refunds
Active usersPostgresUsers with activity event in windowDepends on your product schema
Trial to paidPostgres plus StripeTrials started versus first paymentOften 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.

StagePurposeOutput
Schedule and windowDefine start and end datesA reportContext object
FetchPull raw data from each systemRaw responses stored or logged
NormalizeConvert to a shared metric schemaConsistent numeric fields
ValidateData quality checks and completenessPass or fail with clear reasons
AggregateCompute totals, deltas, and ratiosFinal KPI object
SummarizeGenerate a short narrativeSlack-ready text and email body
DeliverSend to Slack and emailMessage IDs, deliver status
PersistOptional: store results in PostgresWeekly 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.

PatternWhen to useProsCons
Single workflowEarly stage, fewer metricsEasier to debug in one canvasCan grow messy over months
Orchestrator plus sub-workflowsMultiple data sources, multiple teamsReuse, separation of concernsNeeds 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.

ParameterExampleWhy it matters
Business timezoneEurope ZagrebAligns with how stakeholders interpret “weekly”
Week startMondayMatches common EU reporting
Run timeTuesday 08:00Allows for GA4 and Stripe processing lag
WindowPrior Mon 00:00 to Sun 23:59:59Prevents 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.

JavaScript
// 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#

MetricGA4 metric nameTypical dimensionNotes
SessionssessionsdateBaseline traffic
UserstotalUsersdateUseful to sanity-check sessions
Key eventseventCounteventNameFilter to your key events
PurchasespurchasesdateIf 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.

Bash
# GA4 Analytics Data API endpoint
POST https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport
JSON
{
  "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.

JavaScript
// 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#

OptionWhat it measuresBest forCommon pitfall
Charges paidMoney capturedOne-time paymentsExcludes invoices and some subscription flows
Invoices paidSubscription revenueSaaS billingNeeds adjustments for credits and proration
Balance transactionsNet cash movementFinance alignmentMore 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.

Bash
# List invoices paid during window
GET https://api.stripe.com/v1/invoices?limit=100&status=paid&created[gte]=START_TS&created[lte]=END_TS

Normalize to your KPI schema:

JavaScript
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.

CheckRuleWhy
Currency consistencyAll invoices in same currencyPrevents summing EUR and USD
Amount sanitygrossRevenue is greater than or equal to 0Detects parsing bugs
Volume sanityinvoiceCount within expected rangeDetects 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#

KPIExample query targetNotes
Weekly active usersevents tableCount distinct user IDs in window
New signupsusers tableFilter by created_at
Trials startedsubscriptions tableFilter by trial start
Activation ratederivedActivated divided by signups

Example query for weekly active users:

SQL
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:

FieldTypeExampleSource
weekLabelstring2026-05-19 to 2026-05-25Context
sessionsnumber12450GA4
usersnumber8920GA4
grossRevenuenumber18350.25Stripe
refundsnumber320.00Stripe
waunumber1560Postgres
signupCountnumber210Postgres

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.

JavaScript
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#

RuleExample thresholdAction if failed
Required fields presentsessions, grossRevenue, wauFail and alert
Non-negative metricsall numeric KPIs greater than or equal to 0Fail
GA4 completenessusers greater than 0 on non-holiday weeksWarn or fail
Stripe completenessinvoiceCount consistent with historical baselineWarn
Spike detectionweek over week change greater than 50 percentSend with warning banner
Data freshnesswindow end is at least 24 hours behind nowFail 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.

JavaScript
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.

SourceCommon failureRetry approachBackoff
GA4 API429 rate limit, 5xxRetry 3 times30s, 2m, 5m
Stripe429, network timeoutsRetry 3 times30s, 2m, 5m
Postgresconnection timeoutRetry 2 times30s, 2m
Slack or emailtransient networkRetry 2 times30s, 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.

ColumnTypeExample
week_startdate2026-05-19
week_enddate2026-05-25
payloadjsonbnormalized KPIs and summary
delivered_attimestamptznull or timestamp
delivery_channeltextslack, email

Then, the workflow can do this:

  1. 1
    Compute window.
  2. 2
    Check if record exists and delivered.
  3. 3
    If delivered, stop.
  4. 4
    If not delivered, compute KPIs and summary, upsert record.
  5. 5
    Deliver.
  6. 6
    Update 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#

SectionContentMax length
HeaderWeek label, high-level outcome1 line
GrowthWeek over week deltas for key KPIs3 to 5 bullets
RisksAnomalies or missing data notes1 to 2 bullets
Next actionsOne suggestion based on data1 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:

JavaScript
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 detailRecommendationWhy
Channelone dedicated channelreduces noise elsewhere
Threadingoptional, start a thread for drilldownskeeps channel clean
Attachmentsavoid heavy formattingreduces rendering issues
Linksinclude dashboard linksenables 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 fieldRecommendation
Fromops or analytics mailbox
Toa group alias
SubjectWeekly KPI Digest: YYYY-MM-DD to YYYY-MM-DD
Bodysame 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#

AreaWhat to implementEffort
Logsstore raw API responses for failed runs onlylow
Alertingsend an error summary to an ops channellow
Versioningkeep KPI definitions in one placemedium
Ownershipadd an “owner” field and runbook link in workflow descriptionlow
Secrets hygienerestricted keys, rotate credentialsmedium
Data driftweekly anomaly checksmedium

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

Share
A
Adrijan OmićevićSamioda Team
All articles →

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.