Skip to main content

LeenLabs

Screenshot: every node completing successfully in a live n8n test run.

Affiliate disclosure: some links in this article may be affiliate links. If you sign up through them, we may earn a commission at no extra cost to you. See our Terms of Service.

If you run Google Ads for a business or for clients, you know the ritual. You open the dashboard, scan the campaign list, compare clicks and costs with yesterday in your head, copy a few numbers into a spreadsheet, and message someone about the campaign that looks off. None of it is difficult. All of it is easy to skip on a busy day, and the day you skip it is the day a campaign can quietly keep spending while nobody is looking.

This free n8n workflow does that morning check for you. Every day at 9 AM it pulls your campaigns from Google Ads, gives each one a score from 0 to 100, writes the results into Google Sheets, pings your team in Slack about campaigns worth scaling or worth a closer look, and emails a short report for each campaign. This guide explains every step in plain English, shows exactly how the scoring works, lists what can go wrong and how to fix it, and tells you honestly what has and has not been verified.

Template source: this workflow comes from the n8n Library at n8n.io, the public collection of workflow templates. We did not create the original template. LeenLabs adjusted its JSON for publication and documented it. The adjustments are listed in the section What we changed in the original template.

At a glance

  • What it does: checks your Google Ads campaigns every day, scores them from 0 to 100, records the results in Google Sheets, alerts Slack and sends email reports.
  • Apps used: n8n, Google Ads, Google Sheets, Slack and an email account (SMTP).
  • Trigger: a schedule, every day at 9 AM once the workflow is turned on.
  • Setup effort: higher than most of our free workflows, because Google Ads API access is the slowest part to arrange.
  • Important to know: the “AI” in one node’s name is a fixed scoring formula, not an artificial intelligence model. The thresholds are presets that you should adjust.
  • Safe by design: it only reads from Google Ads. It never changes your campaigns, budgets or bids.
  • Test status: logic verified with simulated data on n8n 2.38.5; not tested on live APIs.

In this guide

  1. The daily check this workflow takes off your plate
  2. Words you will see in this guide
  3. Apps and accounts this workflow talks to
  4. What to have ready before you import anything
  5. How the scoring works: a formula, not artificial intelligence
  6. Follow the data: what each of the 11 steps does
  7. Get the workflow JSON
  8. Install it in ten steps
  9. What it will not do: limits, costs and trade-offs
  10. Access and data safety
  11. Problems you may meet and how to fix them
  12. Too much of a puzzle? Let us build or adapt it for you
  13. What we changed in the original template
  14. Logic verified with simulated data on n8n 2.38.5; not tested on live APIs
  15. Three ways to adapt the workflow
  16. Frequently asked questions
  17. Tools used in this workflow
  18. Keep exploring

The daily check this workflow takes off your plate

Checking campaign performance by hand fails in predictable ways. You forget on busy days. You look at raw numbers without a yardstick, so “is 3.2% a good click-through rate?” turns into guesswork each morning. The results live in your head or in a screenshot, so there is no history to look back on. And nobody else on the team finds out unless you tell them.

In one run, this workflow handles six jobs:

  • Collects your campaigns and their metrics from Google Ads.
  • Scores each campaign from 0 to 100 using the same formula every day.
  • Sorts campaigns into two paths: Excellent or Good on one side, Fair or Underperforming on the other.
  • Records the results in two Google Sheets tabs: a current dashboard and a growing log.
  • Alerts your team in Slack with a message that fits the path the campaign took.
  • Reports by email, with suggested next steps, and builds an overall summary of the run.

It suits freelancers, small agencies and in-house marketers who run Google Ads and want a steady daily signal without opening a dashboard. It is not a good fit if you need real-time monitoring (it runs once a day), if you cannot get Google Ads API access, or if you expect it to optimize campaigns on its own. It only reports and recommends.

Words you will see in this guide

Automation and advertising each come with their own jargon. Here are the terms in everyday language.

  • n8n (say “n-eight-n”): a tool that connects apps and makes them follow your instructions automatically.
  • Workflow: one complete chain of instructions, from the first step to the last.
  • Node: one step in that chain. Each node does a single small job.
  • Trigger: the node that starts everything. Here it is a schedule, like an alarm clock.
  • Credential: a saved login that lets n8n use one of your accounts. Think of it as giving an assistant a key to one specific room.
  • API: a way for two programs to exchange information directly.
  • Developer token: a code that Google asks for before a program may read data from a Google Ads account through the API.
  • SMTP: the standard way programs send email through an email account.
  • Code node: a node that runs a short JavaScript snippet. Three nodes in this workflow use one.
  • CTR (click-through rate): the share of people who saw your ad and clicked it. 100 views and 3 clicks is a 3% CTR.
  • Conversion: a valuable action you defined in Google Ads, such as a purchase or a sign-up. Conversion rate is conversions divided by clicks.
  • Cost per conversion: how much you spent for each conversion on average.
  • Micros: the way Google Ads stores money, in millionths of your currency unit. The workflow converts them for you.
  • JSON file: a plain text file that describes an entire workflow. Import it and every node appears, already connected.

Apps and accounts this workflow talks to

App Role in the workflow What you need
n8n Runs the workflow every day An n8n instance that stays online (cloud or self-hosted)
Google Ads Source of the campaign data An ad account with campaigns, Google Ads API access (including a developer token) and the account’s customer ID
Google Sheets Dashboard and history A Google account and a spreadsheet with two tabs
Slack Alerts for your team A Slack workspace, a channel, and permission to connect an app
Email (SMTP) Sends the report emails SMTP details for a mailbox that is allowed to send, and the recipient’s address

What to have ready before you import anything

  1. An n8n instance that is always running. A schedule only fires if n8n is on. A self-hosted n8n on a laptop that sleeps will miss days.
  2. A Google Ads account with conversion tracking. The score gives 35 of its 100 points to conversion rate. Without conversion tracking, conversions stay at zero and every campaign scores low.
  3. Google Ads API access. Google generally requires a developer token to read account data through the API, and the n8n credential asks for it. How you obtain it, and how long approval takes, depends on Google’s current rules, so check Google’s and n8n’s documentation first. If you reach the account through a manager account, you also need its ID. You will also need the 10-digit customer ID of the account you want to monitor.
  4. A Google Sheet with two tabs named exactly Dashboard and Log. Both tabs need the same twelve headers in the first row, spelled exactly as in the table below.
  5. A Slack channel for the alerts.
  6. SMTP details for the sending mailbox, plus the address that should receive the reports. Many email providers require an app password rather than your normal password for SMTP, so check your provider’s rules.

