{
  "name": "Events Extractor \u2192 Google Sheets",
  "nodes": [
    {
      "id": "sticky-doc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        -400
      ],
      "parameters": {
        "width": 520,
        "height": 1000,
        "content": "## Events Extractor \u2192 Google Sheets\n\n### How it works\n\nThis workflow runs daily and reads a list of event-listing page URLs from a Google Sheet. For each URL, it captures the fully-rendered page HTML with Snapshot Site, cleans it, and passes it to an AI Information Extractor that pulls out structured event fields. Extracted events are appended to a results sheet (deduplicated by registration URL); comparison failures are logged to an errors sheet instead of stopping the run.\n\n### Setup steps\n\n- Install the `n8n-nodes-snapshot-site` community node (Settings \u2192 Community nodes \u2192 Install \u2192 `n8n-nodes-snapshot-site`) \u2014 verified by n8n.\n- Add a Snapshot Site credential, a Google Sheets credential, and an AI chat model credential (e.g. OpenAI) for the Information Extractor.\n- Create a source sheet with a `url` column (one event page per row) and a results sheet with columns matching the extracted fields (see the workflow README for the full list).\n- Paste this exact header row into row 1 of the **Events** tab (tab-separated, one column per cell): `eventName\torganizer\tcategory\tstartDate\tendDate\ttime\tvenue\taddress\tcity\tcountry\tonlineOrOffline\tticketPrice\tregistrationUrl\tdescription`\n- Paste this header row into row 1 of the **Extraction Errors** tab: `url\tmessage\toccurredAt`\n- After importing, open the two Google Sheets write nodes (Log Extraction Error, Save Events) and switch Mapping Column Mode from \"Map Each Column Manually\" to \"Map Automatically\" \u2014 it always imports blank regardless of the JSON, but since the sheet headers above already match the incoming field names, this needs no manual typing.\n- Point the two Google Sheets nodes at your spreadsheet and adjust the schedule.\n\n### Customization\n\nAdd more source columns (e.g. a `region` tag), swap the AI model, or point the same Clean HTML \u2192 Extractor pattern at a different page type \u2014 this is the base pattern for every extraction workflow in this collection."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-zone-loop",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -360,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 820,
        "height": 380,
        "content": "## Read sources and loop\n\nReads every event page URL from the source sheet and processes them one at a time."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-zone-capture",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        500,
        -220
      ],
      "parameters": {
        "color": 7,
        "width": 780,
        "height": 720,
        "content": "## Capture and clean\n\nCaptures the fully-rendered HTML, checks for API errors, and strips scripts/styles before the AI step."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-zone-extract",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1340,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 780,
        "height": 600,
        "content": "## Extract and save\n\nExtracts structured event fields with AI, splits the array into individual rows, and appends them to the results sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "When Every Day at 10AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -280,
        60
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 10
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "read-event-sources",
      "name": "Read Event Sources",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        0,
        60
      ],
      "parameters": {
        "resource": "sheet",
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Event Sources"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/PLACEHOLDER/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "loop-over-sources",
      "name": "Loop Over Event Sources",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        280,
        60
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "all-sources-processed",
      "name": "All Sources Processed",
      "type": "n8n-nodes-base.noOp",
      "position": [
        560,
        -100
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "get-page-html",
      "name": "Get Page HTML",
      "type": "n8n-nodes-snapshot-site.snapshotSite",
      "onError": "continueErrorOutput",
      "maxTries": 3,
      "position": [
        560,
        140
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "operation": "screenshot",
        "additionalFields": {
          "delay": 5,
          "format": "html"
        }
      },
      "credentials": {
        "snapshotSiteApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 5000
    },
    {
      "id": "if-api-error",
      "name": "Check for API Error",
      "type": "n8n-nodes-base.if",
      "position": [
        840,
        140
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.error === true }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "build-error-record",
      "name": "Build Error Record",
      "type": "n8n-nodes-base.set",
      "position": [
        840,
        340
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "url",
              "type": "string",
              "value": "={{ $('Loop Over Event Sources').item.json.url }}"
            },
            {
              "name": "message",
              "type": "string",
              "value": "={{ $json.error?.message || $json.message }}"
            },
            {
              "name": "occurredAt",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "log-extraction-error",
      "name": "Log Extraction Error",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1120,
        340
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": []
        },
        "resource": "sheet",
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Extraction Errors"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/PLACEHOLDER/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "clean-html",
      "name": "Clean HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        60
      ],
      "parameters": {
        "jsCode": "const MAX_CHARS = 100000;\nconst raw = $input.item.json.html || '';\nlet cleaned = raw\n  .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n  .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n  .replace(/<svg[\\s\\S]*?<\\/svg>/gi, '')\n  .replace(/<!--[\\s\\S]*?-->/g, '')\n  .replace(/\\s+/g, ' ')\n  .trim();\nif (cleaned.length > MAX_CHARS) {\n  cleaned = cleaned.slice(0, MAX_CHARS);\n}\nreturn [{ json: { url: $input.item.json.url, cleanedHtml: cleaned } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "extract-event-fields",
      "name": "Extract Event Fields",
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "position": [
        1400,
        60
      ],
      "parameters": {
        "text": "={{ $json.cleanedHtml }}",
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"events\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"eventName\": { \"type\": \"string\" },\n          \"organizer\": { \"type\": \"string\" },\n          \"category\": { \"type\": \"string\" },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"time\": { \"type\": \"string\" },\n          \"venue\": { \"type\": \"string\" },\n          \"address\": { \"type\": \"string\" },\n          \"city\": { \"type\": \"string\" },\n          \"country\": { \"type\": \"string\" },\n          \"onlineOrOffline\": { \"type\": \"string\" },\n          \"ticketPrice\": { \"type\": \"string\" },\n          \"registrationUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" }\n        }\n      }\n    }\n  }\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "extraction-model",
      "name": "Extraction Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1400,
        260
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "split-events",
      "name": "Split Events",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1680,
        60
      ],
      "parameters": {
        "fieldToSplitOut": "output.events"
      },
      "typeVersion": 1
    },
    {
      "id": "save-events",
      "name": "Save Events",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1960,
        60
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "registrationUrl"
          ]
        },
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Events"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/PLACEHOLDER/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "timezone": "UTC",
    "executionOrder": "v1",
    "executionTimeout": 300,
    "saveManualExecutions": true
  },
  "connections": {
    "Clean HTML": {
      "main": [
        [
          {
            "node": "Extract Event Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Events": {
      "main": [
        [
          {
            "node": "Loop Over Event Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Events": {
      "main": [
        [
          {
            "node": "Save Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Page HTML": {
      "main": [
        [
          {
            "node": "Check for API Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Error Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extraction Model": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Event Fields",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Build Error Record": {
      "main": [
        [
          {
            "node": "Log Extraction Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Event Sources": {
      "main": [
        [
          {
            "node": "Loop Over Event Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for API Error": {
      "main": [
        [
          {
            "node": "Build Error Record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Clean HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Event Fields": {
      "main": [
        [
          {
            "node": "Split Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Extraction Error": {
      "main": [
        [
          {
            "node": "Loop Over Event Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Every Day at 10AM": {
      "main": [
        [
          {
            "node": "Read Event Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Event Sources": {
      "main": [
        [
          {
            "node": "All Sources Processed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Page HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}