{
  "name": "GSC weekly report by email",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 7
            }
          ]
        }
      },
      "id": "2f6a1c1e-5b0d-4d7e-9a43-1c8b0f6e7a01",
      "name": "Monday 07:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "id": "b2c7d9e1-3f4a-4b6c-8d2e-6f1a3c5b7d06",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1a2b3c4-d5e6-4f70-8a91-0b2c3d4e5f07",
              "name": "property",
              "value": "sc-domain:example.com",
              "type": "string"
            },
            {
              "id": "d2b3c4d5-e6f7-4a81-9b02-1c3d4e5f6a08",
              "name": "fromEmail",
              "value": "reports@example.com",
              "type": "string"
            },
            {
              "id": "e3c4d5e6-f7a8-4b92-8c13-2d4e5f6a7b09",
              "name": "toEmail",
              "value": "you@example.com",
              "type": "string"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($('Config').first().json.property) }}/searchAnalytics/query",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSearchConsoleOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ startDate: $today.minus({ days: 9 }).toFormat('yyyy-MM-dd'), endDate: $today.minus({ days: 3 }).toFormat('yyyy-MM-dd'), dimensions: ['query'], rowLimit: 1000 }) }}",
        "options": {}
      },
      "id": "7c3e9d52-0a4b-4f1e-8d6c-2b9a5e4f3a02",
      "name": "GSC last 7 days",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($('Config').first().json.property) }}/searchAnalytics/query",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSearchConsoleOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ startDate: $today.minus({ days: 16 }).toFormat('yyyy-MM-dd'), endDate: $today.minus({ days: 10 }).toFormat('yyyy-MM-dd'), dimensions: ['query'], rowLimit: 1000 }) }}",
        "options": {}
      },
      "id": "a91b4f07-6e2d-4c3a-b5f8-3d0c7e1a9b03",
      "name": "GSC previous 7 days",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Compares the last 7 days of Search Console query rows with the 7 days before.\n// Totals are sums over the returned query rows: Search Console leaves out\n// anonymized queries, so they come out lower than the Performance report totals.\nconst current = $('GSC last 7 days').first().json.rows ?? [];\nconst previous = $('GSC previous 7 days').first().json.rows ?? [];\nif (current.length === 0) {\n  throw new Error('Search Console returned no query rows for the last 7 days. Check the property in the Config node and that the Google account behind the credential has access to it in Search Console.');\n}\nconst sum = (rows, key) => rows.reduce((s, r) => s + (r[key] ?? 0), 0);\nconst signed = (n) => (n > 0 ? '+' : '') + n;\nconst before = new Map(previous.map((r) => [r.keys[0], r.clicks]));\nconst clicks = sum(current, 'clicks');\nconst impressions = sum(current, 'impressions');\nconst clicksBefore = sum(previous, 'clicks');\nconst impressionsBefore = sum(previous, 'impressions');\nconst top = [...current]\n  .sort((a, b) => b.clicks - a.clicks)\n  .slice(0, 10)\n  .map((r, i) => {\n    const prev = before.get(r.keys[0]);\n    // Missing from the previous rows is not the same as new: both requests return at most\n    // 1,000 rows and Search Console may omit rows.\n    const change = prev === undefined ? 'not in the previous rows' : signed(r.clicks - prev);\n    return (i + 1) + '. ' + r.keys[0] + ': ' + r.clicks + ' clicks (' + change + ')';\n  });\nconst text = [\n  'Clicks: ' + clicks + ' (' + signed(clicks - clicksBefore) + ' vs the previous 7 days)',\n  'Impressions: ' + impressions + ' (' + signed(impressions - impressionsBefore) + ')',\n  previous.length === 0 ? 'The previous 7 days returned no rows, so no query has a change.' : '',\n  '',\n  'Top 10 queries by clicks:',\n  ...top,\n].filter((line, i) => line !== '' || i === 3).join('\\n');\nreturn [{ json: { subject: 'Search Console weekly: ' + clicks + ' clicks (' + signed(clicks - clicksBefore) + ')', text } }];"
      },
      "id": "d4e8a2b6-1f9c-4a7d-9e3b-4c2f8a6d1e04",
      "name": "Compare weeks",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "fromEmail": "={{ $('Config').first().json.fromEmail }}",
        "toEmail": "={{ $('Config').first().json.toEmail }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.text }}",
        "options": {}
      },
      "id": "e5f1b3c7-2a0d-4b8e-8f4c-5d3a9b7e2f05",
      "name": "Send report",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1100,
        0
      ]
    },
    {
      "id": "f4d5e6f7-a8b9-4c03-9d24-3e5f6a7b8c10",
      "name": "Note: before you start",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -300
      ],
      "parameters": {
        "content": "## Before you start\n\n1. **Access.** The Google account you connect below must see the property in Search Console. Any permission level works, restricted user included. No property in its list? A verified owner adds the account under Settings → Users and permissions.\n2. **Google Cloud.** Pick or create a project and enable the **Google Search Console API** (APIs & Services → Library).\n3. **OAuth consent screen.** Internal if you are on Google Workspace; otherwise External with your account added as a test user. An External app in Testing mode loses consent and tokens after 7 days: reconnect the credential or publish the app.\n4. **OAuth client.** Credentials → OAuth client ID → Web application. Paste the OAuth Redirect URL from the n8n credential form into Authorized redirect URIs, character for character.\n5. **n8n credential.** Google Search Console OAuth2 API with that client ID and secret, then Sign in with Google. Not in your credential list? Use Google OAuth2 API with the scope `https://www.googleapis.com/auth/webmasters.readonly` and pick it in both HTTP nodes instead.\n6. **SMTP credential** for the email node.\n\nThen work through the numbered notes from left to right.",
        "height": 640,
        "width": 540
      }
    },
    {
      "id": "a5e6f7a8-b9c0-4d14-8e35-4f6a7b8c9d11",
      "name": "Note: step 1 config",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        160,
        -300
      ],
      "parameters": {
        "content": "### 1 · Set your property and addresses\n\nOpen **Config** and change:\n- `property`: copy it from the Search Console property picker. Domain property: `sc-domain:example.com`. URL prefix: `https://www.example.com/`, with the trailing slash.\n- `fromEmail`, `toEmail`.",
        "height": 260,
        "width": 220
      }
    },
    {
      "id": "b6f7a8b9-c0d1-4e25-9f46-5a7b8c9d0e12",
      "name": "Note: step 2 credentials",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        400,
        -300
      ],
      "parameters": {
        "content": "### 2 · Pick the credential in BOTH nodes\n\nOpen **GSC last 7 days** and **GSC previous 7 days** one by one and select your Google Search Console OAuth2 API credential in each. A red warning on a node means it still has no credential.\n\nThe URL reads the property from Config; do not edit it here.",
        "height": 260,
        "width": 420
      }
    },
    {
      "id": "c7a8b9c0-d1e2-4f36-8a57-6b8c9d0e1f13",
      "name": "Note: step 3 smtp",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1040,
        -300
      ],
      "parameters": {
        "content": "### 3 · Pick your SMTP credential\n\nOpen **Send report** and select it. The addresses come from Config.",
        "height": 260,
        "width": 240
      }
    },
    {
      "id": "d8b9c0d1-e2f3-4a47-9b68-7c9d0e1f2a14",
      "name": "Note: step 4 test",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        820,
        160
      ],
      "parameters": {
        "content": "### 4 · Test, then switch it on\n\nClick **Execute workflow** and read the email. An empty last week stops the run at **Compare weeks** with an error, so nothing is sent. When the email looks right, activate the workflow: it runs every Monday at 07:00 in your instance's timezone.",
        "height": 180,
        "width": 460
      }
    }
  ],
  "connections": {
    "Monday 07:00": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "GSC last 7 days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GSC last 7 days": {
      "main": [
        [
          {
            "node": "GSC previous 7 days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GSC previous 7 days": {
      "main": [
        [
          {
            "node": "Compare weeks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare weeks": {
      "main": [
        [
          {
            "node": "Send report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}
