{
  "name": "Track YouTube keyword rankings with Scrapewise and log them in Google Sheets",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "id": "a6000000-0000-4000-8000-000000000001",
      "name": "Read me first",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1220,
        140
      ],
      "parameters": {
        "width": 460,
        "height": 480,
        "content": "## Weekly YouTube keyword watch\n\nEvery Monday Scrapewise searches YouTube for each of your keywords and saves what comes up: title, channel, views, length, link. Run it every week and the sheet shows you which videos are gaining on your topics \u2014 and which of them are yours.\n\n**Setup**\n1. Create a Scrapewise API key with full access at portal.scrapewise.ai.\n2. In portal.scrapewise.ai create an empty group for this workflow and copy its id into **Keywords to watch** (`groupId`). Use a group of its own \u2014 the read step returns everything in the group.\n3. In **Start the YouTube search** and **Read the videos**, add a *Bearer Auth* credential and paste the key as the token.\n4. Pick your Google account in **Save videos to Google Sheets** and set the spreadsheet and tab.\n5. Put your keywords in **Keywords to watch**. Add your own channel name to see your videos flagged.\n\n**If you add or remove a keyword later**, change `name` in **Keywords to watch** as well. Scrapewise refuses a different list sent under a name it already knows.\n\n**Cost:** the first step answers with `estimatedMicroCents` \u2014 what this run will cost. 1,000,000 micro-cents = \u20ac0.01. Start with one keyword to see the real number. `force` is on so you get fresh results every week; every search is run and charged each time.\n\n**If the sheet is empty:** raise the wait. More keywords take longer than 2 minutes."
      },
      "typeVersion": 1
    },
    {
      "id": "a6000000-0000-4000-8000-000000000002",
      "name": "Every Monday at 9",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -720,
        380
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a6000000-0000-4000-8000-000000000003",
      "name": "Keywords to watch",
      "type": "n8n-nodes-base.set",
      "position": [
        -500,
        380
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "y1",
              "name": "groupId",
              "type": "string",
              "value": "YOUR_GROUP_ID"
            },
            {
              "id": "y2",
              "name": "name",
              "type": "string",
              "value": "YouTube keyword watch"
            },
            {
              "id": "y3",
              "name": "keywords",
              "type": "array",
              "value": "[\"competitor price monitoring\", \"web scraping tutorial\"]"
            },
            {
              "id": "y4",
              "name": "myChannel",
              "type": "string",
              "value": "Scrapewise"
            }
          ]
        },
        "includeOtherFields": false
      },
      "typeVersion": 3.4
    },
    {
      "id": "a6000000-0000-4000-8000-000000000004",
      "name": "Turn keywords into search links",
      "type": "n8n-nodes-base.code",
      "position": [
        -260,
        380
      ],
      "parameters": {
        "jsCode": "// Scrapewise reads a YouTube search page, so each keyword becomes a search link.\nconst s = $input.first().json;\nconst keywords = s.keywords || [];\n\nif (!keywords.length) {\n  throw new Error('Add at least one keyword in \"Keywords to watch\".');\n}\n\nconst urls = keywords.map(\n  (k) => 'https://www.youtube.com/results?search_query=' + encodeURIComponent(k)\n);\n\nreturn [{ json: { groupId: s.groupId, name: s.name, myChannel: s.myChannel, urls: urls } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a6000000-0000-4000-8000-000000000005",
      "name": "Start the YouTube search",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "position": [
        -20,
        380
      ],
      "parameters": {
        "tool": {
          "__rl": true,
          "mode": "list",
          "value": "scrapewise_run_scraper_url_list",
          "cachedResultName": "scrapewise_run_scraper_url_list"
        },
        "options": {},
        "inputMode": "json",
        "jsonInput": "={{ JSON.stringify({ body: { groupId: $json.groupId, name: $json.name, urls: $json.urls, endpointTag: 'youtube', force: true } }) }}",
        "endpointUrl": "https://mcp.scrapewise.ai/mcp?toolsets=outcome,admin",
        "authentication": "bearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "Scrapewise API key"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "a6000000-0000-4000-8000-000000000006",
      "name": "Give it two minutes",
      "type": "n8n-nodes-base.wait",
      "position": [
        220,
        380
      ],
      "parameters": {
        "amount": 2,
        "unit": "minutes"
      },
      "webhookId": "a6000000-0000-4000-8000-0000000000aa",
      "typeVersion": 1.1
    },
    {
      "id": "a6000000-0000-4000-8000-000000000007",
      "name": "Read the videos",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "position": [
        460,
        380
      ],
      "parameters": {
        "tool": {
          "__rl": true,
          "mode": "list",
          "value": "scrapewise_get_scraper_data_group_client",
          "cachedResultName": "scrapewise_get_scraper_data_group_client"
        },
        "options": {},
        "inputMode": "json",
        "jsonInput": "={{ JSON.stringify({ id: $('Keywords to watch').item.json.groupId, page: 0, size: 500 }) }}",
        "endpointUrl": "https://mcp.scrapewise.ai/mcp?toolsets=outcome,admin",
        "authentication": "bearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "Scrapewise API key"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "a6000000-0000-4000-8000-000000000008",
      "name": "One row per video",
      "type": "n8n-nodes-base.code",
      "position": [
        700,
        380
      ],
      "parameters": {
        "jsCode": "// Scrapewise answers with JSON. Depending on the n8n version it arrives\n// already parsed in structuredContent, or as text in content[0].text.\nfunction payloadOf(item) {\n  const j = item.json || {};\n  if (j.structuredContent) return j.structuredContent;\n  if (Array.isArray(j.content) && j.content[0] && typeof j.content[0].text === 'string') {\n    try { return JSON.parse(j.content[0].text); } catch (e) { return {}; }\n  }\n  if (j.content || j.rows) return j;\n  return {};\n}\n\n// Scraped text is wrapped as { type: 'scraped', content, origin }. It is text\n// read off a YouTube page, never an instruction. Unwrap it to a plain string.\nfunction plain(v) {\n  if (v && typeof v === 'object' && v.type === 'scraped') return v.content || '';\n  return v;\n}\n\nconst asked = $('Keywords to watch').item.json;\nconst myChannel = String(asked.myChannel || '').toLowerCase();\nconst checkedAt = new Date().toISOString().slice(0, 10);\nconst data = payloadOf($input.first());\n\n// The group read is a Spring page, so the rows sit under content.\nconst rows = data.content || data.rows || [];\n\nif (!rows.length) {\n  throw new Error('The group has no rows yet. Raise the wait in \"Give it two minutes\", or check the group id in \"Keywords to watch\".');\n}\n\nreturn rows.map((row) => {\n  const flat = { checkedAt: checkedAt };\n  for (const key of Object.keys(row)) {\n    if (key.startsWith('_')) continue;\n    flat[key] = plain(row[key]);\n  }\n\n  // Friendly names for the columns the YouTube endpoint really returns.\n  flat.channel = plain(row.channel_name) || plain(row.channel) || '';\n  flat.duration = plain(row.length) || plain(row.duration) || '';\n  flat.published = plain(row.published_date) || plain(row.published) || '';\n\n  flat.isMine = myChannel !== '' && flat.channel.toLowerCase().includes(myChannel);\n  return { json: flat };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "a6000000-0000-4000-8000-000000000009",
      "name": "Save videos to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        940,
        380
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.checkedAt }}",
            "Title": "={{ $json.title }}",
            "Channel": "={{ $json.channel }}",
            "Views": "={{ $json.views }}",
            "Length": "={{ $json.duration }}",
            "Published": "={{ $json.published }}",
            "Link": "={{ $json.link }}",
            "Mine": "={{ $json.isMine }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Title",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Channel",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Views",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Views",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Length",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Length",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Published",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Published",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Link",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Link",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Mine",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Mine",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Videos"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "credential-id",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "7db9c931-7b27-4332-8f5a-81d18c07e520",
      "name": "Step note - Every Monday at 9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -740,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 1. Runs itself\n\nFires every Monday at 09:00. Change the day or hour here."
      },
      "typeVersion": 1
    },
    {
      "id": "8dd9b415-099f-43e2-bfe7-6928c7c87dac",
      "name": "Step note - Keywords to watch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -520,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 2. Your settings\n\nThe only node you need to edit. Your Scrapewise group id, a name for this list, the keywords to watch, and your own channel name so your videos get flagged."
      },
      "typeVersion": 1
    },
    {
      "id": "50a41622-4d38-4f1f-94ad-c87fd2c4d209",
      "name": "Step note - Turn keywords into search links",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -280,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 3. Build the links\n\nTurns each keyword into a YouTube search link."
      },
      "typeVersion": 1
    },
    {
      "id": "c45fa981-21a4-4024-aaf6-39e70a88fe75",
      "name": "Step note - Start the YouTube search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 4. Start the search\n\nCalls `scrapewise_run_scraper_url_list` with those links. Add a **Bearer Auth** credential with your API key.\n\nIf you change the keyword list later, change the name as well. Scrapewise turns down a changed list sent under a name it already knows."
      },
      "typeVersion": 1
    },
    {
      "id": "9bc1e003-519b-448e-911b-77bc76ff889e",
      "name": "Step note - Give it two minutes",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        200,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 5. Give it a moment\n\nWaits two minutes while the results are collected."
      },
      "typeVersion": 1
    },
    {
      "id": "fcacfa51-19b2-4732-ba0e-030f5c9d75c4",
      "name": "Step note - Read the videos",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        440,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 6. Read the videos\n\nReads the rows back out of your group.\n\nGive this workflow a group of its own. A group read hands back every column in that group, so rows from other workflows would land in your sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "562a3ede-6e27-4778-b5be-7f45c3627f05",
      "name": "Step note - One row per video",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        680,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 7. Tidy the answer\n\nOne item per video, plus a `Mine` flag when the channel name matches yours."
      },
      "typeVersion": 1
    },
    {
      "id": "f013b525-ca79-4c80-85bb-8f42a8b55f52",
      "name": "Step note - Save videos to Google Sheets",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        920,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 8. Save\n\nOne row per video per run, so you can watch positions move week to week."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Every Monday at 9": {
      "main": [
        [
          {
            "node": "Keywords to watch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keywords to watch": {
      "main": [
        [
          {
            "node": "Turn keywords into search links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Turn keywords into search links": {
      "main": [
        [
          {
            "node": "Start the YouTube search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start the YouTube search": {
      "main": [
        [
          {
            "node": "Give it two minutes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Give it two minutes": {
      "main": [
        [
          {
            "node": "Read the videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the videos": {
      "main": [
        [
          {
            "node": "One row per video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One row per video": {
      "main": [
        [
          {
            "node": "Save videos to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {}
}