The twelve headers, in this order:

Column What goes in it
Campaign ID The campaign’s ID (the Dashboard tab uses it to find and update the right row)
Campaign Name The campaign’s name
Status The campaign status reported by Google Ads
Score The 0 to 100 score
Tier Excellent, Good, Fair or Underperforming
Alert Level normal, warning or critical
Spend Cost over the selected date range, in your account currency
Conversions Conversions over the selected date range
CTR % Click-through rate, as a percentage
Conversion Rate % Conversions divided by clicks, as a percentage
Cost per Conversion Spend divided by conversions
Updated At When the analysis ran

How the scoring works: a formula, not artificial intelligence

One node is called “AI Performance Analysis”. We kept that name so this article and the JSON file match, but no AI model is involved. The node is a short piece of JavaScript that adds up points according to fixed rules. That is good news: the same numbers always produce the same score, and you can read and change every rule.

Each campaign can earn up to 100 points from four checks:

Check Points What happens otherwise
CTR 30 points for 5% or more, 20 for 3% or more, 10 for 1.5% or more Below 1.5% earns 0 points and raises a warning
Conversion rate 35 points for 10% or more, 25 for 5% or more, 15 for 2% or more Below 2% earns 0 points and raises a critical alert
Cost per conversion 25 points for $25 or less, 15 for $50 or less, 8 for $100 or less Above $100 earns 0 points and raises a warning. Campaigns with no conversions skip this check
Traffic volume 10 points for 1,000 clicks or more, 5 for 500 or more Under 100 clicks adds a note suggesting more budget or wider targeting

The total decides the tier: 75 or more is Excellent, 55 or more is Good, 35 or more is Fair, and anything lower is Underperforming.

Here is a made-up example. A campaign with a 4% CTR earns 20 points. A 4% conversion rate earns 15. A cost per conversion of $20 earns 25. Having 600 clicks earns 5. The total is 65, which is a Good campaign.

Then comes the routing step. Campaigns that are Excellent or Good go down the “scale opportunity” path. Everything else, meaning Fair and Underperforming, goes down the “issues detected” path. So a Fair campaign triggers the issues alert even if nothing is technically broken.

Read this before you trust the numbers: the thresholds above (a 5% CTR, a $25 cost per conversion and so on) are the template’s presets. We have no source saying they suit your industry, your product price or your country. A $25 cost per conversion could be excellent for one business and a loss for another. Treat the first weeks as calibration and change the thresholds to match your own numbers. The email text also contains preset suggestions, such as raising a budget by 20 to 30% for Excellent campaigns. Those are generic prompts for you to think about, not advice tailored to your account.

Follow the data: what each of the 11 steps does

The workflow has 11 nodes. Data flows through them in three stages: collect, decide, then act.

Schedule Daily Check

The trigger. Once the workflow is turned on, it starts every day at 9 AM, using the time zone in your n8n settings. If the workflow is not turned on, it never fires.

Fetch Google Ads Data

This node reads your campaigns from Google Ads, using your customer ID. In this file it asks for enabled campaigns over the last 7 days. That date range matters: the spend and conversion numbers in your sheet and emails cover those 7 days, not a single day.

AI Performance Analysis

The scoring formula described above. For each campaign it works out CTR, conversion rate, cost per conversion and average cost per click, awards points, assigns a tier and an alert level, and writes a list of insights and recommendations.

Route by Performance

A yes-or-no decision. Is the tier Excellent or Good? If yes, the campaign takes the first path. If no, it takes the second. Both paths then do the same four things: update the dashboard, log the campaign, send the matching Slack alert, and generate the action plan.

Update Campaign Dashboard

This Google Sheets node writes one row per campaign into the Dashboard tab. It matches rows by Campaign ID, so tomorrow’s run updates today’s row instead of adding a new one.

Log All Campaigns

This Google Sheets node adds every campaign to the Log tab as a new row, every day. That tab becomes your history, so you can see how a campaign’s score moved over weeks.

Alert: Scale Opportunity

A Slack message for each Excellent or Good campaign, with its score, tier, spend, conversions and cost per conversion.

Alert: Issues Detected

A Slack message for each Fair or Underperforming campaign, with its score, tier, alert level and the recommendations the formula produced.

Generate Action Plan

A Code node that writes the email for each campaign. There are three versions. Excellent and Good campaigns get a scaling email. Other campaigns with a warning or critical alert get an urgent email. Anything else gets a plain daily report. This node also sets a priority (High, Urgent or Medium).

Email Performance Report

Sends the email written in the previous step, one per campaign, to the recipient you set.

Generate Daily Summary

Counts how many campaigns fall into each tier and adds up total spend and total conversions for the whole run. This node runs once per execution. Note that the summary is calculated but not sent anywhere: you can read it in the execution results in n8n. One of the adaptations below shows how to send it to Slack.

Get the workflow JSON

Open the box below to copy the JSON, or download the file. It contains the 11 nodes above, connected in order, with empty fields where your own credentials and settings go, and short notes on the canvas with setup reminders.

Please read this first: this JSON is free and provided as is. n8n versions, installed nodes and Google Ads setups differ from one person to another, so it may not run correctly on your side without small adjustments. We cannot test it on every setup. That is why this article includes a full section, Problems you may meet and how to fix them, with a fix for each one. And if you would rather not deal with any of it, you can request a custom workflow and let us handle it.

Click to show or hide the workflow JSON

Click inside the box, select the whole text, copy it, then paste it onto an empty n8n canvas. If selecting is awkward on your device, use the download link above.

