{
  "name": "Lection change alerts: new rows to Slack and Google Sheets",
  "nodes": [
    {
      "parameters": {
        "content": "## Lection change alerts (new rows only)\n1. Create a Google Sheet with a header row that includes your unique column (for example `product_url` or `place_id`).\n2. Set **dedupeColumn** in the *Settings* node to that exact column name (it is used to find new rows).\n3. Set the **Sheet ID** in both Google Sheets nodes and the **Slack channel** in the *Slack notify* node.\n4. Set `REPLACE_WITH_LECTION_SECRET` in *Secret matches?* (or delete that node if you do not use a secret).\n5. Activate, then paste the production URL into Lection's Webhook delivery.",
        "height": 320,
        "width": 640,
        "color": 4
      },
      "id": "aa7923cb-ae7c-42c8-98a4-f29201bfe267",
      "name": "Setup notes B",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -260
      ]
    },
    {
      "parameters": {
        "content": "### How new rows are found\nThe *Find new rows* Code node reads every existing key from the sheet and keeps only incoming records whose key is not already there. Rows without a key are skipped (they cannot be deduplicated). Keys are compared as strings.\nIf nothing is new, the workflow only responds 200.",
        "height": 180,
        "width": 640,
        "color": 5
      },
      "id": "ad4f182f-258e-4557-b77e-bd30886ddb88",
      "name": "Logic note B",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        100
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "REPLACE-ME-lection-path",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "001de596-8959-44de-ab2d-13222ce9c90f",
      "name": "Lection webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "webhookId": "b79bb47c-87e9-4bc0-b9bc-9a67eb9bef38"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c0495cd9-c79a-4f47-9fec-9e3088c82642",
              "leftValue": "={{ $json.headers['x-lection-secret'] }}",
              "rightValue": "REPLACE_WITH_LECTION_SECRET",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "1f356a66-ebfe-4c19-8035-758a2cf3110c",
      "name": "Secret matches?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "Unauthorized",
        "options": {
          "responseCode": 401
        }
      },
      "id": "a499739f-22db-4b59-a7cd-2accac6db9e7",
      "name": "Respond 401",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        480,
        500
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a6db5021-d576-4622-a138-ead38fc39b96",
              "leftValue": "={{ $json.body.event }}",
              "rightValue": "scheduled_scrape.completed",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "fdd9cd12-5211-4625-83e5-8df119b32da2",
              "leftValue": "={{ $json.body.dataset.rowCount }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7d3fb045-81ea-4ce0-a9f0-d4d024438e89",
      "name": "Completed run with rows?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\"ok\": true, \"newRows\": 0, \"note\": \"Not a completed run with rows; nothing checked.\"}",
        "options": {}
      },
      "id": "45e591c7-4b56-41e3-94cc-faa82670df88",
      "name": "Respond skipped",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        720,
        500
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "28f7b505-a965-4e16-bd30-19df4dac9948",
              "name": "dedupeColumn",
              "value": "REPLACE_UNIQUE_COLUMN",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "52be1afb-0061-476a-af7e-22843e31d967",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        720,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_SPREADSHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Sheet1"
        },
        "options": {}
      },
      "id": "c4d89ed0-7cd8-4150-a0a9-5af999a98b42",
      "name": "Read existing rows",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        960,
        300
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_CREDENTIAL_ID",
          "name": "Google Sheets account"
        }
      },
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const KEY = $('Settings').first().json.dedupeColumn;\nconst incoming = $('Completed run with rows?').first().json.body.dataset.records ?? [];\nconst seen = new Set();\nfor (const item of $input.all()) {\n  const v = item.json[KEY];\n  if (v !== undefined && v !== null && v !== '') seen.add(String(v));\n}\nconst fresh = [];\nfor (const record of incoming) {\n  const v = record[KEY];\n  if (v === undefined || v === null || v === '') continue;\n  const key = String(v);\n  if (seen.has(key)) continue;\n  seen.add(key);\n  fresh.push({ json: record });\n}\nreturn fresh.length ? fresh : [{ json: { __noNewRows: true } }];\n"
      },
      "id": "c66d17c9-1fc1-4092-a230-a88fbe5edf5b",
      "name": "Find new rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c22c5dbf-b428-4a23-b058-3008a0f9e8f2",
              "leftValue": "={{ $json.__noNewRows }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7302039b-bac0-43c8-9f0c-7fd723cbfcd9",
      "name": "No new rows?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1440,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\"ok\": true, \"newRows\": 0}",
        "options": {}
      },
      "id": "ff48d49f-c517-4ca1-ba6d-d118fdef9856",
      "name": "Respond no new rows",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1680,
        460
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_SPREADSHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Sheet1"
        },
        "options": {},
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "id": "e89fc7c7-a632-482f-acf7-ec2ce952bcc6",
      "name": "Append new rows",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1680,
        300
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_CREDENTIAL_ID",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const fresh = $('Find new rows').all().map(i => i.json);\nconst name = $('Completed run with rows?').first().json.body.schedule?.name ?? 'Lection schedule';\nconst KEY = $('Settings').first().json.dedupeColumn;\nconst lines = fresh.slice(0, 20).map(r => `• ${r[KEY]}`);\nconst more = fresh.length > 20 ? `\\n…and ${fresh.length - 20} more` : '';\nconst text = `*${name}*: ${fresh.length} new row(s)\\n` + lines.join('\\n') + more;\nreturn [{ json: { text, count: fresh.length } }];\n"
      },
      "id": "1ca0dfdf-f575-4752-b0ad-6680a0db404e",
      "name": "Build alert",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        300
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#REPLACE_CHANNEL"
        },
        "messageType": "text",
        "text": "={{ $json.text }}",
        "otherOptions": {}
      },
      "id": "abb92c59-de6b-42cf-b2bf-127cfc9502fc",
      "name": "Slack notify",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        2160,
        300
      ],
      "credentials": {
        "slackApi": {
          "id": "REPLACE_CREDENTIAL_ID",
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"ok\": true, \"newRows\": $('Build alert').first().json.count } }}",
        "options": {}
      },
      "id": "3bb1d78a-97d2-4d3e-a0ec-72450d401c2e",
      "name": "Respond 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2400,
        300
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Lection webhook": {
      "main": [
        [
          {
            "node": "Secret matches?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Secret matches?": {
      "main": [
        [
          {
            "node": "Completed run with rows?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Completed run with rows?": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond skipped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Read existing rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read existing rows": {
      "main": [
        [
          {
            "node": "Find new rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find new rows": {
      "main": [
        [
          {
            "node": "No new rows?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No new rows?": {
      "main": [
        [
          {
            "node": "Respond no new rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append new rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append new rows": {
      "main": [
        [
          {
            "node": "Build alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build alert": {
      "main": [
        [
          {
            "node": "Slack notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack notify": {
      "main": [
        [
          {
            "node": "Respond 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "900d249e-3d10-40db-ad61-914e3473d2d4",
  "meta": {
    "templateCredsSetupCompleted": false,
    "description": "Receives Lection webhooks, finds rows whose unique key is not yet in the sheet, appends only those, and posts a Slack summary."
  },
  "tags": []
}
