Checking GA4 every morning to pull the same metrics is one of the most common time-wasters in analytics teams. A 10-minute daily task that adds up to nearly an hour per week — and often gets skipped on busy days, meaning you don’t notice traffic drops or conversion anomalies until they’ve been running for days.
This guide shows you how to build an n8n workflow that does it for you: connects to the GA4 Data API, pulls your key metrics, uses an AI node to write a natural-language summary, and posts the whole thing to a Slack channel on a daily schedule.
By the end, you’ll have an automated morning report that surfaces what actually changed — not just what the numbers are, but whether they’re significantly up, down, or flat compared to last week.
What You’ll Build
A daily n8n workflow that:
- Runs every weekday morning at 9:00 AM
- Pulls yesterday’s sessions, users, conversions, and revenue from GA4
- Compares to the same day last week
- Sends the data to an AI node (Claude or GPT-4) to write a 2-paragraph summary
- Posts the formatted report to a Slack channel
Time to set up: 45–60 minutes Cost: Free (n8n community + Google API + Slack incoming webhook); AI node costs ~$0.001 per daily report
Prerequisites
- n8n — self-hosted (install guide) or cloud account at n8n.io
- Google Analytics 4 property with data
- Google Cloud project with the Analytics Data API enabled
- Slack workspace with permission to create incoming webhooks
- OpenAI or Anthropic API key (for the AI summary node)
Step 1: Set Up Google Analytics Data API Access
The GA4 Data API is what n8n uses to query your analytics data. Unlike the BigQuery approach, the Data API gives you aggregated report data — exactly what you need for daily summaries.
Enable the API
- Go to Google Cloud Console
- Select or create a project
- APIs & Services → Library → search “Google Analytics Data API” → Enable
Create a Service Account
- APIs & Services → Credentials → Create Credentials → Service Account
- Name:
n8n-ga4-reporter - Role: Viewer (read-only is sufficient)
- Done
- Click the service account → Keys tab → Add Key → JSON → download the JSON file
Add the Service Account to Your GA4 Property
- GA4 Admin → Account Access Management (or Property Access)
- Click + → Add users
- Enter the service account email (visible in your JSON file as
client_email) - Role: Viewer
- Add
Step 2: Get Your Slack Webhook URL
- Go to api.slack.com/apps → Create New App → From scratch
- Name:
GA4 Reporter - Incoming Webhooks → Activate Incoming Webhooks → toggle on
- Add New Webhook to Workspace → choose your target channel
- Copy the webhook URL — looks like:
https://hooks.slack.com/services/T.../B.../XXXXX
Step 3: Build the n8n Workflow
Overview of Nodes
Schedule Trigger → GA4 (Yesterday) → GA4 (Last Week) → Code (Format) → AI Agent → Slack
Node 1: Schedule Trigger
- Node type: Schedule Trigger
- Trigger rule: Every day at 9:00 AM, Monday–Friday
- Settings:
- Mode: Custom (cron expression)
- Expression:
0 9 * * 1-5
Node 2: HTTP Request — GA4 Yesterday
n8n has a native GA4 node, but the HTTP Request node gives you more control over the API. We’ll use the GA4 Data API directly.
- Node type: HTTP Request
- Method: POST
- URL:
https://analyticsdata.googleapis.com/v1beta/properties/YOUR_PROPERTY_ID:runReport
Replace YOUR_PROPERTY_ID with your GA4 numeric property ID (found in GA4 Admin → Property Settings).
Authentication:
- Auth type: Predefined Credential Type
- Credential type: Google API (import your service account JSON)
Body (JSON):
{
"dateRanges": [
{
"startDate": "yesterday",
"endDate": "yesterday"
}
],
"dimensions": [
{ "name": "date" }
],
"metrics": [
{ "name": "sessions" },
{ "name": "totalUsers" },
{ "name": "conversions" },
{ "name": "purchaseRevenue" },
{ "name": "bounceRate" },
{ "name": "averageSessionDuration" }
]
}
Node 3: HTTP Request — GA4 Same Day Last Week
Duplicate the previous node and change the dateRanges to:
{
"dateRanges": [
{
"startDate": "8daysAgo",
"endDate": "8daysAgo"
}
],
"dimensions": [{ "name": "date" }],
"metrics": [
{ "name": "sessions" },
{ "name": "totalUsers" },
{ "name": "conversions" },
{ "name": "purchaseRevenue" },
{ "name": "bounceRate" },
{ "name": "averageSessionDuration" }
]
}
Node 4: Code Node — Format the Data
Add a Code node to extract and compare the metrics from both API responses:
// Extract metrics from GA4 API response
function extractMetrics(response) {
const row = response.rows?.[0];
if (!row) return null;
const values = row.metricValues;
return {
date: row.dimensionValues[0].value,
sessions: parseInt(values[0].value),
users: parseInt(values[1].value),
conversions: parseInt(values[2].value),
revenue: parseFloat(values[3].value).toFixed(2),
bounceRate: (parseFloat(values[4].value) * 100).toFixed(1),
avgSessionDuration: Math.round(parseFloat(values[5].value))
};
}
const yesterday = extractMetrics($('GA4 Yesterday').first().json);
const lastWeek = extractMetrics($('GA4 Last Week').first().json);
function pctChange(current, previous) {
if (!previous || previous === 0) return 'N/A';
const change = ((current - previous) / previous) * 100;
const sign = change >= 0 ? '+' : '';
return `${sign}${change.toFixed(1)}%`;
}
const formatted = {
date: yesterday.date,
metrics: {
sessions: {
value: yesterday.sessions,
change: pctChange(yesterday.sessions, lastWeek.sessions),
lastWeek: lastWeek.sessions
},
users: {
value: yesterday.users,
change: pctChange(yesterday.users, lastWeek.users),
lastWeek: lastWeek.users
},
conversions: {
value: yesterday.conversions,
change: pctChange(yesterday.conversions, lastWeek.conversions),
lastWeek: lastWeek.conversions
},
revenue: {
value: `£${yesterday.revenue}`,
change: pctChange(parseFloat(yesterday.revenue), parseFloat(lastWeek.revenue)),
lastWeek: `£${lastWeek.revenue}`
},
bounceRate: {
value: `${yesterday.bounceRate}%`,
change: pctChange(parseFloat(yesterday.bounceRate), parseFloat(lastWeek.bounceRate)),
lastWeek: `${lastWeek.bounceRate}%`
},
avgSessionDuration: {
value: `${Math.floor(yesterday.avgSessionDuration / 60)}m ${yesterday.avgSessionDuration % 60}s`,
lastWeek: `${Math.floor(lastWeek.avgSessionDuration / 60)}m ${lastWeek.avgSessionDuration % 60}s`
}
},
rawYesterday: yesterday,
rawLastWeek: lastWeek
};
return formatted;
Node 5: AI Agent — Write the Summary
- Node type: OpenAI (or Anthropic Claude node)
- Model:
gpt-4o-mini(cost-efficient) orclaude-3-haiku-20240307 - Prompt:
You are an analytics assistant. Write a 2-paragraph daily website analytics summary based on the following data.
Be concise and focus on notable changes. Use British English.
If a metric changed significantly (more than 10%), highlight it.
Suggest one possible reason for significant changes if relevant.
End with one brief action recommendation.
Data:
Date: {{ $json.date }}
Sessions: {{ $json.metrics.sessions.value }} ({{ $json.metrics.sessions.change }} vs last week)
Users: {{ $json.metrics.users.value }} ({{ $json.metrics.users.change }} vs last week)
Conversions: {{ $json.metrics.conversions.value }} ({{ $json.metrics.conversions.change }} vs last week)
Revenue: {{ $json.metrics.revenue.value }} ({{ $json.metrics.revenue.change }} vs last week)
Bounce Rate: {{ $json.metrics.bounceRate.value }} ({{ $json.metrics.bounceRate.change }} vs last week)
Write 2 short paragraphs only. No headers. No bullet points.
Node 6: Slack — Post the Report
- Node type: HTTP Request (POST to Slack webhook)
- URL: Your Slack webhook URL
- Method: POST
- Body (JSON):
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "📊 Daily Analytics Report — {{ $('Code Node').item.json.date }}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Sessions*\n{{ $('Code Node').item.json.metrics.sessions.value }} ({{ $('Code Node').item.json.metrics.sessions.change }})"
},
{
"type": "mrkdwn",
"text": "*Users*\n{{ $('Code Node').item.json.metrics.users.value }} ({{ $('Code Node').item.json.metrics.users.change }})"
},
{
"type": "mrkdwn",
"text": "*Conversions*\n{{ $('Code Node').item.json.metrics.conversions.value }} ({{ $('Code Node').item.json.metrics.conversions.change }})"
},
{
"type": "mrkdwn",
"text": "*Revenue*\n{{ $('Code Node').item.json.metrics.revenue.value }} ({{ $('Code Node').item.json.metrics.revenue.change }})"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "{{ $('AI Agent').item.json.choices[0].message.content }}"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Powered by n8n + GA4 Data API | <https://analytics.google.com|View in GA4>"
}
]
}
]
}
Step 4: Test the Workflow
- In n8n, click Execute workflow (the play button)
- Check each node’s output by clicking on it
- Verify the GA4 nodes return data (look for
rowsarray in the response) - Verify the Code node produces formatted metrics
- Check the Slack channel — the report should appear
Common issues:
- GA4 node returns empty rows: Check that your service account has been added to the GA4 property and that the property ID is correct
- Authentication failure: Verify the service account JSON is loaded correctly in n8n credentials
- Slack not posting: Test your webhook URL with a manual curl request first
Extensions and Improvements
Add Channel Breakdown
Extend the GA4 query to include sessionDefaultChannelGrouping as a dimension to show which channels drove traffic:
{
"dimensions": [
{ "name": "sessionDefaultChannelGrouping" }
],
"metrics": [
{ "name": "sessions" },
{ "name": "conversions" }
],
"dateRanges": [{ "startDate": "yesterday", "endDate": "yesterday" }],
"orderBys": [{ "metric": { "metricName": "sessions" }, "desc": true }],
"limit": 5
}
Add Anomaly Detection
Instead of just showing percentage changes, add logic to flag anomalies:
function isAnomaly(current, previous, threshold = 0.25) {
if (!previous) return false;
return Math.abs((current - previous) / previous) > threshold;
}
// Add to your code node:
const anomalies = [];
if (isAnomaly(yesterday.sessions, lastWeek.sessions)) {
anomalies.push(`⚠️ Sessions ${pctChange(yesterday.sessions, lastWeek.sessions)} — investigate`);
}
Run Weekly Summary on Fridays
Add a second schedule trigger for Friday at 5 PM that queries the full week and sends a weekly summary instead of a daily one.
Email Version
Replace the Slack node with an email node (SMTP or Gmail) to send the report to stakeholders who prefer email over Slack.
The Value of Automated Reporting
The goal isn’t to replace human analysis — it’s to ensure that basic monitoring happens consistently without requiring someone to remember to do it. When anomalies appear in the automated report, that’s the trigger for deeper investigation.
We build custom n8n workflows for analytics reporting, Shopify order automation, Amazon seller data pipelines, and multi-channel dashboard automation. Book a free consultation to discuss what workflow automation could save your team each week.