{
  "name": "Monitor Google Ads Campaigns Daily with n8n (free workflow JSON)",
  "nodes": [
    {
      "id": "97e85725-b387-480e-a9d1-8066a5aef2de",
      "name": "Schedule Daily Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -960,
        240
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 9
            }
          ]
        }
      }
    },
    {
      "id": "ac8ed47a-01cd-46b9-aff3-6fb6297f0b33",
      "name": "Fetch Google Ads Data",
      "type": "n8n-nodes-base.googleAds",
      "typeVersion": 1,
      "position": [
        -736,
        240
      ],
      "parameters": {
        "managerCustomerId": "",
        "clientCustomerId": "",
        "additionalOptions": {
          "dateRange": "LAST_7_DAYS",
          "campaignStatus": "ENABLED"
        },
        "requestOptions": {}
      }
    },
    {
      "id": "ae7508bf-4292-4db2-93ac-be1af7bf73d0",
      "name": "AI Performance Analysis",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -512,
        240
      ],
      "parameters": {
        "jsCode": "// AI-powered campaign performance analysis\nconst items = $input.all();\nconst analyzedItems = [];\n\nfor (const item of items) {\n  const data = item.json;\n  let performanceScore = 0;\n  let insights = [];\n  let recommendations = [];\n  let alertLevel = 'normal';\n  \n  // Extract metrics.\n  // The Google Ads node can return fields nested (campaign / metrics) or flat (id, name, costMicros...).\n  // This block reads both. If your numbers come out as 0, open the output of \"Fetch Google Ads Data\"\n  // and adjust the field names below. Clicks fall back to interactions when clicks is not returned.\n  const m = data.metrics || data;\n  const camp = data.campaign || data;\n  const clicks = Number(m.clicks ?? m.interactions ?? 0);\n  const impressions = Number(m.impressions ?? 0);\n  const cost = Number(m.cost_micros ?? m.costMicros ?? 0) / 1000000;\n  const conversions = Number(m.conversions ?? 0);\n  const ctr = impressions > 0 ? (clicks / impressions) * 100 : 0;\n  const conversionRate = clicks > 0 ? (conversions / clicks) * 100 : 0;\n  const costPerConversion = conversions > 0 ? cost / conversions : 0;\n  const avgCpc = clicks > 0 ? cost / clicks : 0;\n  \n  // CTR Analysis (Click-Through Rate)\n  if (ctr >= 5) {\n    performanceScore += 30;\n    insights.push(`Excellent CTR: ${ctr.toFixed(2)}% (+30 points)`);\n  } else if (ctr >= 3) {\n    performanceScore += 20;\n    insights.push(`Good CTR: ${ctr.toFixed(2)}% (+20 points)`);\n  } else if (ctr >= 1.5) {\n    performanceScore += 10;\n    insights.push(`Average CTR: ${ctr.toFixed(2)}% (+10 points)`);\n    recommendations.push('Consider A/B testing ad copy to improve CTR');\n  } else {\n    insights.push(`Low CTR: ${ctr.toFixed(2)}% (0 points)`);\n    recommendations.push('URGENT: Review ad relevance and targeting');\n    alertLevel = 'warning';\n  }\n  \n  // Conversion Rate Analysis\n  if (conversionRate >= 10) {\n    performanceScore += 35;\n    insights.push(`Outstanding conversion rate: ${conversionRate.toFixed(2)}% (+35 points)`);\n  } else if (conversionRate >= 5) {\n    performanceScore += 25;\n    insights.push(`Strong conversion rate: ${conversionRate.toFixed(2)}% (+25 points)`);\n  } else if (conversionRate >= 2) {\n    performanceScore += 15;\n    insights.push(`Moderate conversion rate: ${conversionRate.toFixed(2)}% (+15 points)`);\n    recommendations.push('Optimize landing pages for better conversions');\n  } else {\n    insights.push(`Low conversion rate: ${conversionRate.toFixed(2)}% (0 points)`);\n    recommendations.push('CRITICAL: Review landing page experience and targeting');\n    alertLevel = 'critical';\n  }\n  \n  // Cost Efficiency Analysis\n  if (costPerConversion > 0) {\n    if (costPerConversion <= 25) {\n      performanceScore += 25;\n      insights.push(`Excellent cost per conversion: $${costPerConversion.toFixed(2)} (+25 points)`);\n    } else if (costPerConversion <= 50) {\n      performanceScore += 15;\n      insights.push(`Good cost per conversion: $${costPerConversion.toFixed(2)} (+15 points)`);\n    } else if (costPerConversion <= 100) {\n      performanceScore += 8;\n      insights.push(`Fair cost per conversion: $${costPerConversion.toFixed(2)} (+8 points)`);\n      recommendations.push('Look for ways to reduce cost per conversion');\n    } else {\n      insights.push(`High cost per conversion: $${costPerConversion.toFixed(2)} (0 points)`);\n      recommendations.push('URGENT: Review bid strategy and audience targeting');\n      alertLevel = alertLevel === 'critical' ? 'critical' : 'warning';\n    }\n  }\n  \n  // Volume Analysis\n  if (clicks >= 1000) {\n    performanceScore += 10;\n    insights.push(`High traffic volume: ${clicks} clicks (+10 points)`);\n  } else if (clicks >= 500) {\n    performanceScore += 5;\n    insights.push(`Good traffic volume: ${clicks} clicks (+5 points)`);\n  } else if (clicks < 100) {\n    insights.push(`Low traffic volume: ${clicks} clicks (0 points)`);\n    recommendations.push('Consider increasing budget or expanding targeting');\n  }\n  \n  // Determine performance tier\n  let performanceTier = 'Underperforming';\n  if (performanceScore >= 75) {\n    performanceTier = 'Excellent';\n  } else if (performanceScore >= 55) {\n    performanceTier = 'Good';\n  } else if (performanceScore >= 35) {\n    performanceTier = 'Fair';\n  }\n  \n  // Add strategic recommendations\n  if (performanceTier === 'Excellent') {\n    recommendations.push('Scale this campaign - increase budget by 20-30%');\n    recommendations.push('Use this as a template for new campaigns');\n  } else if (performanceTier === 'Good') {\n    recommendations.push('Test incremental budget increases');\n    recommendations.push('Identify top-performing ad groups to scale');\n  }\n  \n  analyzedItems.push({\n    json: {\n      ...data,\n      campaignId: camp.id,\n      campaignName: camp.name,\n      campaignStatus: camp.status,\n      performanceScore: performanceScore,\n      performanceTier: performanceTier,\n      alertLevel: alertLevel,\n      insights: insights,\n      recommendations: recommendations,\n      metrics: {\n        clicks: clicks,\n        impressions: impressions,\n        cost: cost.toFixed(2),\n        conversions: conversions,\n        ctr: ctr.toFixed(2),\n        conversionRate: conversionRate.toFixed(2),\n        costPerConversion: costPerConversion.toFixed(2),\n        avgCpc: avgCpc.toFixed(2)\n      },\n      analyzedAt: new Date().toISOString()\n    }\n  });\n}\n\nreturn analyzedItems;"
      }
    },
    {
      "id": "cc1a1556-0544-4588-8259-b41891aae4fd",
      "name": "Route by Performance",
      "type": "n8n-nodes-base.if",
      "position": [
        -288,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "excellent",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.performanceTier }}",
              "rightValue": "Excellent"
            },
            {
              "id": "good",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.performanceTier }}",
              "rightValue": "Good"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "0be284fc-c850-4e6c-921c-17507e3bfd38",
      "name": "Update Campaign Dashboard",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        160,
        48
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list"
        },
        "sheetName": {
          "__rl": true,
          "value": "Dashboard",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Campaign ID": "={{ $json.campaignId }}",
            "Campaign Name": "={{ $json.campaignName }}",
            "Status": "={{ $json.campaignStatus }}",
            "Score": "={{ $json.performanceScore }}",
            "Tier": "={{ $json.performanceTier }}",
            "Alert Level": "={{ $json.alertLevel }}",
            "Spend": "={{ $json.metrics.cost }}",
            "Conversions": "={{ $json.metrics.conversions }}",
            "CTR %": "={{ $json.metrics.ctr }}",
            "Conversion Rate %": "={{ $json.metrics.conversionRate }}",
            "Cost per Conversion": "={{ $json.metrics.costPerConversion }}",
            "Updated At": "={{ $json.analyzedAt }}"
          },
          "matchingColumns": [
            "Campaign ID"
          ],
          "schema": [
            {
              "id": "Campaign ID",
              "displayName": "Campaign ID",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Campaign Name",
              "displayName": "Campaign Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Score",
              "displayName": "Score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Tier",
              "displayName": "Tier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Alert Level",
              "displayName": "Alert Level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Spend",
              "displayName": "Spend",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Conversions",
              "displayName": "Conversions",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CTR %",
              "displayName": "CTR %",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Conversion Rate %",
              "displayName": "Conversion Rate %",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Cost per Conversion",
              "displayName": "Cost per Conversion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Updated At",
              "displayName": "Updated At",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "e92b8a04-20a2-431a-9662-1365a87657ec",
      "name": "Log All Campaigns",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        -64,
        432
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list"
        },
        "sheetName": {
          "__rl": true,
          "value": "Log",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Campaign ID": "={{ $json.campaignId }}",
            "Campaign Name": "={{ $json.campaignName }}",
            "Status": "={{ $json.campaignStatus }}",
            "Score": "={{ $json.performanceScore }}",
            "Tier": "={{ $json.performanceTier }}",
            "Alert Level": "={{ $json.alertLevel }}",
            "Spend": "={{ $json.metrics.cost }}",
            "Conversions": "={{ $json.metrics.conversions }}",
            "CTR %": "={{ $json.metrics.ctr }}",
            "Conversion Rate %": "={{ $json.metrics.conversionRate }}",
            "Cost per Conversion": "={{ $json.metrics.costPerConversion }}",
            "Updated At": "={{ $json.analyzedAt }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Campaign ID",
              "displayName": "Campaign ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Campaign Name",
              "displayName": "Campaign Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Score",
              "displayName": "Score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Tier",
              "displayName": "Tier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Alert Level",
              "displayName": "Alert Level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Spend",
              "displayName": "Spend",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Conversions",
              "displayName": "Conversions",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CTR %",
              "displayName": "CTR %",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Conversion Rate %",
              "displayName": "Conversion Rate %",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Cost per Conversion",
              "displayName": "Cost per Conversion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Updated At",
              "displayName": "Updated At",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "c47437f1-c24f-41ae-8a78-cfa9afa73f86",
      "name": "Alert: Scale Opportunity",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        -64,
        -96
      ],
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "",
          "mode": "list"
        },
        "text": "=:rocket: Scale opportunity: {{ $json.campaignName }} scored {{ $json.performanceScore }}/100 ({{ $json.performanceTier }}). Spend {{ $json.metrics.cost }}, conversions {{ $json.metrics.conversions }}, cost per conversion {{ $json.metrics.costPerConversion }}.",
        "otherOptions": {}
      }
    },
    {
      "id": "776c44d4-1e01-42e4-b42b-163e1d1cfa73",
      "name": "Generate Action Plan",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -64,
        240
      ],
      "parameters": {
        "jsCode": "// Generate detailed action plan based on performance\nconst items = $input.all();\nconst actionPlanItems = [];\n\nfor (const item of items) {\n  const data = item.json;\n  const tier = data.performanceTier || 'Fair';\n  const campaignName = data.campaignName || 'Campaign';\n  const alertLevel = data.alertLevel || 'normal';\n  \n  let emailSubject = '';\n  let emailBody = '';\n  let priority = 'Medium';\n  \n  if (tier === 'Excellent' || tier === 'Good') {\n    priority = 'High';\n    emailSubject = `✅ ${campaignName}: Scaling Opportunity Detected`;\n    emailBody = `Hi PPC Team,\n\nGreat news! Your campaign \"${campaignName}\" is performing exceptionally well.\n\n📊 PERFORMANCE SUMMARY\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPerformance Score: ${data.performanceScore}/100\nPerformance Tier: ${tier}\nSpend (selected date range): $${data.metrics.cost}\nConversions: ${data.metrics.conversions}\nConversion Rate: ${data.metrics.conversionRate}%\nCost per Conversion: $${data.metrics.costPerConversion}\n\n💡 KEY INSIGHTS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${data.insights.map((insight, i) => `${i + 1}. ${insight}`).join('\\n')}\n\n🎯 RECOMMENDED ACTIONS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${data.recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\\n')}\n\n📈 SCALING STRATEGY\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n• Increase daily budget by 20-30%\n• Monitor performance closely for next 3-5 days\n• If metrics hold, continue scaling gradually\n• Consider duplicating successful ad groups\n• Test expansion to similar audiences\n\nThis campaign is ready to scale. Let's capitalize on this momentum!\n\nBest,\nAutomated PPC Intelligence System`;\n  } else if (alertLevel === 'critical' || alertLevel === 'warning') {\n    priority = 'Urgent';\n    emailSubject = `⚠️ ${campaignName}: Performance Issues Detected`;\n    emailBody = `Hi PPC Team,\n\n⚠️ ATTENTION REQUIRED\n\nYour campaign \"${campaignName}\" requires immediate attention due to performance concerns.\n\n📊 PERFORMANCE SUMMARY\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPerformance Score: ${data.performanceScore}/100\nPerformance Tier: ${tier}\nAlert Level: ${alertLevel.toUpperCase()}\nSpend (selected date range): $${data.metrics.cost}\nConversions: ${data.metrics.conversions}\nConversion Rate: ${data.metrics.conversionRate}%\nCost per Conversion: $${data.metrics.costPerConversion}\n\n⚠️ IDENTIFIED ISSUES\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${data.insights.map((insight, i) => `${i + 1}. ${insight}`).join('\\n')}\n\n🔧 REQUIRED ACTIONS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${data.recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\\n')}\n\n🚨 IMMEDIATE NEXT STEPS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. Review campaign settings and targeting\n2. Analyze recent changes that may have impacted performance\n3. Check landing page experience and load times\n4. Review competitor activity\n5. Consider pausing low-performing ad groups\n6. Test new ad variations\n\nPlease address these issues within 24 hours to prevent budget waste.\n\nBest,\nAutomated PPC Intelligence System`;\n  } else {\n    emailSubject = `📊 ${campaignName}: Daily Performance Report`;\n    emailBody = `Hi PPC Team,\n\nHere's your daily performance summary for \"${campaignName}\".\n\n📊 PERFORMANCE SUMMARY\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPerformance Score: ${data.performanceScore}/100\nPerformance Tier: ${tier}\nSpend (selected date range): $${data.metrics.cost}\nConversions: ${data.metrics.conversions}\nConversion Rate: ${data.metrics.conversionRate}%\nCost per Conversion: $${data.metrics.costPerConversion}\n\n💡 INSIGHTS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${data.insights.map((insight, i) => `${i + 1}. ${insight}`).join('\\n')}\n\n📋 RECOMMENDATIONS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${data.recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\\n')}\n\nContinue monitoring and optimizing for best results.\n\nBest,\nAutomated PPC Intelligence System`;\n  }\n  \n  actionPlanItems.push({\n    json: {\n      ...data,\n      emailSubject: emailSubject,\n      emailBody: emailBody,\n      priority: priority,\n      actionRequired: alertLevel === 'critical' || alertLevel === 'warning',\n      scaleRecommended: tier === 'Excellent' || tier === 'Good'\n    }\n  });\n}\n\nreturn actionPlanItems;"
      }
    },
    {
      "id": "b2b2389c-05b5-49c2-824a-50817acf150e",
      "name": "Email Performance Report",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        160,
        240
      ],
      "parameters": {
        "fromEmail": "CHANGE-ME@example.com",
        "toEmail": "CHANGE-ME@example.com",
        "subject": "={{ $json.emailSubject }}",
        "emailFormat": "text",
        "text": "={{ $json.emailBody }}",
        "options": {}
      }
    },
    {
      "id": "65a1623b-f9a9-415d-849a-42f779c3a766",
      "name": "Alert: Issues Detected",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        -64,
        624
      ],
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "",
          "mode": "list"
        },
        "text": "=:warning: Attention needed: {{ $json.campaignName }} scored {{ $json.performanceScore }}/100 ({{ $json.performanceTier }}, alert level {{ $json.alertLevel }}). {{ $json.recommendations.join('; ') }}",
        "otherOptions": {}
      }
    },
    {
      "id": "53086c74-d3f7-4761-91f1-706dd04aeda7",
      "name": "Generate Daily Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        384,
        144
      ],
      "parameters": {
        "jsCode": "// Build one summary for the whole run, from the analyzed campaigns.\n// It reads the analysis node directly, so it works whichever branch triggered it.\nconst items = $('AI Performance Analysis').all();\nconst countTier = (tier) => items.filter(item => item.json.performanceTier === tier).length;\n\nconst totalSpend = items.reduce((sum, item) => sum + parseFloat(item.json.metrics.cost || 0), 0);\nconst totalConversions = items.reduce((sum, item) => sum + Number(item.json.metrics.conversions || 0), 0);\n\nconst summary = {\n  totalCampaigns: items.length,\n  excellentCampaigns: countTier('Excellent'),\n  goodCampaigns: countTier('Good'),\n  fairCampaigns: countTier('Fair'),\n  underperformingCampaigns: countTier('Underperforming'),\n  totalSpend: totalSpend.toFixed(2),\n  totalConversions: totalConversions\n};\n\nsummary.dailySummary = `📊 Daily PPC Performance Summary\n\n${summary.totalCampaigns} ${summary.totalCampaigns === 1 ? 'campaign' : 'campaigns'} analyzed:\n• ${summary.excellentCampaigns} Excellent 🚀\n• ${summary.goodCampaigns} Good ✅\n• ${summary.fairCampaigns} Fair ⚠️\n• ${summary.underperformingCampaigns} Underperforming 🔴\n\nTotal Spend (selected date range): $${summary.totalSpend}\nTotal Conversions: ${summary.totalConversions}`;\n\nreturn [{ json: summary }];"
      },
      "executeOnce": true
    },
    {
      "id": "7936c950-f1d7-4f1a-9ea3-ea693a9955fd",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1552,
        64
      ],
      "parameters": {
        "width": 500,
        "height": 572,
        "content": "## Google Ads daily check\n\nEvery day at 9 AM the workflow reads your Google Ads campaigns, scores each one from 0 to 100 with a fixed formula, saves the results to Google Sheets, alerts Slack and emails a report.\n\n**Before you run it:**\n- Add credentials: Google Ads, Google Sheets, Slack, SMTP (email)\n- Fill in your Google Ads customer ID in Fetch Google Ads Data\n- Choose your spreadsheet (tabs Dashboard and Log) and your Slack channel\n- Set the sender and recipient in Email Performance Report\n- Adjust the scoring thresholds in AI Performance Analysis if needed\n\nSource: based on a template from the n8n Library at n8n.io, adjusted and documented by LeenLabs."
      }
    },
    {
      "id": "ef8d2d41-a0ef-4b1d-818e-f5a7d7226537",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -992,
        112
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Runs every day at 9 AM (time zone of your n8n settings)"
      }
    },
    {
      "id": "fa399e33-4467-47c1-9348-cb158ee868a2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -768,
        112
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Reads your campaigns and their metrics from Google Ads"
      }
    },
    {
      "id": "fb6102c7-dc3e-4c25-971d-06466655d812",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -544,
        112
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Scores each campaign from 0 to 100 with a fixed formula (a rule-based score, not an AI model)"
      }
    },
    {
      "id": "0a0be77c-952d-4b11-ba26-65572a0e736a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -320,
        112
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Excellent or Good goes up, Fair or Underperforming goes down"
      }
    },
    {
      "id": "7ffd9913-010f-4cb0-9254-bbbb90871c2e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -256,
        432
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Adds every campaign to the Log tab (history)"
      }
    },
    {
      "id": "81f36a32-5ee6-4dbf-b84e-b00677bb0264",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        128,
        -80
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Updates one row per campaign in the Dashboard tab"
      }
    },
    {
      "id": "19224e33-b214-421e-b867-73f7e8434669",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -272,
        -96
      ],
      "parameters": {
        "width": 160,
        "height": 128,
        "content": "Slack message for campaigns scored Excellent or Good"
      }
    },
    {
      "id": "a0a6c66c-167f-4bb7-9e88-d15abf368632",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -256,
        624
      ],
      "parameters": {
        "width": 160,
        "height": 128,
        "content": "Slack message for campaigns scored Fair or Underperforming"
      }
    },
    {
      "id": "1f7e7fcd-258b-4bc1-ba66-a6775f5b165c",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -96,
        112
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Writes the email text for each campaign"
      }
    },
    {
      "id": "caa7a611-c83b-4936-bc34-86ee7a405754",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        128,
        416
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Sends the report email (set sender and recipient)"
      }
    },
    {
      "id": "a15c7d78-b442-4af0-a02c-97a46f8fc623",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        352,
        16
      ],
      "parameters": {
        "width": 160,
        "height": 96,
        "content": "Adds up spend, conversions and how many campaigns fall in each tier"
      }
    }
  ],
  "connections": {
    "Generate Action Plan": {
      "main": [
        [
          {
            "node": "Email Performance Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Performance": {
      "main": [
        [
          {
            "node": "Update Campaign Dashboard",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log All Campaigns",
            "type": "main",
            "index": 0
          },
          {
            "node": "Alert: Scale Opportunity",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Action Plan",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Campaign Dashboard",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log All Campaigns",
            "type": "main",
            "index": 0
          },
          {
            "node": "Alert: Issues Detected",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Action Plan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Daily Check": {
      "main": [
        [
          {
            "node": "Fetch Google Ads Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Google Ads Data": {
      "main": [
        [
          {
            "node": "AI Performance Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Performance Analysis": {
      "main": [
        [
          {
            "node": "Route by Performance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Performance Report": {
      "main": [
        [
          {
            "node": "Generate Daily Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Campaign Dashboard": {
      "main": [
        [
          {
            "node": "Generate Daily Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}

Install it in ten steps

  1. Get the JSON from the box above or the download link.
  2. Import it. Open a new empty workflow in n8n, then use the menu to import from a file, or paste the JSON onto the canvas. Menu labels vary slightly between versions.
  3. Check the 11 nodes appear, connected in the order described above, with the notes on the canvas.
  4. Connect Google Ads. Open Fetch Google Ads Data, create the Google Ads credential (this is where the developer token goes), then type your customer ID. Add the manager account ID only if you use one.
  5. Connect Google Sheets. In both Update Campaign Dashboard and Log All Campaigns, add your credential, select your spreadsheet, and select the Dashboard and Log tabs.
  6. Connect Slack. In both alert nodes, add your Slack credential and select the channel.
  7. Set up the email. In Email Performance Report, add your SMTP credential and replace both placeholder addresses (the sender and the recipient).
  8. Adjust the thresholds in AI Performance Analysis if you already know what good numbers look like for your account. You can also do this after a first run.
  9. Run it once by hand. Use the button that executes the workflow and watch each node. A manual run sends real Slack messages and real emails, so use a test channel and your own address first.
  10. Check, then turn it on. Compare the sheet rows with your Google Ads dashboard. When the numbers match, turn the workflow on (the button is called Activate or Publish depending on your n8n version) so the 9 AM schedule starts.

What it will not do: limits, costs and trade-offs

Costs. The workflow file is free, but the services around it have their own plans and limits: n8n (cloud or self-hosted), Google Sheets, Slack and your email provider. Google Ads API access is subject to Google’s own rules. Prices and conditions change, so check each provider’s current page. We do not quote figures here on purpose.

It reports; it does not act. No node in this workflow writes anything to Google Ads. It will not pause a campaign, change a budget or edit a bid. Every decision stays with you.

Seven-day numbers, daily emails. The data covers the last 7 days, while the email arrives daily. Read the figures as “performance over the past week, checked today”.

Enabled campaigns only. The file asks Google Ads for enabled campaigns. Paused campaigns are ignored.

Conversions can change after the fact. Google Ads can update conversion numbers some time after a click happens, so a score is a snapshot, not a final verdict.

Currency. The workflow converts Google’s micros into regular units correctly, but the emails and the summary show a dollar sign. If your account uses another currency, the amounts are right and the symbol is wrong. You can change the symbol in the Code nodes.

One message per campaign. Slack alerts and emails are sent per campaign. An account with many campaigns means many messages every morning. Problem 8 below explains how to reduce that.

Preset thresholds. As explained above, the scoring rules are not industry benchmarks.

Unknown volume limits. We have not tested how many campaigns it can process in one run, so we cannot promise a number.

One account per run. The Google Ads node holds a single customer ID. Monitoring several accounts means adding more nodes, which is beyond this template.

Access and data safety

This workflow handles spending and performance data from your ad account, plus the credentials that let n8n reach it. A few habits keep that safe:

  • Credentials stay in n8n. The JSON file contains none of your logins. Do not paste screenshots or exports that show them.
  • Limit who receives the data. Send the emails to people who should see ad spend, and keep the Google Sheet and the Slack channel restricted to the same group.
  • Use the minimum access you can. Connect the Google account, Slack app and mailbox that the workflow actually needs, and review them from time to time.
  • Know the rules of your ad account. If you manage accounts for clients, make sure you are allowed to export their data to a spreadsheet and by email.

Problems you may meet and how to fix them

Every n8n setup is different, so this workflow can behave differently for you than in our check. Here are the most likely problems. For each one: what you see, why it happens, and what to do. They come from how the nodes are built and from running the Code nodes on sample data, not from a live test with a real ad account.

1. The Google Ads node cannot connect or complains about a token or permission

What you see: an error when running Fetch Google Ads Data, often mentioning a developer token, authorization or permission.

Why: Google Ads API access has several requirements: a developer token, an authorized Google account, and the right customer ID (plus the manager account ID if you reach the account through one).

Fix: check each requirement in Google’s and n8n’s current documentation. Confirm that the Google account you signed in with has access to the ad account, and that the customer ID is the account’s own 10-digit number. If the token is not yet approved for your use, the node cannot work until it is.

2. The node runs but returns no campaigns

What you see: green checks, then nothing happens after Fetch Google Ads Data. No sheet rows, no Slack, no email.

Why: the file asks for enabled campaigns over the last 7 days. If none match, or the customer ID points to an empty account, the workflow ends quietly, because there is nothing to analyze.

Fix: check the customer ID, then check that the account has enabled campaigns. You can also change the date range and status options inside the Google Ads node.

3. Every campaign scores 0 or shows zero clicks

What you see: all campaigns land in Underperforming with metrics at 0.

Why: the scoring node looks for specific field names (clicks, impressions, cost in micros, conversions). The Google Ads node may return different names or a different structure depending on its version. We could not confirm the exact output of your version. The Code node reads two common layouts, and if clicks are missing it uses interactions as a stand-in, but it cannot guess every layout.

Fix: run Fetch Google Ads Data on its own and open its output. Note the real field names for clicks, impressions, cost and conversions. Then open AI Performance Analysis and edit the few lines under “Extract metrics” so they point to those names. Also remember that conversions stay at 0 when conversion tracking is not set up in Google Ads.

4. The workflow never runs at 9 AM

What you see: no new rows or messages in the morning.

Why: a schedule only fires when the workflow is turned on and n8n is running. The hour also follows the time zone set in your n8n instance, which may differ from yours.

Fix: turn the workflow on (Activate or Publish), make sure your n8n instance stays online, and check the time zone in your n8n settings.

5. Google Sheets cannot find the tab, or the columns are empty

What you see: an error about a missing sheet, or rows with blank cells.

Why: the tabs must be named exactly Dashboard and Log, and the first row must contain the twelve headers exactly as listed. The nodes match data to columns by name.

Fix: rename the tabs or select your own tabs in the two nodes, and correct the headers. If you rename a header, update the matching column in the node as well.

6. The Dashboard tab gets duplicate rows

What you see: the same campaign appears several times in the Dashboard tab.

Why: Update Campaign Dashboard finds the row to update through the Campaign ID column. If that header is missing or misspelled, or the ID cell is empty, it cannot match and adds a new row.

Fix: check that the header reads exactly Campaign ID and that the IDs are being filled in. The Log tab is supposed to add rows every day, so duplicates there are normal.

7. Slack stays silent, or the wrong alert fires

What you see: no Slack message, an error such as “not in channel”, or an issues alert for a campaign that looks fine.

Why: the Slack app connected to n8n has not been added to the channel. As for the unexpected alert, remember that Fair campaigns count as “issues” because only Excellent and Good take the other path.

Fix: add the app to the channel and select that channel in both alert nodes. If you want Fair campaigns to count as fine, edit the conditions in Route by Performance.

8. Too many Slack messages and emails

What you see: dozens of messages every morning.

Why: the workflow sends one Slack message and one email per campaign.

Fix: the simplest option is to narrow what gets through. For example, change the conditions in Route by Performance so that only Excellent campaigns take the first path, or disable the nodes you do not need. Sending a single combined message instead needs extra nodes, which is a good candidate for a custom build.

9. The email node fails

What you see: a red Email Performance Report node, with errors about authentication, the sender address or the connection.

Why: the file ships with placeholder addresses, and SMTP needs a correct host, port, security setting and login. Many providers refuse your normal password and require an app password. Some also reject a sender address that does not belong to the mailbox.

Fix: replace both addresses, re-enter the SMTP details from your provider’s documentation, and send from the mailbox you authenticate with.

10. The daily summary looks empty or wrong

What you see: zero campaigns, or totals that seem off.

Why: Generate Daily Summary reads the results of AI Performance Analysis directly. If that node produced no items or zero metrics (see problems 2 and 3), the summary mirrors that.

Fix: solve problems 2 and 3 first. Then run again and open the output of Generate Daily Summary in the execution results.

11. Amounts show the wrong currency symbol

What you see: a dollar sign on amounts from a euro, pound or other account.

Why: the symbol is written into the text of the emails and the summary.

Fix: open Generate Action Plan and Generate Daily Summary and replace the dollar sign in the text. Do not remove the ${ and } pieces, which insert the numbers.

12. A Code node shows a syntax error after you edited it

What you see: a red Code node with a syntax error.

Why: code copied from some web pages arrives with special characters turned into their web-encoded form. For example, the arrow => that JavaScript uses can appear as =&gt;, which JavaScript cannot read. Generate Action Plan and Generate Daily Summary both contain such arrows.

Fix: replace any =&gt; with =>. To avoid the problem, import the downloadable file instead of copying code from a web page.

13. A node shows a question mark or a version warning

What you see: a node that n8n does not recognize, or a message about an unsupported node version.

Why: the file uses current node versions (for example Google Sheets 4.5 and Slack 2.3). An older n8n may not know them, and a node may have changed in a newer one.

Fix: update n8n if you can. If not, delete the affected node, add a fresh one of the same kind from the nodes panel, give it the same name, and reconnect it. The settings to copy are described in the steps above.

Too much of a puzzle? Let us build or adapt it for you

Some people enjoy tinkering with n8n and with Google Ads settings. Others have campaigns to run and want a workflow that simply works on their own accounts. If you are in the second group, LeenLabs builds and adapts n8n workflows for people like you. We can:

  • Tune this workflow to your numbers: thresholds, scoring rules and email wording that match your industry and your goals.
  • Set it up on your n8n with your accounts, so it is ready to run.
  • Change what it sends and where: one combined daily message instead of one per campaign, a different channel, or a different tool.
  • Build a new workflow from scratch for another task you want to automate.

Request a Custom Workflow, describe what you want to automate, and we will come back to you with the next steps. Prefer to ask a question first? Write to support@leenlabs.site.

What we changed in the original template

We want you to know exactly how this file differs from the template it comes from. Compared with the version we received for testing, LeenLabs made these changes:

  1. Real nodes instead of test nodes. For testing, Google Ads, Google Sheets, Slack and email were replaced by placeholder nodes. The published file uses the real nodes, with empty fields for your own settings.
  2. A clear daily time. The schedule is set to every day at 9 AM. The test copy used a 24-hour interval, which counts from the moment the workflow is turned on.
  3. A summary that calculates correctly. The original summary node referred to its own result, and the nodes feeding it no longer carry the scoring data once real Google Sheets and email nodes are used. Generate Daily Summary is now a Code node that reads the scoring results directly and runs once.
  4. More tolerant field reading. AI Performance Analysis now accepts nested or flat Google Ads output, and uses interactions when clicks are missing.
  5. A more accurate label. The emails and summary now say “Spend (selected date range)” instead of “Daily Spend”, because the data covers the date range set in the Google Ads node.
  6. Cleaner notes on the canvas. We removed promotional text and outside links from the sticky notes and rewrote them as setup reminders.

Logic verified with simulated data on n8n 2.38.5; not tested on live APIs

Here is exactly what that means. We ran a check on n8n 2.38.5 in which Google Ads, Google Sheets, Slack and email were replaced by mock nodes that return placeholder fields. Those placeholders contained no campaign metrics, so the scoring node worked on zero values. Every simulated campaign therefore landed in Underperforming and took the “issues” path. The “scale opportunity” path was not exercised with realistic numbers in that check. We also did not wait for a real daily trigger.

The published JSON replaces the mock nodes with real ones and includes the changes listed above, so its Google Ads, Google Sheets, Slack and email settings have never been run against live accounts. We did run the three Code nodes separately on sample campaigns with different metrics, and they produced the expected tiers, emails and summary. That confirms the JavaScript, not the way n8n will talk to your accounts. This is why we recommend a first manual run with a test Slack channel and your own email address, followed by a comparison with your Google Ads dashboard.

Three ways to adapt the workflow

  1. Make the scoring yours. Open AI Performance Analysis and change the numbers in the rules, for example the CTR levels, or the cost per conversion limits. Start with the cost per conversion, because that is the rule most tied to your own margins.
  2. Send the daily summary to Slack. Add a Slack node after Generate Daily Summary and use its dailySummary field as the message text. This gives you one overview message in addition to the per-campaign alerts.
  3. Change the date range or the hour. In Fetch Google Ads Data you can change the date range and the campaign status. In Schedule Daily Check you can change the hour. If you pick a shorter range, remember that the scores will then reflect that range.

These variations are suggestions based on how n8n works, not changes we have tested on this template. If you want one of them built for you, send us a request.

Frequently asked questions

Is this really AI?

No. The node called AI Performance Analysis applies fixed rules and adds up points. It does not learn from your data or use a language model.

Will it change my campaigns or budgets?

No. The workflow only reads from Google Ads. Suggestions such as raising a budget appear in the messages for you to decide on.

Is the workflow free?

The workflow file is free. The tools it connects have their own plans and limits.

Do I need all four outputs (sheet, Slack, email)?

No. If you do not want one of them, disable or delete that node. The others keep their own connections, but check the result of a run after any change.

Can it monitor several Google Ads accounts?

Not as it stands. The Google Ads node holds one customer ID. Covering more accounts needs extra nodes, which is something we can build for you.

What if the JSON does not work on my n8n?

It can happen, because setups differ. Go through the problems section above: each common issue comes with a fix. If it still does not work, or you do not want to spend time on it, request a custom workflow and we will handle it.

Who built the original template?

The original template comes from the n8n Library at n8n.io. LeenLabs adjusted and documented it.

Tools used in this workflow

The links below go to each tool’s home page, and some may be affiliate links, as explained in our Terms of Service.

  • n8n (may be an affiliate link): the automation platform that runs the workflow, available hosted or self-hosted.
  • Google Ads (may be an affiliate link): the advertising platform that provides the campaign data.
  • Google Sheets (may be an affiliate link): the spreadsheet that holds your dashboard and history.
  • Slack (may be an affiliate link): the messaging app that carries the alerts.

Keep exploring

Browse more free n8n workflows from LeenLabs.

Need this adapted to your own setup?

Tell us what you want to automate and we will come back to you with the next steps.

Request a Custom Workflow

Leave a Reply

Your email address will not be published. Required fields are marked *