AI Agents 13 min read

How to Build an AI Agent That Sends GA4 Reports to Slack Using n8n

A step-by-step guide to building an n8n workflow that pulls data from the GA4 Data API, formats it with an AI summary, and posts it automatically to Slack on a schedule.

A
Aumlytics Team
·

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:

  1. Runs every weekday morning at 9:00 AM
  2. Pulls yesterday’s sessions, users, conversions, and revenue from GA4
  3. Compares to the same day last week
  4. Sends the data to an AI node (Claude or GPT-4) to write a 2-paragraph summary
  5. 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

  1. Go to Google Cloud Console
  2. Select or create a project
  3. APIs & ServicesLibrary → search “Google Analytics Data API” → Enable

Create a Service Account

  1. APIs & ServicesCredentialsCreate CredentialsService Account
  2. Name: n8n-ga4-reporter
  3. Role: Viewer (read-only is sufficient)
  4. Done
  5. Click the service account → Keys tab → Add KeyJSON → download the JSON file

Add the Service Account to Your GA4 Property

  1. GA4 Admin → Account Access Management (or Property Access)
  2. Click +Add users
  3. Enter the service account email (visible in your JSON file as client_email)
  4. Role: Viewer
  5. Add

Step 2: Get Your Slack Webhook URL

  1. Go to api.slack.com/appsCreate New AppFrom scratch
  2. Name: GA4 Reporter
  3. Incoming WebhooksActivate Incoming Webhooks → toggle on
  4. Add New Webhook to Workspace → choose your target channel
  5. 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) or claude-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

  1. In n8n, click Execute workflow (the play button)
  2. Check each node’s output by clicking on it
  3. Verify the GA4 nodes return data (look for rows array in the response)
  4. Verify the Code node produces formatted metrics
  5. 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.

#n8n#ga4#automation#slack#ai-agents#google-analytics#reporting

Want This Implemented Correctly?

Let our team apply these concepts to your specific setup — with QA validation and 30 days of support.